1 线程池基础
1.1 线程池是什么?
线程池,就是一个线程的池子,里面有若干线程,它们的目的就是执行提交给线程池的任务,执行完一个任务后不会退出,而是继续等待或执行新任务。案例如下图所示:首先创建了一个线程池,线程池中有 5 个线程,然后线程池将 10000 个任务分配给这 5 个线程,这 5 个线程反复领取任务并执行,直到所有任务执行完毕,这就是线程池的思想。
1.2 线程池内部结构?
线程池的内部结构主要由四部分组成,如图所示:
(1)第一部分是线程池管理器,它主要负责管理线程池的创建、销毁、添加任务等管理操作,它是整个线程池的管家;
(2)第二部分是工作线程,也就是图中的线程 t0~t9,这些线程勤勤恳恳地从任务队列中获取任务并执行;
(3)第三部分是任务队列,作为一种缓冲机制,线程池会把当下没有处理的任务放入任务队列中,由于多线程同时从任务队列中获取任务是并发场景,此时就需要任务队列满足线程安全的要求,所以线程池中任务队列采用 BlockingQueue 来保障线程安全;
(4)第四部分是任务,任务要求实现统一的接口,以便工作线程可以处理和执行;
1.3 使用线程池比手动创建线程好在哪里?
1.3.1 如果每个任务都创建一个线程会带来哪些问题?
(1)反复创建线程系统开销比较大,每个线程创建和销毁都需要时间,如果任务比较简单,那么就有可能导致创建和销毁线程消耗的资源比线程执行任务本身消耗的资源还要大;
(2)过多的线程会占用过多的内存等资源,还会带来过多的上下文切换,同时还会导致系统不稳定。
1.3.2 线程池解决问题思路?
(1)针对反复创建线程开销大的问题,线程池用一些固定的线程一直保持工作状态并反复执行任务;
(2)针对过多线程占用太多内存资源的问题,解决思路更直接,线程池会根据需要创建线程,控制线程的总数量,避免占用过多内存资源。
1.3.3 使用线程池比手动创建线程主要有三点好处?
(1)线程池可以解决线程生命周期的系统开销问题,同时还可以加快响应速度。因为线程池中的线程是可以复用的,我们只用少量的线程去执行大量的任务,这就大大减小了线程生命周期的开销。而且线程通常不是等接到任务后再临时创建,而是已经创建好时刻准备执行任务,这样就消除了线程创建所带来的延迟,提升了响应速度,增强了用户体验。
(2)线程池可以统筹内存和CPU的使用,避免资源使用不当。线程池会根据配置和任务数量灵活地控制线程数量,不够的时候就创建,太多的时候就回收,避免线程过多导致内存溢出,或线程太少导致CPU资源浪费,达到了一个完美的平衡。
(3)线程池可以统一管理资源。比如线程池可以统一管理任务队列和线程,可以统一开始或结束任务,比单个线程逐一处理任务要更方便、更易于管理,同时也有利于数据统计,比如我们可以很方便地统计出已经执行过的任务的数量。
1.4 线程池的构造方法
ThreadPoolExecutor有多个构造方法,都需要一些参数,主要构造方法有:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
}
1.5 如何向线程池提交任务?
(1)方式一:execute()方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功。
threadsPool.execute(new Runnable() {
@Override
public void run() {
// TODO Auto-generated method stub
}
});
(2)方式二:submit()方法用于提交需要返回值的任务。一个future类型的对象,通过这个future对象可以判断任务是否执行成功,并且可以通过future的get()方法来获取返回值,get()方法会阻塞当前线程直到任务完成。
2 线程池实现“线程复用”的原理?
2.1 线程复用原理
线程池可以把线程和任务进行解耦,线程归线程,任务归任务,摆脱了之前通过Thread创建线程时的一个线程必须对应一个任务的限制。在线程池中,同一个线程可以从BlockingQueue中不断提取新任务来执行,其核心原理在于线程池对Thread进行了封装,并不是每次执行任务都会调用Thread.start()来创建新线程,而是让每个线程去执行一个“循环任务”。在这个“循环任务”中,不停地检查是否还有任务等待被执行,如果有则直接去执行这个任务,也就是调用任务的run方法,把run方法当作和普通方法一样的地位去调用,这样相当于把每个任务的run()方法串联了起来,所以线程数量并不增加。
2.2 线程池创建新线程的时机和规则
(1)当提交任务后,线程池首先会检查当前线程数,如果此时线程数小于核心线程数,比如最开始线程数量为0,则新建线程并执行任务;
(2)随着任务的不断增加,线程数会逐渐增加,并达到核心线程数,此时如果仍有任务被不断提交,就将任务加入BlockingQueue任务队列中,等待核心线程执行完当前任务后重新从BlockingQueue中提取正在等待被执行的任务;
(3)如果任务特别的多,达到了BlockingQueue的容量上限,此时线程池就会启动后备力量,线程池会在核心线程数corePoolSize的基础上继续创建线程来执行任务;
(4)假设任务被不断提交,线程池会持续创建线程直到线程数达到最大线程数maximumPoolSize,如果依然有任务被提交,这就超过了线程池的最大处理能力,线程池就会拒绝这些任务;
2.3 线程复用源码解析
2.3.1 从execute方法开始分析,源码如下所示
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// (1)
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// (2)
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// (3)
else if (!addWorker(command, false))
reject(command);
}
2.3.2 线程复用源码解析
(1)判断当前线程数是否小于核心线程数,如果小于核心线程数就调用addWorker()方法增加一个Worker,这里的Worker就可以理解为一个线程:
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
那addWorker方法又是做什么用的呢?addWorker方法的主要作用是在线程池中创建一个线程,并执行第一个参数传入的任务,如果第二个参数传入true代表增加线程时判断当前线程是否少于corePoolSize,小于则增加新线程,大于等于则不增加;同理,如果传入false代表增加线程时判断当前线程是否少于maxPoolSize,小于则增加新线程,大于等于则不增加。所以第二个参数的含义是以核心线程数,还是以最大线程数为界限,进行是否新增线程的判断。addWorker()方法如果返回true代表添加成功,如果返回false代表添加失败。
(2)如果当前线程数大于或等于核心线程数,或者addWorker失败了
if (isRunning(c) && workQueue.offer(command)) {
int recheck = ctl.get();
if (! isRunning(recheck) && remove(command))
reject(command);
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
通过if (isRunning© && workQueue.offer(command)) 检查线程池状态是否为Running,如果线程池状态是Running就把任务放入任务队列中,也就是workQueue.offer(command)。
如果线程池已经不处于Running状态,说明线程池被关闭,那么就移除刚刚添加到任务队列中的任务,并执行拒绝策略,如下:
if (! isRunning(recheck) && remove(command))
reject(command);
如果前面判断到线程池状态为Running,那么当任务被添加进来之后就需要防止没有可执行线程的情况发生(比如之前的线程被回收了或意外终止了)。所以此时如果检查当前线程数为0,也就是workerCountOf(recheck) == 0,那就执行addWorker()方法新建线程。
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
(3)如果线程池不是Running状态,或线程数大于或等于核心线程数,并且任务队列已经满了
else if (!addWorker(command, false))
reject(command);
此时需要添加新线程,直到线程数达到“最大线程数”,所以此时就会调用addWorker方法并将第二个参数传入false,传入 false代表判断当前线程数是否少于maxPoolSize,小于则增加新线程,大于等于则不增加,也就是以maxPoolSize为上限创建新的线程。addWorker方法如果返回true代表添加成功,如果返回false代表任务添加失败,说明当前线程数已经达到maxPoolSize,然后执行拒绝策略reject方法。
(4)额外分析Worker
在execute方法中,多次调用addWorker方法把任务传入,addWorker方法会添加并启动一个Worker,这里的Worker可以理解为是对Thread的包装,Worker内部有一个Thread对象,它正是最终真正执行任务的线程,所以一个Worker就对应线程池中的一个线程,addWorker就代表增加线程。线程复用的逻辑实现主要在Worker类中的run方法里执行的runWorker方法中,它会自动循环获取工作队列的任务来执行,简化后的runWorker方法代码如下所示:
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
final Thread thread;
Worker(Runnable firstTask) {
this.thread = getThreadFactory().newThread(this);
}
public void run() {
runWorker(this);
}
// 执行后,自动循环获取工作队列的任务来执行
final void runWorker(Worker w) {
Runnable task = w.firstTask;
while (task != null || (task = getTask()) != null) {
try {
task.run();
} finally {
task = null;
}
}
}
}
3 线程池的各个参数的含义?
(1)corePoolSize:(核心线程数):并不是一开始就创建这么多线程,刚创建一个线程池后,不会预先创建核心线程,只有当有任务时才会创建;而且核心线程不会因为空闲而被终止,keepAliveTime参数不适用于它。如果调用了线程池的prestartAllCoreThreads()方法,线程池会提前创建并启动所有基本线程。
(2)maximumPoolSize:(最大线程数):使用了无界队列,这个参数就没有效果。
(3)keepAliveTime(空闲线程存活时间):线程池的非核心线程中的空闲线程的存活时间。一个非核心线程在空闲等待新任务时,会有一个最长等待时间keepAliveTime,如果到了时间还是没有新任务,就会被销毁。如果该值为0,表示所有线程都不会超时终止。
(4)TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS)、小时(HOURS)、分钟(MINUTES)、毫秒(MILLISECONDS)、微秒(MICROSECONDS,千分之一毫秒)和纳秒(NANOSECONDS,千分之一微秒)
(5)BlockingQueue<> workQueue(用于存放任务的队列):用于保存等待执行的任务的阻塞队列。
(6)ThreadFactory:线程工厂,用来创建新线程,可以通过线程工厂给每个创建出来的线程设置更有意义的名字;
(7)RejectedExecutionHandler(任务拒绝策略):详细看第4部分 ;
(8)补充:用一个古代故事理解
把corePoolSize与maximumPoolSize比喻成长工与临时工,通常古代一个大户人家会有几个固定的长工,负责日常的工作,而大户人家起初肯定也是从零开始雇佣长工的。假如长工数量被老爷设定为5人,也就对应了corePoolSize,不管这5个长工是忙碌还是空闲,都会一直在大户人家待着。可到了农忙或春节,长工的人手显然就不够用了,这时就需要雇佣更多的临时工,这些临时工就相当于在corePoolSize的基础上继续创建新线程,但临时工也是有上限的,也就对应了maximumPoolSize,随着农忙或春节结束,老爷考虑到人工成本便会解约掉这些临时工,家里工人数量便会从maximumPoolSize降到corePoolSize,所以老爷家的工人数量会一致保持在corePoolSize和maximumPoolSize的区间。
Gif来源于:拉钩教育 Java并发编程核心第10讲:线程池的各个参数的含义?
4 线程池有哪4种任务拒绝策略?
4.1 拒绝的时机
(1)调用shutdown等方法关闭线程池后,即使线程池内部还有没执行完的任务,由于线程池已经关闭,此时再向线程池提交任务,就会被拒绝;
(2)当线程池没有能力继续处理新提交的任务了,也就是线程池处于饱和状态时。案例说明:新建一个线程池,使用容量上限为10的ArrayBlockingQueue作为任务队列,并且指定线程池的核心线程数为5,最大线程数为10,假设此时有20个耗时任务被提交,在这种情况下,线程池会首先创建核心数量的线程,也就是5个线程来执行任务,然后任队列里去放任务,队列的10个容量被放满了之后,会继续创建新线程,直到达到最大线程数10。此时线程池中一共有20个任务,其中10个任务正在被10个线程执行,还有10个任务在任务队列中等待,而且由于线程池的最大线程数量就是10,所以已经不能再增加更多的线程来帮忙处理任务了,这就意味着此时线程池工作饱和,这个时候再提交新任务时就会被拒绝。
4.2 拒绝策略
ThreadPoolExecutor 类中为我们提供了 4 种默认的拒绝策略来应对不同的场景,都实现了 RejectedExecutionHandler 接口,如图所示:
(1)AbortPolicy,默认的方式。这种拒绝策略在拒绝任务时,会直接抛出一个类型为RejectedExecutionException的RuntimeException,让你感知到任务被拒绝了,于是你便可以根据业务逻辑选择重试或者放弃提交等策略;
Caused by: java.util.concurrent.RejectedExecutionException: Task com.seniorlibs.thread.threadpool.ThreadPoolManagerTest$MyTask@d34fd7b rejected from java.util.concurrent.ThreadPoolExecutor@ced9798[Running, pool size = 10, active threads = 10, queued tasks = 10, completed tasks = 0]
at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2014)
at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:794)
at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1340)
at com.seniorlibs.thread.threadpool.ThreadPoolManagerTest.textRejectedExecution(ThreadPoolManagerTest.java:57)
at com.seniorlibs.thread.MainActivity.textRejectedExecution(MainActivity.java:70)
public static class AbortPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
throw new RejectedExecutionException("Task " + r.toString() + " rejected from " + e.toString());
}
}
(2)DiscardPolicy,这种拒绝策略正如它的名字所描述的一样,当新任务被提交后直接被丢弃掉,也不会给你任何的通知,相对而言存在一定的风险,因为我们提交的时候根本不知道这个任务会被丢弃,可能造成数据丢失;
public static class DiscardPolicy implements RejectedExecutionHandler {
/**
* Does nothing, which has the effect of discarding task r.
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
}
(3)DiscardOldestPolicy,如果线程池没被关闭且没有能力执行,则会丢弃任务队列中的头结点,通常是存活时间最长的任务,这种策略与第二种不同之处在于它丢弃的不是最新提交的,而是队列中存活时间最长的,这样就可以腾出空间给新提交的任务,但同理它也存在一定的数据丢失风险。
public static class DiscardOldestPolicy implements RejectedExecutionHandler {
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
e.getQueue().poll();
e.execute(r);
}
}
}
(4)CallerRunsPolicy,相对而言它就比较完善了,当有新任务提交后,如果线程池没被关闭且没有能力执行,则把这个任务交于提交任务的线程执行,也就是谁提交任务,谁就负责执行任务。
这样做主要有两点好处:
第一点:新提交的任务不会被丢弃,这样也就不会造成业务损失;
第二点:由于谁提交任务谁就要负责执行任务,这样提交任务的线程就得负责执行任务,而执行任务又是比较耗时的,在这段期间,提交任务的线程被占用,也就不会再提交新的任务,减缓了任务提交的速度,相当于是一个负反馈。在此期间,线程池中的其他线程也可以充分利用这段时间来执行掉一部分任务,腾出一定的空间,相当于是给了线程池一定的缓冲期。
public static class CallerRunsPolicy implements RejectedExecutionHandler {
/**
* 直接在提交任务的线程执行(通常会直接将任务交给主线程执行,因为是在主线程提交的任务)
*/
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
if (!e.isShutdown()) {
r.run();
}
}
}
// 线程池只有1个线程(第1个任务耗时10秒),而且在主线程提交的任务(每个任务耗时1秒)
线程序号:11 Thread.currentThread().getName():main
线程序号:0 Thread.currentThread().getName():Thread #1
线程序号:12 Thread.currentThread().getName():main
线程序号:13 Thread.currentThread().getName():main
线程序号:14 Thread.currentThread().getName():main
线程序号:15 Thread.currentThread().getName():main
线程序号:16 Thread.currentThread().getName():main
线程序号:17 Thread.currentThread().getName():main
线程序号:18 Thread.currentThread().getName():main
线程序号:19 Thread.currentThread().getName():main
线程序号:20 Thread.currentThread().getName():main
线程序号:1 Thread.currentThread().getName():Thread #1
线程序号:2 Thread.currentThread().getName():Thread #1
线程序号:4 Thread.currentThread().getName():Thread #1
线程序号:5 Thread.currentThread().getName():Thread #1
.................
(5)通过实现 RejectedExecutionHandler接口来实现自己的拒绝策略,在接口中我们需要实现rejectedExecution方法,在 rejectedExecution方法中,执行例如打印日志、暂存任务、重新执行等自定义的拒绝策略,以便满足业务需求。
private static class CustomRejectionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 打印日志、暂存任务、重新执行等拒绝策略
}
}
4 线程池常用的阻塞队列有哪些?
4.1 LinkedBlockingQueue
(1)特点:一个基于链表结构的无界队列,默认无限大(可以预定义容量);创建的线程就不会超过corePoolSize(因此,maximumPoolSize的值也就无效了)
(2)缺点:无界队列将导致当所有corePoolSize线程都工作时,新任务在队列中等待,可能会造成占用大量内存,并发生OOM;
4.2 ArrayBlockingQueue
(1)特点:一个基于数组结构的有界队列,按FIFO先进先出对元素排序;有助于防止占用大量内存;
(2)缺点:
4.3 SynchronousQueue:
(1)特点:一个不存储元素的队列,直接提交;
(2)缺点:
4.4 DelayedWorkQueue
(1)特点:延迟执行任务;内部元素并不是按照放入的时间排序,而是会按照延迟的时间长短对任务进行排序,内部采用的是“堆”的数据结构;
(2)缺点:
4.5 PriorityBlockingQueue
(1)特点:一个具有优先级的无界阻塞优先级队列;
(2)缺点:
4 Executors提供了哪5种常见的线程池?
4.1 newSingleThreadExecutor
(1)特点:使用唯一的线程去执行任务;使用无界队列LinkedBlockingQueue;空闲线程存活时间keepAliveTime为0,所以此1个线程创建后不会超时终止。因此该线程池会保证所有任务按照指定顺序执行,适合用于所有任务都需要按被提交的顺序依次执行的场景。如果线程在执行任务的过程中发生异常,线程池也会重新创建一个线程来执行后续的任务。
(2)过程:
(3)缺点:如使用的队列是容量没有上限的LinkedBlockingQueue,如果我们对任务的处理速度比较慢,那么随着请求的增多,队列中堆积的任务也会越来越多,最终大量堆积的任务会占用大量内存,并发生OOM。
(4)源码:
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
4.2 newFixedThreadPool
(1)特点:核心线程数和最大线程数是一样的,也就是固定线程数的线程池;使用无界队列LinkedBlockingQueue,任务数超过线程数,线程池也不会再创建更多的线程来处理任务,而是会把超出固定线程数的任务放到任务队列中进行等待;空闲线程存活时间keepAliveTime为0,所以此n个线程创建后不会超时终止。
(2)过程:
(3)缺点:同4.1
(4)源码:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory);
}
4.3 newCachedThreadPool
(1)特点:核心线程为0;最大线程数是几乎可以无限增加的,所以即是可缓存线程池;任务队列是容量为0的SynchronousQueue,实际不存储任何任务,它只负责对任务进行中转和传递,所以任何任务都会被立即执行;空闲线程存活时间keepAliveTime为60s,所以当线程闲置时会被回收。所以效率比较高,比较适合在系统负载不太高下,执行大量的执行时间比较短的任务。
(2)过程:当我们提交一个任务后,线程池会判断已创建的线程中是否有空闲线程,如果有空闲线程则将任务直接指派给空闲线程,如果没有空闲线程,则新建线程去执行任务,这样就做到了动态地新增线程。注意:由于corePool为0,无法添加线程执行任务,所以先把任务添加到队列中。
(3)缺点:由于CachedThreadPool并不限制线程的数量,当任务数量特别多的时候,就可能会导致创建非常多的线程,最终超过了操作系统的上限而无法创建新线程,或者导致内存不足。
(4)源码:
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
4.4 newScheduledThreadPool
(1)特点:核心线程数固定;最大线程数是几乎可以无限增加的;任务队列是容量为DelayedWorkQueue,首先把任务添加到队列中,再定时及周期性的取出任务—>执行—>修改时间—>添加回队列中;空闲线程存活时间keepAliveTime为10ms,所以当线程闲置时会被回收。适合执行定时任务或者具有周期性的重复任务。
(2)过程:注意1:和前3种线程池不同的地方在于,这是首先把任务添加到队列中,再定时及周期性的取出任务 。
注意2:如果在任务的执行中遇到异常,后续执行被取消;最优实践是:将所有执行代码用try-catch包裹。
(3)缺点:任务队列是DelayedWorkQueue,这是一个延迟队列,同时也是一个无界队列,所以和LinkedBlockingQueue一样,如果队列中存放过的任务,就可能导致OOM。
(4)源码:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE,
10L, MILLISECONDS,
new DelayedWorkQueue());
}
private void delayedExecute(RunnableScheduledFuture<?> task) {
if (isShutdown())
reject(task);
else {
super.getQueue().add(task); // 首先把任务添加到队列中
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
else
ensurePrestart();
}
}
(5)使用
public void newScheduledThreadPool(View view) {
ScheduledExecutorService service = Executors.newScheduledThreadPool(10);
// 2秒后执行一次任务后就结束
// service.schedule(new MyTask(), 2, TimeUnit.SECONDS);
/**
* 以固定的频率执行任务: 第一次延时2秒后,每次延时3秒执行一次任务(以任务开始的时间为时间起点开始计时,时间到就开始执行第二次任务,而不管任务需要花多久执行)
*
* 06-27 15:52:02.386 new Random().nextInt():-1912208461 Thread.currentThread().getName():pool-1-thread-1
* 06-27 15:52:05.386 new Random().nextInt():-1378944765 Thread.currentThread().getName():pool-1-thread-1
* 06-27 15:52:08.385 new Random().nextInt():-97809203 Thread.currentThread().getName():pool-1-thread-2
*/
// service.scheduleAtFixedRate(new MyTask(), 2, 3, TimeUnit.SECONDS);
/**
* 任务结束的时间为下一次循环的时间起点开始计时: 第一次延时2秒后,每次延时3+1秒执行一次任务(以任务结束的时间为下一次循环的时间起点开始计时)
*
* 06-27 15:58:24.535 new Random().nextInt():779350097 Thread.currentThread().getName():pool-1-thread-1
* 06-27 15:58:28.536 new Random().nextInt():277033286 Thread.currentThread().getName():pool-1-thread-1
* 06-27 15:58:32.538 new Random().nextInt():-1066926209 Thread.currentThread().getName():pool-1-thread-2
*/
service.scheduleWithFixedDelay(new MyTask(), 2, 3, TimeUnit.SECONDS);
}
4.5 ForkJoinPool
(1)特点:适合执行可以产生子任务的任务。
(2)过程:
(3)缺点:
(4)源码:
7 如何合理配置线程池?合适的线程数量是多少?CPU核心数和线程数的关系?
7.1 为什么要合理配置线程池?
(1)目的是为了充分并合理地使用CPU和内存等资源,从而最大限度地提高程序的性能。在实际工作中,我们需要根据任务类型的不同选择对应的策略。
(2)准则:线程的平均工作时间所占比例越高,就需要越少的线程;线程的平均等待时间所占比例越高,就需要越多的线程;
7.2 CPU密集型任务
(1)特点:常见的CPU密集型任务,比如加密、解密、压缩、计算等需要使用大量的CPU计算操作。
(2)建议:最佳的核心线程数为CPU核心数;最大线程数为CPU核心数的1~2倍;使用容量更大的任务队列;可用暂存任务、重新执行等拒绝策略;
(3)原因:如果设置过多的线程数,实际上并不会起到很好的效果。假设我们设置的线程数量是CPU核心数的2倍以上,因为计算任务非常重,会占用大量的CPU资源,所以这时CPU的每个核心工作基本都是满负荷的;而我们又设置了过多的线程,每个线程都想去利用CPU资源来执行自己的任务,这就会造成不必要的上下文切换带来的开销,此时线程数的增多并没有让性能提升,反而由于线程数量过多会导致性能下降。
针对这种情况,我们最好还要同时考虑在同一台机器上还有哪些其他会占用过多CPU资源的程序在运行,然后对资源使用做整体的平衡。
(4)案例:
static {
// 返回可用处理器的Java虚拟机的数量
int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// 线程池核心容量
final int CORE_POOL_SIZE = CPU_COUNT;
// 线程池最大容量
final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2;
// 过剩的空闲线程的存活时间
final int KEEP_ALIVE_TIME = 1;
// 使用有界队列,可以配置大一些,例如几千
BlockingQueue<Runnable> blockingQueue = new LinkedBlockingQueue<>(3000);
// ThreadFactory 线程工厂,通过工厂方法newThread来获取新线程
final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "ThreadPoolManager CPU #" + mCount.getAndIncrement());
}
};
// 线程池执行耗时任务时发生异常所需要做的拒绝执行处理。注意:一般不会执行到这里
final RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 打印日志、暂存任务、重新执行等拒绝策略
Executors.newCachedThreadPool().execute(r);
}
};
sCpuThreadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
KEEP_ALIVE_TIME, TimeUnit.SECONDS,
blockingQueue, sThreadFactory, rejectedExecutionHandler);
}
7.3 耗时IO密集型任务
(1)特点:常见的耗时IO型任务,比如数据库、文件读取、写入,网络请求等任务,这种任务的特点是并不会特别消耗CPU资源,但是IO密集型任务并不是一直在执行任务,总体会占用比较多的时间。
(2)建议:最佳的核心线程数为CPU核心数的很多倍;更大的最大线程数;稍小容量的任务队列;可用暂存任务、重新执行等拒绝策略;
(3)原因:而如果我们设置更多的线程数,那么当一部分线程正在等待IO的时候,它们此时并不需要CPU来计算,那么另外的线程便可以利用CPU去执行其他的任务,互不影响,这样的话在任务队列中等待的任务就会减少,可以更好地利用资源。如果我们设置过少的线程数,就可能导致CPU资源的浪费。
(4)案例:
static {
// 线程池核心容量
final int CORE_POOL_SIZE = 10;
// 线程池最大容量
final int MAXIMUM_POOL_SIZE = 128;
// 过剩的空闲线程的存活时间
final int KEEP_ALIVE_TIME = 1;
// 使用有界队列,可以配置稍小容量的任务队列,例如几百
BlockingQueue<Runnable> blockingQueue = new LinkedBlockingQueue<>(512);
// ThreadFactory 线程工厂,通过工厂方法newThread来获取新线程
ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
@Override
public Thread newThread(@NonNull Runnable r) {
return new Thread(r, "ThreadPoolManager IO #" + mCount.getAndIncrement());
}
};
// 线程池执行耗时任务时发生异常所需要做的拒绝执行处理。注意:一般不会执行到这里
final RejectedExecutionHandler rejectedExecutionHandler = new RejectedExecutionHandler() {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 打印日志、暂存任务、重新执行等拒绝策略
Executors.newCachedThreadPool().execute(r);
}
};
sIoThreadPoolExecutor = new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE,
KEEP_ALIVE_TIME, TimeUnit.SECONDS,
blockingQueue, sThreadFactory, rejectedExecutionHandler);
}
7.5 AsnyncTask的线程池申请
// 获取当前的cpu核心数
private static final int CPU_COUNT = Runtime.getRuntime().availableProcessors();
// 线程池核心容量
private static final int CORE_POOL_SIZE = CPU_COUNT + 1;
// 线程池最大容量
private static final int MAXIMUM_POOL_SIZE = CPU_COUNT * 2 + 1;
// 过剩的空闲线程的存活时间
private static final int KEEP_ALIVE = 1;
// ThreadFactory 线程工厂,通过工厂方法newThread来获取新线程
private static final ThreadFactory sThreadFactory = new ThreadFactory() {
private final AtomicInteger mCount = new AtomicInteger(1);
public Thread newThread(Runnable r) {
return new Thread(r, "AsyncTask #" + mCount.getAndIncrement());
}
};
// 静态阻塞式队列,用来存放待执行的任务,初始容量:128个
private static final BlockingQueue<Runnable> sPoolWorkQueue = new LinkedBlockingQueue<Runnable>(128);
public static final Executor THREAD_POOL_EXECUTOR
= new ThreadPoolExecutor(CORE_POOL_SIZE, MAXIMUM_POOL_SIZE, KEEP_ALIVE,
TimeUnit.SECONDS, sPoolWorkQueue, sThreadFactory,
new ThreadPoolExecutor.DiscardOldestPolicy());
8 如何正确关闭线程池?shutdown()和shutdownNow()的区别?
8.1 shutdown()
(1)特点:调用shutdown()后线程池会在执行完正在执行的任务和队列中等待的任务后,才彻底关闭。
(2)原理:遍历线程池中的所有空闲线程,然后逐个调用线程的interrupt()方法来中断线程,所以无法响应中断的线程可能永远无法终止。
public void shutdown() {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(SHUTDOWN);
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
(3)区别:shutdown()只是将线程池的状态设置成SHUTDOWN状态,然后逐个中断所有执行完任务的线程。
8.2 shutdownNow()
(1)特点:调用shutdownNow()后线程池会在关闭所有线程;如果无法响应中断的线程可能永远无法终止。
(2)原理:遍历线程池中的所有线程,然后逐个调用线程的interrupt()方法来中断线程,所以无法响应中断的线程可能永远无法终止。
public List<Runnable> shutdownNow() {
List<Runnable> tasks;
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
checkShutdownAccess();
advanceRunState(STOP);
interruptWorkers();
tasks = drainQueue();
} finally {
mainLock.unlock();
}
tryTerminate();
return tasks;
}
(3)区别:shutdownNow()首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。
8.3 isShutdown()
(1)特点:返回true仅仅代表线程池开始了关闭的流程;可能线程池中依然有线程在执行任务,队列里也可能有等待被执行的任务。
(2)原理
8.4 isTerminated()
(1)特点:检测线程池是否真正“终结”了,这不仅代表线程池已关闭,同时代表线程池中的所有任务都已经都执行完毕了。
(2)原理
8.5 awaitTermination()
(1)特点:awaitTermination(5, TimeUnit.SECONDS)表示当前线程会尝试等待一段指定的时间,如果在等待时间内,线程池已关闭并且内部的任务都执行完毕了,也就是说线程池真正“终结”了,那么方法就返回true,否则超时返回false。
(2)原理
9 参考链接
Java并发编程的艺术:P200-227