概念: 线程池主要是控制运⾏线程的数量,将待处理任务放到等待队列,然后创建线程执⾏这些任务。 如果超过了最⼤线程数,则等待。
为什么⽤线程池?
10年前单核CPU电脑,假的多线程,像⻢戏团⼩丑玩多个球,CPU需要来回切换。
现在是多核电脑,多个线程各⾃跑在独⽴的CPU上,不⽤切换效率⾼。
线程池的优点:
线程池做的⼯作主要是控制运⾏的线程数量,处理过程中将任务放⼊队列,然后在线程创建后启动这些任务,如果线程数量超过了最⼤数量,超出数量的线程排队等候,等其他线程执⾏完毕,再从队列中取出任务来执⾏。
线程池的主要特点为:线程复⽤;控制最⼤并发数;管理线程。
体系: Executor→→ExecutorService→AbstractExecutorService→ThreadPoolExecutor。
ThreadPoolExecutor是线程池创建的核⼼类。类似Arrays、Collections工具类,Executor也有自己的工具类Executors。
使⽤ LinkedBlockingQueue实现,定⻓线程池。
特点:执⾏⻓期任务性能好,创建⼀个线程池,⼀池有N个固定的线程,有固定线程数的线程
使⽤ LinkedBlockingQueue实现,⼀池只有⼀个线程。
特点:⼀个任务⼀个任务的执⾏,⼀池⼀线程
使⽤ SynchronousQueue实现,变⻓线程池。
特点:执⾏很多短期异步任务,线程池根据需要创建新线程,但在先前构建的线程可⽤时将重⽤他们。 可扩容,遇强则强
任务类
模拟十个客户来办理业务
private static void threadPoolTask(ExecutorService threadPool) {//模拟有10个顾客来办理业务try {for (int i = 1; i <= 10; i++) {threadPool.execute(() -> {System.out.println(Thread.currentThread().getName() + "\t办理业务");try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}});}} catch (Exception e) {e.printStackTrace();} finally {threadPool.shutdown();}}
//一个池子有5个工作线程,类似银行有5个受理窗口threadPoolTask(Executors.newFixedThreadPool(5));
创建newSingleThreadExecutor线程池,观察结果发现只有一个线程可以使用。
System.out.println("======Single Thread Pool=========");// //一个池子有1个工作线程,类似银行有1个受理窗口threadPoolTask( Executors.newSingleThreadExecutor() );
System.out.println("=====Cached Thread Pool=======");// //不定量线程,一个池子有N个工作线程,类似银行有N个受理窗口threadPoolTask( Executors.newCachedThreadPool() );
问题:
上述我们使用10个客户来模拟,如果用100个呢,我们来观察结果
参数 | 意义 |
---|---|
corePoolSize | 线程池中的常驻核⼼线程数 |
maximumPoolSize | 线程池中能够容纳同时并发的最⼤线程数 ,此值必须⼤于等于1 |
keepAliveTime | 多余的空闲线程的存活时间 ,当前池中线程数量超过corePoolSize时,当空闲时间达到keepAliveTime时,多余线程会被销毁直到只剩下corePoolSize个线程为⽌ |
unit | keepAliveTime存活时间的单位 |
workQueue | 任务队列 ,存放已提交但尚未执⾏的任务 |
threadFactory | 表示⽣成线程池中⼯作线程的线程⼯⼚ ,⽤于创建线程,⼀般默认的即可 |
handler | 拒绝策略 ,表示当队列满了,并且⼯作线程⼤于等于线程池的最⼤线程数(maximumPoolSize)时,如何来拒绝请求执⾏的runnable的策略 |
线程池的创建参数,就像⼀个银⾏。
corePoolSize
就像银⾏的“当值窗⼝“,⽐如今天有2位柜员在受理客户请求(任务)。workQueue
)等待。maximumPoolSize
,为5个。handler
,告诉不断涌⼊的客户, 叫他们不要进⼊,已经爆满了。keepAlivetTime
将多余的3个”加班窗⼝“取消,恢复到2个”当值窗⼝“上⾯银⾏的例⼦,实际上就是线程池的⼯作原理。
流程:
- 在创建了线程池后,开始等待请求。
- 当调⽤
execute()
⽅法添加⼀个请求任务时,线程池会做出如下判断:
2.1 如果正在运⾏的线程数量⼩于corePoolSize
,那么⻢上创建核⼼线程运⾏执⾏这个任务;
2.2 如果正在运⾏的线程数量⼤于或等于corePoolSize
,那么将这个任务放⼊队列;
2.3 如果这个时候等待队列已满,且正在运⾏的线程数量⼩于maximumPoolSize
,那么还是要创
建⾮核⼼线程⽴刻运⾏这个任务;
2.4 如果这个时候等待队列已满,且正在运⾏的线程数量⼤于或等于maximumPoolSize
,那么线程池会启动饱和拒绝策略来执⾏。- 当⼀个线程完成任务时,它会从等待队列中取出下⼀个任务来执⾏。
- 当⼀个线程⽆事可做超过⼀定的时间(
keepAliveTime
)后,线程会判断:
如果当前运⾏的线程数⼤于corePoolSize
,那么这个⾮核⼼线程就被停掉。当线程池的所有任
务完成后,它最终会收缩到corePoolSize
的⼤⼩。
《Java 开发⼿册》是阿⾥巴巴集团技术团队:
当等待队列满时,且达到最⼤线程数,再有新任务到来,就需要启动拒绝策略。JDK提供了四种拒绝策
略,分别是:
AbortPolicy
:默认的策略,直接抛出 RejectedExecutionException异常,阻⽌系统正常运⾏。CallerRunsPolicy
:既不会抛出异常,也不会终⽌任务,⽽是将任务返回给调⽤者,从⽽降低新任务的流量。DiscardOldestPolicy
:抛弃队列中等待最久的任务,然后把当前任务加⼊队列中尝试再次提交任务。DiscardPolicy
:该策略默默地丢弃⽆法处理的任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的⼀种策略。单⼀、可变、定⻓都不⽤!原因就是FixedThreadPool
和 SingleThreadExecutor
底层都是⽤ LinkedBlockingQueue
实现的,这个队列最⼤⻓度为 Integer.MAX_VALUE
,显然会导致OOM
。所以实际⽣产⼀般⾃⼰通过的7个参数,⾃定义线程池
System.out.println("=====Custom Thread Pool=======");threadPoolTask( new ThreadPoolExecutor(2,5,1L,TimeUnit.SECONDS,new LinkedBlockingQueue<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.DiscardPolicy()));
结果分析
对于CPU密集型任务,最⼤线程数是CPU线程数+1。
对于IO密集型任务(文件上传下载),尽量多配点,可以是CPU线程数*2,或者CPU线程数/(1-阻塞系数)。
IO密集型,即该任务需要⼤量的IO,即⼤量的阻塞。
在单线程上运⾏IO密集型的任务会导致浪费⼤量的CPU运算能⼒浪费在等待。
所以在IO密集型任务中使⽤多线程可以⼤⼤的加速程序运⾏,及时在单核CPU上,这种加速主要就是利⽤了被浪费掉的阻塞时间。
IO密集型时,⼤部分线程都阻塞,故需要多配置线程数:
**参考公式:**CPU核数 / 1 - 阻塞系数 阻塞系数在 0.8~0.9 之间
⽐如 8 核 CPU:8/1 - 0.9 = 80个线程数