根据摩尔定律所说:集成电路上可容纳的晶体管数量每 18 个月翻一番,所以 CPU 上的晶体管数量会愈来愈多。html
但随着时间的推移,集成电路上可容纳的晶体管数量已趋向饱和,摩尔定律也渐渐失效,所以多核 CPU 逐渐变为主流,与之相对应的多线程编程也开始变得普及和流行起来,这固然也是好久以前的事了,对于如今而言多线程编程已经成为程序员必备的职业技能了,那接下来咱们就来盘一盘“线程池”这个多线程编程中最重要的话题。java
线程池(ThreadPool)是一种基于池化思想管理和使用线程的机制。它是将多个线程预先存储在一个“池子”内,当有任务出现时能够避免从新建立和销毁线程所带来性能开销,只须要从“池子”内取出相应的线程执行对应的任务便可。程序员
池化思想在计算机的应用也比较普遍,好比如下这些:数据库
线程池的优点主要体如今如下 4 点:编程
同时阿里巴巴在其《Java开发手册》中也强制规定:线程资源必须经过线程池提供,不容许在应用中自行显式建立线程。数组
说明:线程池的好处是减小在建立和销毁线程上所消耗的时间以及系统资源的开销,解决资源不足的问题。
若是不使用线程池,有可能形成系统建立大量同类线程而致使消耗完内存或者“过分切换”的问题。缓存
知道了什么是线程池以及为什要用线程池以后,咱们再来看怎么用线程池。安全
线程池的建立方法总共有 7 种,但整体来讲可分为 2 类:多线程
ThreadPoolExecutor
建立的线程池;Executors
建立的线程池。线程池的建立方式总共包含如下 7 种(其中 6 种是经过 Executors
建立的,1 种是经过 ThreadPoolExecutor
建立的):并发
单线程池的意义
从以上代码能够看出 newSingleThreadExecutor 和 newSingleThreadScheduledExecutor 建立的都是单线程池,那么单线程池的意义是什么呢?
答:虽然是单线程池,但提供了工做队列,生命周期管理,工做线程维护等功能。
那接下来咱们来看每种线程池建立的具体使用。
建立一个固定大小的线程池,可控制并发的线程数,超出的线程会在队列中等待。
使用示例以下:
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()); }); }
建立一个可缓存的线程池,若线程数超过处理所需,缓存一段时间后会回收,若线程数不够,则新建线程。
使用示例以下:
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 个线程来执行相应的任务。
建立单个线程数的线程池,它能够保证先进先出的执行顺序。
使用示例以下:
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) { } }); } }
执行结果以下:
建立一个能够执行延迟任务的线程池。
使用示例以下:
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 秒以后被执行了,符合咱们的预期。
建立一个单线程的能够执行延迟任务的线程池。
使用示例以下:
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 秒以后被执行了,符合咱们的预期。
建立一个抢占式执行的线程池(任务执行顺序不肯定),注意此方法只有在 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 个参数可供设置。
使用示例以下:
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 最多能够设置 7 个参数,以下代码所示:
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler) { // 省略... }
7 个参数表明的含义以下:
核心线程数,线程池中始终存活的线程数。
最大线程数,线程池中容许的最大线程数,当线程池的任务队列满了以后能够建立的最大线程数。
最大线程数能够存活的时间,当线程中没有任务执行时,最大线程就会销毁一部分,最终保持核心线程数量的线程。
单位是和参数 3 存活时间配合使用的,合在一块儿用于设定线程的存活时间 ,参数 keepAliveTime 的时间单位有如下 7 种可选:
一个阻塞队列,用来存储线程池等待执行的任务,均为线程安全,它包含如下 7 种类型:
较经常使用的是 LinkedBlockingQueue
和 Synchronous
,线程池的排队策略与 BlockingQueue
有关。
线程工厂,主要用来建立线程,默认为正常优先级、非守护线程。
拒绝策略,拒绝处理任务时的策略,系统提供了 4 种可选:
默认策略为 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
http://www.javashuo.com/article/p-miwcjepu-ny.html
关注公众号「Java中文社群」发现更多干货。