正文
三、四种线程池解析
Executors.newSingleThreadExecutor();
package com.xiaojie.juc.thread.pool; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author xiaojie * @version 1.0 * @description: 单线程线程池 * @date 2021/12/12 22:14 */ public class SingleThreadExecutorDemo { public static void main(String[] args) { ExecutorService executorService = Executors.newSingleThreadExecutor(); for (int i=0;i<5;i++){ int finalI = i; executorService.execute(() -> { try { Thread.sleep(30); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("这是一个单线程线程池的demo,线程名称:"+Thread.currentThread().getName()+">>>>>>>"+ finalI); }); } //关闭线程池 executorService.shutdown(); } }
由执行结果可知
1、单线程线程池中的任务是按照提交任务的顺序执行的。
2、池中唯一的线程存活时间是无限制的。
3、当池中的线程正在繁忙时,新提交的任务会进入内部阻塞队列,并且阻塞队列是无界的(LinkedBlockingQueue<Runnable>)。
适用场景
单线程线程池适用于任务按照提交次序,一个任务一个任务的逐个执行的场景。
Executors.newFixedThreadPool
package com.xiaojie.juc.thread.pool; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * @author xiaojie * @version 1.0 * @description: 固定长度的线程池 * @date 2021/12/12 22:29 */ public class FixedThreadPoolDemo { public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(3); for (int i = 0; i < 10; i++) { int finalI = i; executorService.execute(new Runnable() { @Override public void run() { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("这是一个固定长度的线程池的demo,线程名称:" + Thread.currentThread().getName() + ">>>>>>>" + finalI); } }); } executorService.shutdown(); } }
有执行结果可知
1、并不是按照任务提交顺序执行的。
2、如果线程数量没有达到固定数量,每次提交都会创建新的线程,直到达到最大数量
3、如果线程洗的大小达到固定数量就会保持不变,如果某个线程因为异常而结束,那么线程池会补充一个新的线程。
4、如果接收到新任务没有空闲线程也会进入阻塞队列(LinkedBlockingQueue<Runnable>)。
适用场景:需要任务长期执行的场景,固定数量的线程数能够避免频繁的创建和销毁线程,例如CPU密集型的任务,在CPU被工作线程长时间占用的情况下,能确保尽可能减少线程分配。
弊端:
内部使用无界队列来存放任务,当大量任务超过线程池能处理的最大容量时队列无限增大,使服务器资源迅速耗尽。
Executors.newCachedThreadPool()
package com.xiaojie.juc.thread.pool; import java.util.concurrent.*; /** * @author xiaojie * @version 1.0 * @description: 缓存功能的线程池 * @date 2021/12/12 20:31 */ public class CachedThreadPoolDemo { public static void main(String[] args) { ExecutorService executorService = Executors.newCachedThreadPool(); for (int i = 0; i < 10; i++) { int finalI = i; executorService.execute(new Runnable() { @Override public void run() { try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("这是一个缓存功能的线程池的demo,线程名称:" + Thread.currentThread().getName() + ">>>>>>>" + finalI); } }); } executorService.shutdown(); } }
执行结果可知
1、当任务提交时,如果线程繁忙,会创建新的线程执行任务。
2、对线程池的大小没有限制,底层使用SynchronousQueue<Runnable>队列。
3、如果部分线程空闲,线程数量超过了任务数量,就会回收空闲(60秒不执行任务)线程。
适用场景
需要快速处理突发性强,耗时较短的任务场景,例如Netty的NIO场景,RESTAPI瞬时削峰。可缓存线程池的线程数量不固定,有空闲线程就会自动回收,接收到新任务时判断是否有空闲线程,如果没有就直接创建新的线程。
弊端
线程池没有最大线程数量限制,如果大量的异步任务同时执行,可能会因创建线程过多而导致资源耗尽。
Executors.newScheduledThreadPool
package com.xiaojie.juc.thread.pool; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.TimeUnit; /** * @author xiaojie * @version 1.0 * @description: 定时,延迟线程池 * @date 2021/12/12 23:08 */ public class ScheduledThreadPoolDemo { public static void main(String[] args) { ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(3); for (int i = 0; i < 10; i++) { int finalI = i; scheduledExecutorService.schedule(() -> { System.out.println("这是一个延迟线程池的demo,延迟5秒后执行,线程名称:" + Thread.currentThread().getName() + ">>>>>>>" + finalI); }, 5, TimeUnit.SECONDS); scheduledExecutorService.scheduleAtFixedRate(new Runnable() { @Override public void run() { System.out.println("这个方法延迟1秒后执行,然后每隔1秒后重复执行"); } }, 1, 1, TimeUnit.SECONDS); } // scheduledExecutorService.shutdown(); } }
使用 DelayedWorkQueue()队列实现
适用场景
周期性的执行任务的场景,例如一些定时任务的实现,Springboot的任务调度。
四、自定义线程池
Executors创建线程的潜在问题
1、创建newFixedThreadPool的潜在问题在于工作队列,使用LinkedBlockingQueue(无界队列),如果任务的提交速度大于任务的处理速度,就会造成大量的任务在阻塞队列中等待,如果阻塞队列很大,很有可能导致OOM(内存溢出)。
2、创建newSingleThreadExecutor和newFixedThreadPool线程池一样,同样使用LinkedBlockingQueue(无界队列),如果任务的提交速度大于任务的处理速度,就会造成大量的任务在阻塞队列中等待,如果阻塞队列很大,很有可能导致OOM(内存溢出)。
3、newCachedThreadPool线程池的潜在问题在于其核心线程数为0,最大线程数为Integer.MAX_VALUE,使用SynchronousQueue同步队列。如果同时执行大量的任务,就意味会创建大量的线程,可能导致OOM,甚至导致CPU资源耗尽。
4、ScheduledThreadPoolExecutor最大线程数也是Integer.MAX_VALUE,和newCachedThreadPool存在同样的问题。
自己创建线程池
package com.xiaojie.juc.thread.pool; import java.util.concurrent.BlockingQueue; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** * @author xiaojie * @version 1.0 * @description: 自定义创建线程池 * @date 2021/12/12 23:56 */ public class MyThreadPool { //定义工作队列 BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(10); public ThreadPoolExecutor threadPoolExecutor(int corePoolSize, int maximumPoolSize, Long keepAliveTime) { return new ThreadPoolExecutor(corePoolSize, maximumPoolSize, 0L, TimeUnit.MILLISECONDS, workQueue); } public static void main(String[] args) { MyThreadPool myThreadPool = new MyThreadPool(); ThreadPoolExecutor executor = myThreadPool.threadPoolExecutor(3, 10, 60L); for (int i = 0; i < 20; i++) { executor.execute(() -> { //最大可以允许20个任务,超过的将进行拒绝策略 System.out.println("通过ThreadPoolExecutor 定义的线程池" + Thread.currentThread().getName()); }); } } }
五、如何确定线程池线程数
1、由于IO密集型任务的CPU使用率低,导致线程空闲时间很多,因此通常需要开CPU 核心数两倍的线程。当IO线程空闲时,可以启用其他线程继续使用CPU,来提高CPU的利用率。
2、如果是CPU密集型,CPU密集型的任务虽然可以并行的执行,但是并行的任务越多,花在线程切换的时间就越多,CPU执行效率就越低,所以一般设置线程数等于CPU的核心数。
3、混合型既要满足IO又要满足CPU密集的计算公式
最佳线程数=((线程等待时间+线程CPU时间)/线程CPU时间)*CPU核数
参考:《JAVA高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计》-尼恩编著