Java线程池
一、前言
对Java开发来说,线程池是工作中经常用到的工具,掌握线程池技术是成为一名合格Java程序员的必要条件。
二、为什么使用线程池?
Java编程中,常创建线程去执行需要异步执行的功能,如果这些功能很多,不停的创建线程然后销毁线程,是否非常消耗资源的。
项目中用MySQL时,通常会配置一个数据源连接池,例如Durid。执行一条SQL时会用到连接池里的一条连接,执行完会还给连接池。
Java线程池的思想,我感觉和上面连接池很像,是避免反复创建线程对象的高开销,从而提高效率。
三、线程池的基础概念
1. 最大线程数
说到线程池,首先你得有个抽象的概念,它像一个水池。那一个水池,它肯定有大小,或者说深度,因为无限深的水池不存在的。
那么Java线程池同理,它也得设置一个大小,否则线程无限创建,内存和CPU都会消耗100%。
最大线程数它就是指线程池最大能创建的线程数量。
在ThreadPoolExecutor类中,最大线程数是maximumPoolSize属性。
2. 核心线程数
核心线程数是ThreadPoolExecutor类中corePoolSize属性。
上面已经说了,线程池中最大的线程数是maximumPoolSize属性。
默认设置下,创建线程池后,线程池中线程数量=0,每来一个任务时,线程池中会创建一个线程来执行该任务。
- 当已创建线程数 < corePoolSize时,即使此时线程池中存在空闲线程,也会创建新线程,这些线程称为核心线程。
- 当已创建线程数 == corePoolSize时,又有任务来,会把任务放到阻塞队列中排队。
- 当阻塞队列已满,且已创建线程数<maximumPoolSize,创建非核心线程执行任务。
- 当阻塞队列已满,且已创建线程数==maximumPoolSize,使用拒绝策略。
核心线程数 + 非核心线程数 = 最大线程数
所以设置线程池时,要设置corePoolSize、maximumPoolSize这2个参数。
因为通过计算:非核心线程数 = maximumPoolSize - corePoolSize
注意:上面公式的背景是任务超多,已创建线程数达到maximumPoolSize了。
如果 corePoolSize < 已创建线程数 < maximumPoolSize,也就是线程池还没到上限。
那么:非核心线程数 = 已创建线程数 - corePoolSize
下面看下线程池的流程图,可以更好的理解corePoolSize和maximumPoolSize属性。
3. 线程存活时间
线程存活时间是ThreadPoolExecutor类中keepAliveTime和unit这2个属性共同设置。
keepAliveTime是时间大小,而unit是单位(天、小时、分钟、秒、毫秒、微妙、纳秒)
默认当线程池中已创建线程数 > corePoolSize时,keepAliveTime才会起作用。
即一个空闲线程存活到keepAliveTime了,就会被销毁,直到已创建线程数== corePoolSize。
但是如果设置allowCoreThreadTimeOut(true)时,已创建线程数<= corePoolSize,keepAliveTime也会起作用。
4. 阻塞队列
阻塞队列是ThreadPoolExecutor类中workQueue属性。
一个阻塞队列,用来存储等待执行的任务,这个参数会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:
- ArrayBlockingQueue : 有界的数组队列
- LinkedBlockingQueue : 可支持有界/无界的队列,使用链表实现
- PriorityBlockingQueue : 优先队列,可以针对任务排序
- SynchronousQueue : 队列长度为1的队列,和Array有点区别就是:client thread提交到block queue会是一个阻塞过程,直到有一个worker thread连接上来poll task。
ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。
5. 拒绝策略
拒绝策略是ThreadPoolExecutor类中handler属性。
拒绝策略表示当拒绝处理任务时的策略,有以下四种取值:
- ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
- ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
- ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
- ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务
四、Executors工具类创建线程池
在学线程池时,教材和博客上一般都会介绍Executors工具类下面的几种Java已经实现的线程池。
下面定义一个任务类:
public class MyWorker implements Runnable {
private Integer num;
public MyWorker() {
}
public MyWorker(Integer num) {
this.num = num;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "正在执行,数值:" + num);
try {
//假装工作1秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
1. 可变数量 newCachedThreadPool
创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。
public class Thread01_NewCached_Runnable {
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i=1; i<= 100; i++){
executorService.submit(new MyWorker(i));
}
executorService.shutdown();
}
}
运行结果:
pool-1-thread-1正在执行,数值:1
pool-1-thread-2正在执行,数值:2
。。。。。。
pool-1-thread-98正在执行,数值:98
pool-1-thread-99正在执行,数值:99
pool-1-thread-100正在执行,数值:100
从结果可见线程池创建了100个线程来执行任务,返回结果也非常快。
我们来看newCachedThreadPool的定义:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
属性 | newCachedThreadPool的值 |
---|---|
corePoolSize | 0 |
maximumPoolSize | Integer.MAX_VALUE |
keepAliveTime | 60 |
unit | TimeUnit.SECONDS |
workQueue | SynchronousQueue |
threadFactory | defaultThreadFactory |
handler | AbortPolicy |
2. 固定数量 newFixedThreadPool
创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。
public class Thread03_NewFixed_Runnable {
public static void main(String[] args) {
//固定大小=3的线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);
for (int i=1; i<= 100; i++){
executorService.submit(new MyWorker(i));
}
executorService.shutdown();
}
}
运行结果
pool-1-thread-2正在执行,数值:2
pool-1-thread-3正在执行,数值:3
pool-1-thread-1正在执行,数值:1
pool-1-thread-1正在执行,数值:4
pool-1-thread-3正在执行,数值:5
pool-1-thread-2正在执行,数值:6
pool-1-thread-1正在执行,数值:7
pool-1-thread-2正在执行,数值:9
pool-1-thread-3正在执行,数值:8
pool-1-thread-3正在执行,数值:12
pool-1-thread-2正在执行,数值:10
从结果看出,线程池总共创建了3个线程thread-1,thread-2,thread-3,它们共同完成这100个任务。
因为每个任务耗时1秒,可以肉眼看出,控制台每隔1秒才打印3行,速度比上一个newCachedThreadPool的慢些。
看下newFixedThreadPool的实现:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
属性 | newFixedThreadPool的值 |
---|---|
corePoolSize | nThreads |
maximumPoolSize | nThreads |
keepAliveTime | 0 |
unit | TimeUnit.MILLISECONDS |
workQueue | LinkedBlockingQueue 长度最大Integer.MAX_VALUE |
threadFactory | defaultThreadFactory |
handler | AbortPolicy |
3. 固定一个 newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
public class Thread04_NewSingle_Runnable {
public static void main(String[] args) {
ExecutorService executorService = Executors.newSingleThreadExecutor();
for (int i=1; i<= 100; i++){
executorService.submit(new MyWorker(i));
}
executorService.shutdown();
}
}
pool-1-thread-1正在执行,数值:1
pool-1-thread-1正在执行,数值:2
pool-1-thread-1正在执行,数值:3
。。。。。。
pool-1-thread-1正在执行,数值:99
pool-1-thread-1正在执行,数值:100
从结果看线程池里总共就1个线程thread-1,而且任务按照顺序执行,因为每个任务耗时1秒,结果也是每隔1秒打印1行。这个速度和上面的比时最慢的。
看下newSingleThreadExecutor的实现:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
下面看下newSingleThreadExecutor的实现,并和上面2种一起对比看下。
属性 | newCached | newFixed | newSingle |
---|---|---|---|
corePoolSize | 0 | nThreads | 1 |
maximumPoolSize | Integer.MAX_VALUE | nThreads | 1 |
keepAliveTime | 60 | 0 | 0 |
unit | TimeUnit.SECONDS | TimeUnit.MILLISECONDS | TimeUnit.MILLISECONDS |
workQueue | SynchronousQueue | LinkedBlockingQueue | LinkedBlockingQueue |
threadFactory | defaultThreadFactory | defaultThreadFactory | defaultThreadFactory |
handler | AbortPolicy | AbortPolicy | AbortPolicy |
注意:LinkedBlockingQueue 最长为Integer.MAX_VALUE
4. 定时线程池 newScheduledThreadPool
指定核心线程数量,普通线程数量无限,线程执行完任务立即回收,任务队列为延时阻塞队列。这是一个比较特别的线程池,适用于执行定时或周期性的任务。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
long initialDelay,
long delay,
TimeUnit unit);
定时3秒执行
public class Thread05_NewScheduled_Runnable {
public static void main(String[] args) {
ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(3);
MyRunnable myRunnable = new MyRunnable();
for (int i=0; i< 10; i++){
scheduledThreadPool.schedule(myRunnable, 3, TimeUnit.SECONDS);
}
scheduledThreadPool.shutdown();
}
}
五、为什么阿里巴巴不允许使用Executors
在阿里巴巴的Java开发手册中有一条如下:
避免使用Executors创建线程池,那么我们可以自己直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量。
private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
60L, TimeUnit.SECONDS,
new ArrayBlockingQueue(10));
六、自定义线程池,并验证
自定义一个线程池
- corePoolSize = 2
- maximumPoolSize = 10
- 阻塞队列长度 = 2
public class Thread07_MyThreadPool {
private static ThreadPoolExecutor executor = new ThreadPoolExecutor(2, 10, 1L, TimeUnit.HOURS,
new ArrayBlockingQueue<Runnable>(2), new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r,r.getClass().getSimpleName());
return thread;
}
});
public static void main(String[] args) {
for (int i=1; i<= 13; i++){
executor.submit(new MyWorker(i));
}
executor.shutdown();
}
}
在每个线程运行都不结束的情况下,创建线程;
创建1、2时,线程开始执行,使用核心线程;
创建3、4时,因核心线程占用,3、4进入阻塞队列等待;
创建5、6、7、8、9、10、11、12时,因核心线程被占,而且等待队列也已满。
非核心线程数 = 最大线程数(10) - 核心线程数(2) = 8个。
那线程池创建8个非核心线程,处理5、6、7、8、9、10、11、12这8个任务。
从而已创建线程数 == 最大线程数
此时:
- 1,2 在核心线程
- 3,4 在阻塞队列
- 5、6、7、8、9、10、11、12 这8个在非核心线程
创建13,超出最大线程数限制,报错
1秒后,线程池中有空闲线程了,从阻塞队列中获取3、4并执行。
参考博客:
https://guisu.blog.csdn.net/article/details/7945539
https://blog.csdn.net/weixin_42934146/article/details/108661604