依据摩尔定律所说:集成电路上可包容的晶体管数量每 18 个月翻一番,因而 CPU 上的晶体管数量会越来越多。
但随着工夫的推移,集成电路上可包容的晶体管数量已趋势饱和,摩尔定律也慢慢生效,因而多核 CPU 逐步变为支流,与之绝对应的多线程编程也开始变得遍及和流行起来,这当然也是很久之前的事了,对于当初而言多线程编程曾经成为程序员必备的职业技能了,那接下来咱们就来盘一盘“线程池”这个多线程编程中最重要的话题。
什么是线程池?
线程池(ThreadPool)是一种基于池化思维治理和应用线程的机制。它是将多个线程事后存储在一个“池子”内,当有工作呈现时能够防止从新创立和销毁线程所带来性能开销,只须要从“池子”内取出相应的线程执行对应的工作即可。
池化思维在计算机的利用也比拟宽泛,比方以下这些:
- 内存池(Memory Pooling):事后申请内存,晋升申请内存速度,缩小内存碎片。
- 连接池(Connection Pooling):事后申请数据库连贯,晋升申请连贯的速度,升高零碎的开销。
- 实例池(Object Pooling):循环应用对象,缩小资源在初始化和开释时的低廉损耗。
线程池的劣势次要体现在以下 4 点:
- 升高资源耗费:通过池化技术反复利用已创立的线程,升高线程创立和销毁造成的损耗。
- 进步响应速度:工作达到时,无需期待线程创立即可立刻执行。
- 进步线程的可管理性:线程是稀缺资源,如果无限度创立,不仅会耗费系统资源,还会因为线程的不合理散布导致资源调度失衡,升高零碎的稳定性。应用线程池能够进行对立的调配、调优和监控。
- 提供更多更弱小的性能:线程池具备可拓展性,容许开发人员向其中减少更多的性能。比方延时定时线程池ScheduledThreadPoolExecutor,就容许工作延期执行或定期执行。
同时阿里巴巴在其《Java开发手册》中也强制规定:线程资源必须通过线程池提供,不容许在利用中自行显式创立线程。
阐明:线程池的益处是缩小在创立和销毁线程上所耗费的工夫以及系统资源的开销,解决资源有余的问题。
如果不应用线程池,有可能造成零碎创立大量同类线程而导致耗费完内存或者“适度切换”的问题。
晓得了什么是线程池以及为什要用线程池之后,咱们再来看怎么用线程池。
线程池应用
线程池的创立办法总共有 7 种,但总体来说可分为 2 类:
- 一类是通过
ThreadPoolExecutor
创立的线程池; - 另一个类是通过
Executors
创立的线程池。
线程池的创立形式总共蕴含以下 7 种(其中 6 种是通过 Executors
创立的,1 种是通过 ThreadPoolExecutor
创立的):
- Executors.newFixedThreadPool:创立一个固定大小的线程池,可管制并发的线程数,超出的线程会在队列中期待;
- Executors.newCachedThreadPool:创立一个可缓存的线程池,若线程数超过解决所需,缓存一段时间后会回收,若线程数不够,则新建线程;
- Executors.newSingleThreadExecutor:创立单个线程数的线程池,它能够保障先进先出的执行程序;
- Executors.newScheduledThreadPool:创立一个能够执行提早工作的线程池;
- Executors.newSingleThreadScheduledExecutor:创立一个单线程的能够执行提早工作的线程池;
- Executors.newWorkStealingPool:创立一个抢占式执行的线程池(工作执行程序不确定)【JDK 1.8 增加】。
- ThreadPoolExecutor:最原始的创立线程池的形式,它蕴含了 7 个参数可供设置,前面会具体讲。
单线程池的意义
从以上代码能够看出 newSingleThreadExecutor 和 newSingleThreadScheduledExecutor 创立的都是单线程池,那么单线程池的意义是什么呢?
答:尽管是单线程池,但提供了工作队列,生命周期治理,工作线程保护等性能。
那接下来咱们来看每种线程池创立的具体应用。
1.FixedThreadPool
创立一个固定大小的线程池,可管制并发的线程数,超出的线程会在队列中期待。
应用示例如下:
public static void fixedThreadPool() { // 创立 2 个数据级的线程池 ExecutorService threadPool = Executors.newFixedThreadPool(2); // 创立工作 Runnable runnable = new Runnable() { @Override public void run() { System.out.println("工作被执行,线程:" + Thread.currentThread().getName()); } }; // 线程池执行工作(一次增加 4 个工作) // 执行工作的办法有两种:submit 和 execute threadPool.submit(runnable); // 执行形式 1:submit threadPool.execute(runnable); // 执行形式 2:execute threadPool.execute(runnable); threadPool.execute(runnable);}
执行后果如下:
如果感觉以上办法比拟繁琐,还用更简略的应用办法,如下代码所示:
public static void fixedThreadPool() { // 创立线程池 ExecutorService threadPool = Executors.newFixedThreadPool(2); // 执行工作 threadPool.execute(() -> { System.out.println("工作被执行,线程:" + Thread.currentThread().getName()); });}
2.CachedThreadPool
创立一个可缓存的线程池,若线程数超过解决所需,缓存一段时间后会回收,若线程数不够,则新建线程。
应用示例如下:
public static void cachedThreadPool() { // 创立线程池 ExecutorService threadPool = Executors.newCachedThreadPool(); // 执行工作 for (int i = 0; i < 10; i++) { threadPool.execute(() -> { System.out.println("工作被执行,线程:" + Thread.currentThread().getName()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } }); }}
执行后果如下:
从上述后果能够看出,线程池创立了 10 个线程来执行相应的工作。
3.SingleThreadExecutor
创立单个线程数的线程池,它能够保障先进先出的执行程序。
应用示例如下:
public static void singleThreadExecutor() { // 创立线程池 ExecutorService threadPool = Executors.newSingleThreadExecutor(); // 执行工作 for (int i = 0; i < 10; i++) { final int index = i; threadPool.execute(() -> { System.out.println(index + ":工作被执行"); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } }); }}
执行后果如下:
4.ScheduledThreadPool
创立一个能够执行提早工作的线程池。
应用示例如下:
public static void scheduledThreadPool() { // 创立线程池 ScheduledExecutorService threadPool = Executors.newScheduledThreadPool(5); // 增加定时执行工作(1s 后执行) System.out.println("增加工作,工夫:" + new Date()); threadPool.schedule(() -> { System.out.println("工作被执行,工夫:" + new Date()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } }, 1, TimeUnit.SECONDS);}
执行后果如下:
从上述后果能够看出,工作在 1 秒之后被执行了,合乎咱们的预期。
5.SingleThreadScheduledExecutor
创立一个单线程的能够执行提早工作的线程池。
应用示例如下:
public static void SingleThreadScheduledExecutor() { // 创立线程池 ScheduledExecutorService threadPool = Executors.newSingleThreadScheduledExecutor(); // 增加定时执行工作(2s 后执行) System.out.println("增加工作,工夫:" + new Date()); threadPool.schedule(() -> { System.out.println("工作被执行,工夫:" + new Date()); try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } }, 2, TimeUnit.SECONDS);}
执行后果如下:
从上述后果能够看出,工作在 2 秒之后被执行了,合乎咱们的预期。
6.newWorkStealingPool
创立一个抢占式执行的线程池(工作执行程序不确定),留神此办法只有在 JDK 1.8+ 版本中能力应用。
应用示例如下:
public static void workStealingPool() { // 创立线程池 ExecutorService threadPool = Executors.newWorkStealingPool(); // 执行工作 for (int i = 0; i < 10; i++) { final int index = i; threadPool.execute(() -> { System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName()); }); } // 确保工作执行实现 while (!threadPool.isTerminated()) { }}
执行后果如下:
从上述后果能够看出,工作的执行程序是不确定的,因为它是抢占式执行的。
7.ThreadPoolExecutor
最原始的创立线程池的形式,它蕴含了 7 个参数可供设置。
应用示例如下:
public static void myThreadPoolExecutor() { // 创立线程池 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5, 10, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(10)); // 执行工作 for (int i = 0; i < 10; i++) { final int index = i; threadPool.execute(() -> { System.out.println(index + " 被执行,线程名:" + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } }); }}
执行后果如下:
ThreadPoolExecutor 参数介绍
ThreadPoolExecutor 最多能够设置 7 个参数,如下代码所示:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { // 省略... }
7 个参数代表的含意如下:
参数 1:corePoolSize
外围线程数,线程池中始终存活的线程数。
参数 2:maximumPoolSize
最大线程数,线程池中容许的最大线程数,当线程池的工作队列满了之后能够创立的最大线程数。
参数 3:keepAliveTime
最大线程数能够存活的工夫,当线程中没有工作执行时,最大线程就会销毁一部分,最终放弃外围线程数量的线程。
参数 4:unit:
单位是和参数 3 存活工夫配合应用的,合在一起用于设定线程的存活工夫 ,参数 keepAliveTime 的工夫单位有以下 7 种可选:
- TimeUnit.DAYS:天
- TimeUnit.HOURS:小时
- TimeUnit.MINUTES:分
- TimeUnit.SECONDS:秒
- TimeUnit.MILLISECONDS:毫秒
- TimeUnit.MICROSECONDS:奥妙
- TimeUnit.NANOSECONDS:纳秒
参数 5:workQueue
一个阻塞队列,用来存储线程池期待执行的工作,均为线程平安,它蕴含以下 7 种类型:
- ArrayBlockingQueue:一个由数组构造组成的有界阻塞队列。
- LinkedBlockingQueue:一个由链表构造组成的有界阻塞队列。
- SynchronousQueue:一个不存储元素的阻塞队列,即间接提交给线程不放弃它们。
- PriorityBlockingQueue:一个反对优先级排序的无界阻塞队列。
- DelayQueue:一个应用优先级队列实现的无界阻塞队列,只有在提早期满时能力从中提取元素。
- LinkedTransferQueue:一个由链表构造组成的无界阻塞队列。与SynchronousQueue相似,还含有非阻塞办法。
- LinkedBlockingDeque:一个由链表构造组成的双向阻塞队列。
较罕用的是 LinkedBlockingQueue
和 Synchronous
,线程池的排队策略与 BlockingQueue
无关。
参数 6:threadFactory
线程工厂,次要用来创立线程,默认为失常优先级、非守护线程。
参数 7:handler
回绝策略,回绝解决工作时的策略,零碎提供了 4 种可选:
- AbortPolicy:回绝并抛出异样。
- CallerRunsPolicy:应用以后调用的线程来执行此工作。
- DiscardOldestPolicy:摈弃队列头部(最旧)的一个工作,并执行当前任务。
- DiscardPolicy:疏忽并摈弃当前任务。
默认策略为 AbortPolicy
。
线程池的执行流程
ThreadPoolExecutor 要害节点的执行流程如下:
- 当线程数小于外围线程数时,创立线程。
- 当线程数大于等于外围线程数,且工作队列未满时,将工作放入工作队列。
- 当线程数大于等于外围线程数,且工作队列已满:若线程数小于最大线程数,创立线程;若线程数等于最大线程数,抛出异样,回绝工作。
线程池的执行流程如下图所示:
线程回绝策略
咱们来演示一下 ThreadPoolExecutor 的回绝策略的触发,咱们应用 DiscardPolicy
的回绝策略,它会疏忽并摈弃当前任务的策略,实现代码如下:
public static void main(String[] args) { // 工作的具体方法 Runnable runnable = new Runnable() { @Override public void run() { System.out.println("当前任务被执行,执行工夫:" + new Date() + " 执行线程:" + Thread.currentThread().getName()); try { // 期待 1s TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }; // 创立线程,线程的工作队列的长度为 1 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1), new ThreadPoolExecutor.DiscardPolicy()); // 增加并执行 4 个工作 threadPool.execute(runnable); threadPool.execute(runnable); threadPool.execute(runnable); threadPool.execute(runnable);}
咱们创立了一个外围线程数和最大线程数都为 1 的线程池,并且给线程池的工作队列设置为 1,这样当咱们有 2 个以上的工作时就会触发回绝策略,执行的后果如下图所示:
从上述后果能够看出只有两个工作被正确执行了,其余多余的工作就被舍弃并疏忽了。其余回绝策略的应用相似,这里就不一一赘述了。
自定义回绝策略
除了 Java 本身提供的 4 种回绝策略之外,咱们也能够自定义回绝策略,示例代码如下:
public static void main(String[] args) { // 工作的具体方法 Runnable runnable = new Runnable() { @Override public void run() { System.out.println("当前任务被执行,执行工夫:" + new Date() + " 执行线程:" + Thread.currentThread().getName()); try { // 期待 1s TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } } }; // 创立线程,线程的工作队列的长度为 1 ThreadPoolExecutor threadPool = new ThreadPoolExecutor(1, 1, 100, TimeUnit.SECONDS, new LinkedBlockingQueue<>(1), new RejectedExecutionHandler() { @Override public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) { // 执行自定义回绝策略的相干操作 System.out.println("我是自定义回绝策略~"); } }); // 增加并执行 4 个工作 threadPool.execute(runnable); threadPool.execute(runnable); threadPool.execute(runnable); threadPool.execute(runnable);}
程序的执行后果如下:
到底选用哪种线程池?
通过以上的学习咱们对整个线程池也有了肯定的意识了,那到底该如何抉择线程池呢?
咱们来看下阿里巴巴《Java开发手册》给咱们的答案:
【强制要求】线程池不容许应用 Executors 去创立,而是通过 ThreadPoolExecutor 的形式,这样的解决形式让写的同学更加明确线程池的运行规定,躲避资源耗尽的危险。阐明:Executors 返回的线程池对象的弊病如下:
1) FixedThreadPool 和 SingleThreadPool:容许的申请队列长度为 Integer.MAX_VALUE,可能会沉积大量的申请,从而导致 OOM。
2)CachedThreadPool:容许的创立线程数量为 Integer.MAX_VALUE,可能会创立大量的线程,从而导致 OOM。
所以综上状况所述,咱们举荐应用 ThreadPoolExecutor
的形式进行线程池的创立,因为这种创立形式更可控,并且更加明确了线程池的运行规定,能够躲避一些未知的危险。
总结
本文咱们介绍了线程池的 7 种创立形式,其中最举荐应用的是 ThreadPoolExecutor
的形式进行线程池的创立,ThreadPoolExecutor
最多能够设置 7 个参数,当然设置 5 个参数也能够失常应用,ThreadPoolExecutor
当工作过多(解决不过去)时提供了 4 种回绝策略,当然咱们也能够自定义回绝策略,心愿本文的内容能帮忙到你。原创不易,感觉不错就点个赞再走吧!
参考 & 鸣谢
https://tech.meituan.com/2020/04/02/java-pooling-pratice-in-meituan.html
https://www.cnblogs.com/pcheng/p/13540619.html
关注公众号「Java中文社群」发现更多干货。