说到多线程,就肯定短不了线程池,此次对线程池简单做了下总结
1、线程池的几个参数含义
-
corePoolSize与maxPoolSize
-
它俩的关系就类似与地主家的长工与短工,长工就是一直存在的,长工不够用的时候,就会临时雇佣一些短工,等忙碌期过后,就会辞退这些短工;把corePoolSize与maxPoolSize代入到这个例子里,就可以发现corePoolSize就是常驻的线程,不会被回收,maxPoolSize是在corePoolSize无法满足需求的时候才会创建,等任务执行完毕之后进入空闲状态,到了指定的存活时间之后还处于空闲状态,就会被回收掉
-
keepAliveTime
-
当线程数量大于核心线程数时,而此时又没有可执行的任务,就会检查非核心线程数的keepAliveTime,如果超过该时间,就会进行回收,减少资源的消耗
-
ThreadFactory
-
ThreadFactory就是一个线程工厂,用于生产线程
-
workQueue
-
阻塞队列
-
Handler
-
用于处理被拒绝的任务,以下为拒绝的俩种时机:
-
第一种是调用了shutdown等方法关闭了线程池后,如果再向线程池提交任务,就会执行拒绝策略
-
第二种是当前队列已满并且工作线程数已达到最大线程数,也会执行拒绝策略,如图所示
-
拒绝的几种策略
-
AbortPolicy:会抛出一个RejectedExecutionException,可以通过捕获异常做业务补偿处理
-
DiscardPolicy:默认的拒绝策略,会直接丢弃任务,可能会造成逻辑丢失
-
DiscardOldestPolicy:丢弃队列中的头节点,也就是存活最长还未被执行的任务,类似于做了一个替换,丢弃老任务,保存新任务,也有可能会造成逻辑丢失
-
CallerRunsPolicy:把任务交给当前提交任务的线程执行,也就是说谁提交谁执行,这个策略相对来说比较完善,有俩点好处
-
第一点:任务不会丢失
-
第二点:可以做出一个负面反馈,由当前提交任务的线程执行该任务,那这段时间内就不会再提交新任务,减少了任务提交的速度,在此期间,线程池中一部分线程可能已经执行完各自的任务,相当于给了线程池一定的缓冲期
-
自定义拒绝策略:自定义一个类,实现RejectedExecutionHandler接口,重写rejectedExecution方法,在创建线程池时指定为自定义类
2、线程创建的时机
线程初始化时,线程数为0,在提交任务后,会先检查核心线程数,如果当前线程数小于核心线程数,则创建核心线程执行任务,如果当前线程数大于核心线程数,不会直接创建线程,而是会先提交至队列中,如果队列已满,再校验线程池中的线程数量是否已经达到指定的最大线程数,如果没有达到,则创建非核心线程执行任务,如果已经达到,则执行拒绝策略
3、常见的几种线程池
-
FixedThreadPool
-
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue()); } -
它的核心线程数和最大线程数是一样的,之后的线程数都是固定的,就算任务超过线程数,也不会新建线程;由于线程数是固定的,所以为了防止拒绝任务,使用的队列为无界队列LinkedBlockingQueue
-
适用于长期执行的任务
-
CachedThreadPool
-
public static ExecutorService newCachedThreadPool() { return new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS, new SynchronousQueue()); }
-
它的核心线程数为0,最大线程数为Integer.MAX_VALUE,由于它的线程数量设置的很大,相当于没有边界,可以不断的新建,所以它使用的队列为SynchronousQueue,这个队列不存储任务,只转交任务给线程
-
适用于并发量大,执行时间短的任务
-
ScheduledThreadPool
-
public ScheduledThreadPoolExecutor(int corePoolSize) { super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS, new DelayedWorkQueue());}
-
它的最大线程数为Integer.MAX_VALUE,阻塞队列DelayedWorkQueue,该队列可以按照延迟的时间长短对任务进行排序
-
支持定时或周期性执行任务,该线程池有3个方法,分别代表三种不同的周期形式
-
service.schedule(new Task(), 10, TimeUnit.SECONDS)
-
延迟指定时间后执行任务,执行完毕后结束
-
service.scheduleAtFixedRate(new Task(), 10, 15, TimeUnit.SECONDS)
-
第一次延迟10s后执行任务,之后以每次延迟15s的频率执行任务
-
service.scheduleWithFixedDelay(new Task(), 10, 10, TimeUnit.SECONDS)
-
与scheduleAtFixedRate类似,只不过计算时间的起始值不一样,scheduleAtFixedRate是以任务的开始时间作为延迟时间的计时起点,scheduleWithFixedDelay是以任务执行完毕的时间作为延迟时间的计时起点
-
SingleThreadExecutor
-
public static ExecutorService newSingleThreadExecutor() { return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue())); }
-
核心线程数与最大线程数都为1,可以理解为这是一个只有1个线程的线程池,所以为了防止任务丢失,使用的是无界队列LinkedBlockingQueue
-
适用于串行执行的任务,这也就是为什么只有一个线程,还要用线程池的原因
-
ForkJoinPool
-
可以将主任务fork成多个子任务,并行执行,互不影响,最后将子任务执行的结果进行join
-
它跟普通线程池还有个不同的地方就是队列,普通线程池只有一个队列,而它除了用于保存提交任务的队列之外,每个线程都有自己独立的任务队列,用于存储fork之后的任务
-
-
以下例子,用于实现斐波那契数列0-9的值 (从第3项开始,每一项都等于前两项之和)
-
public class TestForkJoinTask extends RecursiveTask { private int i; public TestForkJoinTask(int i) { this.i = i;
}
@Override
protected Integer compute() { if (i <= 1) { return i; } TestForkJoinTask forkJoinTask1 = new TestForkJoinTask(i - 1); forkJoinTask1.fork(); TestForkJoinTask forkJoinTask2 = new TestForkJoinTask(i - 2); forkJoinTask2.fork(); return forkJoinTask1.join() + forkJoinTask2.join(); } } -
public static void testForkJoinPool() { ForkJoinPool forkJoinPool = new ForkJoinPool(); for (int i = 0; i < 100; i++) { ForkJoinTask forkJoinTask = forkJoinPool.submit(new TestForkJoinTask(i));
System.out.println(forkJoinTask.get()); } } -
CompletableFuture
4、线程池的几种状态
-
running:运行状态
-
showdown:停止状态,不接受新任务,处理已经存在的任务
-
stop:停止状态,不接受新任务,不处理已经存在的任务
-
tidying:所有任务终止,有效线程数为0,会进入该状态,进入该状态后会进入terminated状态
-
terminated:线程池彻底终止
5、合理设置线程数量
-
CPU密集型任务: CPU 核心数的 1~2 倍
-
密集型任务需要耗费CPU资源, 线程数设置过大,会造成不必要的上下文切换
-
耗时 IO 型任务: CPU 核心数 *(1+平均等待时间/平均工作时间)
-
在等待IO的时候并不需要CPU,那么另外的线程可以利用CPU执行任务,所以线程数量可以设置大点
-
平均等待时间越长,线程就随之增加,平均工作时间越长,类似于密集型任务,线程就随之减少
注:线程数量设置没有标准答案,需要视业务而定