【Java多线程】2. 线程池、阻塞队列
作者:互联网
线程池/Executor框架
Executor和所有线程池都由JDK实现,位于JUC包中。
为什么要用线程池:
- 减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,执行多个任务。
- 可以根据系统的承受能力,调整线程池中工作线线程的数目。执行线程过少浪费系统资源,过多造成系统拥挤效率不高,甚至由于消耗太多的内存导致死机。
- 还可以提供定时执行、定期执行、线程中断等功能。
Executor 框架管理多个异步任务的执行,而无需程序员显式地管理线程的生命周期。这里的异步是指多个任务的执行互不干扰,不需要进行同步操作。
Java中,线程池的顶级接口是Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是ExecutorService。
基本参数与工作逻辑
1. 基本参数
corePoolSize
:最大核心线程数量maximumPoolSize
:最大线程(核心+非核心)数量keepAliveTime
与TimeUnit
:前者是线程退出需要满足的空闲时间,不允许小于0。后者是前者的单位。allowCoreThreadTimeout
:是否允许核心线程退出,默认为false。一般不启用,想让没有任务时所有线程都退出可以把corePoolSize
设为0。workQueue
:用来存储等待执行任务的阻塞队列。类型为BlockingQueue<Runnable>
,一般在ArrayBlockingQueue
、LinkedBlockingQueue
、SynchronousQueue
三种阻塞队列中选择。threadFactory
:用来创建线程的线程工厂。handler
:拒绝策略。提供了以下4种取值,都是rejectedExecution
的一种实现,也可以自定义:
- ThreadPoolExecutor.AbortPolicy(默认):丢弃任务,并抛出
RejectedExecutionException
异常。- ThreadPoolExecutor.DiscardPolicy:丢弃任务,但不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)。
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务。
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
2. 处理任务
- 若当前线程数小于
corePoolSize
,创建一个线程来执行任务。 - 若线程数大于等于
corePoolSize
,且任务队列未满,将任务放入任务队列。 - 若线程数大于等于
corePoolSize
,且任务队列已满,若线程数小于maximumPoolSize
,创建一个线程 - 当线程数大于等于
corePoolSize
,且任务队列已满,若线程数等于maximumPoolSize
,按设定的拒绝策略来处理。
3. 关闭线程
规则:
- 默认
allowCoreThreadTimeOut
为false,当有线程空闲时间达到keepAliveTime
,且当前线程数量超过corePoolSize
时,关闭此空闲线程。 - 若设置
allowCoreThreadTimeOut
为true,只要线程空闲时间达到keepAliveTime
,无论当前线程数为多少,都会关闭此空闲线程。
详细来说,有下面几种情况
keepAliveTime
>0 且allowCoreThreadTimeout
为false:空闲线程超时退出,保留核心线程数keepAliveTime
==0 且allowCoreThreadTimeout
为false:线程完成任务立刻退出,保留核心线程数keepAliveTime
>0 且allowCoreThreadTimeout
为true:空闲线程超时退出,不保留核心线程数keepAliveTime
==0 且allowCoreThreadTimeout
为true:不允许。
常见线程池
Executors主要提供了4种线程池:
- CachedThreadPool(没有核心线程,非核心数不限制)
- FixedThreadPool(定长,且只有核心线程)
- SingleThreadExecutor 单一线程池
- ScheduledThreadPool 调度线程池
以及 ForkJoinPool。
1. CachedThreadPool
没有核心线程,非核心数不限制。corePoolSize
为0,maximumPoolSize
为Integer.MAX_VALUE
,keepAliveTime
为60秒,任务队列使用的是SynchronousQueue
。
当有任务时创建线程来执行任务,没有任务时超过60秒回收线程。即一个任务创建一个线程,任务队列中永远没有任务。
适用于耗时少,任务量大的情况。
示例
public static void main(String[] args) {
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
cachedThreadPool.execute(/*这里参数为任务类的实例*/);
}
cachedThreadPool.shutdown();
}
2. FixedThreadPool
定长,且只有核心线程。corePoolSize
为构造给定参数nThreads,maximumPoolSize
也为nThreads,keepAliveTime
为0,任务队列使用的是LinkedBlockingQueue
。
可控制线程最大并发数,超出的线程会在队列中等待。
3. SingleThreadExecutor
单一线程池。相当于大小为 1 的 FixedThreadPool,只会用唯一的工作线程来执行任务,保证所有任务按照阻塞队列规定的出列顺序执行。
规定的出列顺序有:FIFO、LIFO、优先级。
4. ScheduledThreadPool
调度线程池。定长,支持定时及周期性任务执行。
与Timer比较:
Timer的内部只有一个线程,如果有多个任务的话就会顺序执行,这样延迟时间和循环时间就会出现问题。
而ScheduledExecutorService不会出现这种情况,更加安全。
因此在对延迟任务和循环任务要求严格的时候,应考虑使用ScheduledExecutorService比Timer。
展开代码
//注意这里的声明不太一样,用了ExecutorService的子类ScheduledExecutorService
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);
//延时三秒执行
scheduledThreadPool.schedule(new Runnable() {
@Override
public void run() {
System.out.println("delay 3 seconds");
}
}, 3, TimeUnit.SECONDS);
//延时1秒,并且每三秒执行一次
scheduledThreadPool.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
System.out.println("delay 1 seconds, and excute every 3 seconds");
}
}, 1, 3, TimeUnit.SECONDS);
5. ForkJoinPool
阻塞队列 BlockingQueue
阻塞:就是在某些情况下会挂起线程。(而一旦条件满足,被挂起的线程又会自动被唤醒。)
线程池两种常见的阻塞情况:
- 队列为空时,消费者被阻塞,直到队列中加入数据。
- 队列已满时,生产者被阻塞,直到队列中有空的位置。
JUC中的 BlockingQueue 接口继承了Queue接口,提供了以下两种阻塞方法:
put(E e)
:如果阻塞队列没有空间,则调用此方法的线程会被阻塞直到有空间再继续。take()
:若阻塞队列为空,则调用此方法的线程会被阻塞直到有元素再继续。
具体实现有:
- FIFO 队列 :LinkedBlockingQueue、ArrayBlockingQueue
- 优先级队列 :PriorityBlockingQueue
- 同步队列:SynchronousQueue
1.ArrayBlockingQueue
基于数组实现的阻塞队列。
内部维护了一个定长数组,来缓存队列中的数据对象,以及两个整形变量,分别标识着队列的头部和尾部在数组中的位置。ArrayBlockingQueue 在插入或删除元素时不会产生或销毁任何额外的对象。
生产者和消费者共用同一个锁对象,因此二者的操作无法真正并行运行。而在创建ArrayBlockingQueue时,可以控制内部锁是否采用公平锁,默认采用非公平锁。
2. LinkedBlockingQueue
基于链表实现的阻塞队列。
内部维护了一个链表,来缓存队列中的数据对象。(区别 1)
生产者和消费者分别采用了独立的锁,因此二者可以并行地操作队列中的数据,以此提高了整个队列的并发性能。(区别 2)
如果构造一个LinkedBlockingQueue,而没有指定其容量大小,会默认一个类似无限大小的容量(Integer.MAX_VALUE)。这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
LinkedBlockingQueue 在插入或删除元素时会生成一个额外的Node对象,在长时间需要高效并发处理大批量数据的系统中,对于GC的影响还是存在一定的影响。(区别 3)
LinkedBlockingQueue 只能使用默认的非公平锁。(区别 4)
标签:Java,队列,keepAliveTime,阻塞,corePoolSize,任务,线程,多线程 来源: https://www.cnblogs.com/BWSHOOTER/p/14372865.html