文章目录
阻塞队列与线程池
阻塞队列
阻塞队列类型
有七种类型的阻塞队列,但常用的线程池中主要用到三种阻塞队列,所以主要看看这三种阻塞队列。
- ArrayBlockingQueue:由数组构成的有界阻塞队列,如果工作中用到阻塞队列,这个是首选。
- LinkedBlockingQueue:由链表构成的有界(但长度默认为Integer.MAX_VALUE)的阻塞队列。如果使用,一定要注意设置默认值。
- SynchronousQueue:只有一个元素的阻塞队列,即存即用。每个一个put操作必须被take后才能继续存入,反之亦然。
入队出队操作
方法类型 | 抛出异常 | 特殊值 | 阻塞 | 超时 |
---|---|---|---|---|
插入 | add(e) | offer(e) | put(e) | offer(e, time, unit) |
移除 | remove() | poll() | take() | poll(time, unit) |
检查(队首,判空) | element() | peek() | 不可用 | 不可用 |
生产消费者模式
传统版v1版本
使用 synchronized, wait, notifyAll
class SharedData{
private int number = 0;
public synchronized void increment() {
while (number != 0) {
try {
//不生产,使用while循环判断防止虚假唤醒
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number++;
System.out.println(Thread.currentThread().getName() + "生产,number == " + number);
notifyAll();
}
public synchronized void decrement() {
while (number == 0) {
try {
//不消费
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
number--;
System.out.println(Thread.currentThread().getName() + "消费,number == " + number);
notifyAll();
}
}
/**
* 初始化一个0,两个线程对其操作,生产一个就消费一个(线程操作资源类)
*/
public class ProductConsumerTraditionTest {
public static void main(String[] args) {
SharedData sharedData = new SharedData();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
sharedData.increment();
}
}, "t1").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
sharedData.decrement();
}
}, "t2").start();
}
}
t1生产,number == 1
t2消费,number == 0
t1生产,number == 1
t2消费,number == 0
t1生产,number == 1
t2消费,number == 0
t1生产,number == 1
t2消费,number == 0
t1生产,number == 1
t2消费,number == 0Process finished with exit code 0
传统版v2版本
使用ReentrantLock, Condition.await, Condition.signalAll
class SharedData2 {
private int number = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment() {
lock.lock();
try {
while (number != 0) {
//不生产,使用while循环判断防止虚假唤醒
condition.await();
}
number++;
System.out.println(Thread.currentThread().getName() + "生产,number == " + number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrement() {
lock.lock();
try {
while (number == 0) {
//不消费
condition.await();
}
number--;
System.out.println(Thread.currentThread().getName() + "消费,number == " + number);
condition.signalAll();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
/**
* 初始化一个0,两个线程对其操作,生产一个就消费一个(线程操作资源类)
*/
public class ProductConsumerTraditionTest2 {
public static void main(String[] args) {
SharedData2 sharedData = new SharedData2();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
sharedData.increment();
}
}, "t1").start();
new Thread(() -> {
for (int i = 0; i < 5; i++) {
sharedData.decrement();
}
}, "t2").start();
}
}
由于synchronized和ReentrantLock都保证了内存可见性,所以资源变量不需要用volatile修饰。
阻塞队列v3版
使用BlockQueue,我们不需要关心什么时候阻塞线程,什么时候唤醒线程,具体的细节BlockQueue帮我们实现了。否则我们要实现业务逻辑,自己控制这些细节还要兼顾性能和线程安全,大大增加了开发难度。
class SharedData3 {
//开启,生产+消费
private volatile boolean FLAG = true;
private AtomicInteger mAtomicInteger = new AtomicInteger();
//这里传的是接口
private BlockingQueue<String> mBlockingQueue = null;
public SharedData3(BlockingQueue<String> blockingQueue) {
mBlockingQueue = blockingQueue;
System.out.println(mBlockingQueue.getClass().getName());
}
public void myProduct() throws Exception {
String data;
boolean retValue;
while (FLAG) {
data = mAtomicInteger.incrementAndGet() + "";
retValue = mBlockingQueue.offer(data, 2L, TimeUnit.SECONDS);
if (retValue) {
System.out.println(Thread.currentThread().getName() + "," + data + "插入成功");
} else {
System.out.println(Thread.currentThread().getName() + "," + data + "插入失败");
}
TimeUnit.SECONDS.sleep(1);
}
System.out.println(Thread.currentThread().getName() + ",生产停止,FLAG = false");
}
public void myConsumer() throws Exception {
String data;
while (FLAG) {
data = mBlockingQueue.poll(2L, TimeUnit.SECONDS);
if (null == data || "".equals(data)) {
FLAG = false;
System.out.println(Thread.currentThread().getName() + ",超过2秒没有取到数据,消费退出");
return;
}
System.out.println(Thread.currentThread().getName() + "," + data + "取出成功");
}
}
public void stop() {
this.FLAG = false;
}
}
public class ProductConsumerTraditionTest3 {
public static void main(String[] args) {
SharedData3 mSharedData3 = new SharedData3(new ArrayBlockingQueue<>(10));
new Thread(() -> {
System.out.println("生产线程启动");
try {
mSharedData3.myProduct();
} catch (Exception e) {
e.printStackTrace();
}
}, "product").start();
new Thread(() -> {
System.out.println("消费线程启动");
try {
mSharedData3.myConsumer();
} catch (Exception e) {
e.printStackTrace();
}
}, "consume").start();
try {
Thread.sleep(5000);
mSharedData3.stop();
System.out.println("5s时间到,MainThread叫停,生产消费结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
java.util.concurrent.ArrayBlockingQueue
生产线程启动
product,1插入成功
消费线程启动
consume,1取出成功
product,2插入成功
consume,2取出成功
product,3插入成功
consume,3取出成功
consume,4取出成功
product,4插入成功
product,5插入成功
consume,5取出成功
5s时间到,MainThread叫停,生产消费结束
product,生产停止,FLAG = false
consume,超过2秒没有取到数据,消费退出Process finished with exit code 0
线程池
线程池
线程池的主要工作是控制运行的线程数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务。如果线程数量超过了线程池最大线程数则排队等候,等其他线程执行完毕后,再从队列中取出任务来执行。
其主要特点为:线程复用;控制最大并发数、管理线程。
- 降低资源消耗:通过复用已创建的线程降低线程频繁地创建和销毁的消耗(创建和销毁大概2M)。
- 提高响应速度:当任务达到时,任务可以不需要等到线程创建就能立即执行。
- 提高现成的可管理性:线程是稀缺资源,如果无限制创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一地分配,调优和监控。
几种线程池
public class ThreadPoolTest {
public static void main(String[] args) {
//定长为5的线程池
// ExecutorService threadPool = Executors.newFixedThreadPool(5);
//单任务线程池
// ExecutorService threadPool = Executors.newSingleThreadExecutor();
//无界线程池
ExecutorService threadPool = Executors.newCachedThreadPool();
//模拟10个用户来办理业务
try {
for (int i = 0; i < 10; i++) {
threadPool.execute(() -> {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + ",办理业务");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
-
newFixedThreadPool:
/** * Creates a thread pool that reuses a fixed number of threads * operating off a shared unbounded queue. At any point, at most * {@code nThreads} threads will be active processing tasks. * If additional tasks are submitted when all threads are active, * they will wait in the queue until a thread is available. * If any thread terminates due to a failure during execution * prior to shutdown, a new one will take its place if needed to * execute subsequent tasks. The threads in the pool will exist * until it is explicitly {@link ExecutorService#shutdown shutdown}. * * @param nThreads the number of threads in the pool * @return the newly created thread pool * @throws IllegalArgumentException if {@code nThreads <= 0} */ public static ExecutorService newFixedThreadPool(int nThreads) { return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>()); }
定长线程池,适合执行长期任务。其底层数据结构使用的是LinkedBlockingQueue。
-
newSingleThreadExecutor
/** * Creates an Executor that uses a single worker thread operating * off an unbounded queue. (Note however that if this single * thread terminates due to a failure during execution prior to * shutdown, a new one will take its place if needed to execute * subsequent tasks.) Tasks are guaranteed to execute * sequentially, and no more than one task will be active at any * given time. Unlike the otherwise equivalent * {@code newFixedThreadPool(1)} the returned executor is * guaranteed not to be reconfigurable to use additional threads. * * @return the newly created single-threaded Executor */ public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService (new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>())); }
单任务线程池,适用于一个任务一个任务执行的场景,线程池中只有一个线程。其底层数据结构使用的是LinkedBlockingQueue。
-
newCachedThreadPool
/** * Creates a thread pool that creates new threads as needed, but * will reuse previously constructed threads when they are * available. These pools will typically improve the performance * of programs that execute many short-lived asynchronous tasks. * Calls to {@code execute} will reuse previously constructed * threads if available. If no existing thread is available, a new * thread will be created and added to the pool. Threads that have * not been used for sixty seconds are terminated and removed from * the cache. Thus, a pool that remains idle for long enough will * not consume any resources. Note that pools with similar * properties but different details (for example, timeout parameters) * may be created using {@link ThreadPoolExecutor} constructors. * * @return the newly created thread pool */ public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue<Runnable>()); }
无界线程池,线程池中线程数量不固定,可以自动扩容。适合执行很多短期异步的小程序或者负载较轻的服务。其底层数据结构使用的是SynchronousQueue。
线程池七大参数
看源码可以看到:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
ThreadPoolExecutor有五个参数,点进去:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
this,为该类的另一构造方法,再点进去:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
可以看到实际上有七个参数,封装了两个,还有五个参数暴露给开发者自定义。
- corePoolSize:线程池中的常驻核心线程数。
- maximumPoolSize:线程池能够容纳的同时执行的最大线程数,此值必须大于1。
- keepAliveTime:多余的空闲线程存活时间。。。
- unit:参数三的时间单位。
- BlockingQueue:阻塞队列,被提交但尚未被执行的任务队列。
- ThreadFactory:生成线程池中工作线程的工厂,用于创建线程,一般设置为默认即可。
- RejectedExecutionHandler:拒绝策略,表示当队列满了,并且工作线程数大于等于线程池中的最大线程数(maximumPoolSize)
线程池运行过程
- 假如corePoolSize=2,maximumPoolSize=5,此时常驻线程已经被占用且正在执行任务,又进来三个线程,这三个线程并不会执行任务,而是放在阻塞队列中,等待核心线程执行完毕。假设阻塞队列的长度=maximumPoolSize-corePoolSize=3(实际上阻塞队列长度取决于底层阻塞队列的数据结构,LinkedBlockingQueue默认长度为Integer.MAX_VALUE),假设又进来两个线程,此时常驻核心线程数已满,阻塞队列也满了,线程池开始扩容,使得corePoolSize = maximumPoolSize = 5,阻塞队列里的任务加入到新扩容的线程中,后进来的两个线程放入阻塞队列中。
- 上述情况,假设扩容完成后,corePoolSize = maximumPoolSize = 5满了,阻塞队列也满了,此时就需要拒绝策略,也就是RejectedExecutionHandler参数。
- 随着时间的推移,假设任务并不多了,所有线程任务都执行完毕了,这时当前线程数量超过corePoolSize,当空闲时间达到keepAliveTime时,多余的空闲线程就会被销毁知道只剩下corePoolSize个线程数。
- 定长线程池看其构造方法可以看到:corePoolSize == maximumPoolSize。
线程池拒绝策略
- AbortPolicy(默认):直接抛出RejectedExecutionException异常,阻止系统正常运行。
- CallRunsPolicy:调用者运行策略,一种调节机制,该策略既不会抛弃任务,也不会抛弃异常,而是将某些任务回退到调用者,从而降低新任务的流量。
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入到队列中尝试再次提交当前任务。
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛出异常。如果允许丢弃任务,这个是最好的方案。
使用哪种线程池
拒绝策略验证
先来看看AbortPolicy什么时候会报异常:
public class CustomThreadPoolTest {
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(
2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
try {
for (int i = 0; i < 9; i++) {
threadPool.execute(() -> {
System.out.println(Thread.currentThread().getName() + "执行任务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
根据前面总结,可以猜到,当需要执行的任务 = maximumPoolSize + 阻塞队列长度时,会报RejectedExecutionException,运行结果如下:
pool-1-thread-2执行任务
pool-1-thread-2执行任务
pool-1-thread-2执行任务
pool-1-thread-2执行任务
pool-1-thread-1执行任务
pool-1-thread-3执行任务
pool-1-thread-4执行任务
pool-1-thread-5执行任务
java.util.concurrent.RejectedExecutionException
将拒绝策略改成:CallerRunsPolicy,可以看到:
pool-1-thread-2执行任务
main执行任务
pool-1-thread-2执行任务
pool-1-thread-1执行任务
pool-1-thread-2执行任务
main执行任务
pool-1-thread-2执行任务
pool-1-thread-1执行任务
pool-1-thread-3执行任务
pool-1-thread-4执行任务
pool-1-thread-5执行任务Process finished with exit code 0
没有报异常,且拒绝策略是返回该调用者线程执行。
再将拒绝策略改成:DiscardOldestPolicy,可以看到:
pool-1-thread-2执行任务
pool-1-thread-3执行任务
pool-1-thread-1执行任务
pool-1-thread-1执行任务
pool-1-thread-3执行任务
pool-1-thread-2执行任务
pool-1-thread-5执行任务
pool-1-thread-4执行任务Process finished with exit code 0
丢弃了两个任务,且丢弃的两个任务是阻塞队列中等待最久的任务。
再看看最后一个拒绝策略
pool-1-thread-1执行任务
pool-1-thread-2执行任务
pool-1-thread-2执行任务
pool-1-thread-2执行任务
pool-1-thread-1执行任务
pool-1-thread-3执行任务
pool-1-thread-4执行任务
pool-1-thread-5执行任务Process finished with exit code 0
当阻塞队列满了,直接拒绝准备新加入的两个任务。
自定义线程池
以上参数的配置需要根据不同业务场景配置不同的线程池,主要分为以下两种。
-
CPU密集型:任务需要大量CPU运算,但没有阻塞,CPU一直全速运行,所以配置尽可能是少的线程数
一般公式:CPU核数 + 1,或者CPU核数*2,一般设置CPU * 2即可。
-
IO密集型:任务中有大量的IO操作,容易阻塞,单线程阻塞会浪费CPU资源,所以配置多一点的线程数
一般公式:CPU核数 / (1 - 阻塞系数),阻塞系数在0.8-0.9之间,比如8核CPU,8/(1-0.9) = 80个线程数。
这里以CPU密集型为例自定义线程池:
/**
* 静态内部类单例,懒加载
*/
public class CPUThreadPool {
private ThreadPoolExecutor executor;
/**
* CPU密集型线程池
* corePoolSize:常驻线程,为CPU线程数
* maximumPoolSize:最大线程数,为corePoolSize的两倍,阻塞队列满了扩容会使用
*/
private CPUThreadPool() {
/**
* 给corePoolSize赋值:当前设备可用处理器核心数*2 + 1,能够让cpu的效率得到最大程度执行(有研究论证的)
*/
//核心常驻线程池的数量,同时能够执行的线程数量
int corePoolSize = Runtime.getRuntime().availableProcessors();
//最大线程池数量,表示当缓冲队列满的时候能继续容纳的等待任务的数量
int maximumPoolSize = corePoolSize * 2;
//非常驻线程存活时间
long keepAliveTime = 1L;
//非常驻线程存活时间单位
TimeUnit unit = TimeUnit.MINUTES;
executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
new SynchronousQueue<>(),//这里阻塞队列长度为1,由于不能设置为0,所以可能存在1个任务被阻塞的隐患,但测试中没有出现过
// new LinkedBlockingQueue<>(1),
Executors.defaultThreadFactory(),
new NewThreadForRejectHandler()
);
}
private static class ThreadPoolHolder {
private static CPUThreadPool mInstance = new CPUThreadPool();
}
public static CPUThreadPool getInstance() {
return ThreadPoolHolder.mInstance;
}
//自定义拒绝策略:被拒绝的任务在一个新的线程中执行
private class NewThreadForRejectHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable runnable, ThreadPoolExecutor executor) {
new Thread(runnable, "new thread for reject").start();
System.out.println(("新线程中的任务被拒绝:Task " + runnable.toString() + "rejected from " + executor.toString()));
// LogUtils.e("新线程中的任务被拒绝:Task " + runnable.toString() + "rejected from " + executor.toString());
}
}
/**
* 执行任务
*/
public void execute(Runnable runnable) {
if (runnable == null || executor == null) return;
executor.execute(runnable);
}
/**
* 从线程池中移除任务
*/
public void remove(Runnable runnable) {
if (runnable == null || executor == null) return;
executor.remove(runnable);
}
/**
* 关闭线程池
* 不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
*/
public void shutdown() {
if (executor == null) return;
executor.shutdown();
}
/**
* 线程池中任务是否执行完
*
* @return
*/
public boolean isTerminated() {
if (executor == null) return false;
return executor.isTerminated();
}
public int getActiveThreadCount() {
return executor.getActiveCount();
}
}