文章目录
一.线程的生命周期
- new Thread()的方法新建一个线程,在线程创建完成之后,线程就进入了
就绪(Runnable)状态
- 进入就绪状态的线程开始进入抢占CPU资源的状态,当线程抢到了CPU的执行权之后,线程就进入了
运行状态(Running)
, - 当该线程的任务执行完成之后或调用的
stop()
方法之后,线程就进入了死亡状态
。
线程还具有一个阻塞的过程,当面对以下几种情况的时候,容易造成线程阻塞
- 当线程主动调用了
sleep()
方法时,线程会进入则阻塞状态
- 当线程中
主动调用了阻塞时的IO方法时
,没有获取到IO返回的数据时会一直阻塞 - 当线程进入正在等待某个通知时,会进入阻塞状态。
为什么会有阻塞状态出现呢?
- 我们都知道,CPU的资源是十分宝贵的,所以,当线程正在进行
某种不确定时长的任务时
,Java就会收回CPU的执行权,从而合理应用CPU的资源。我们根据图可以看出,线程在阻塞过程结束之后,会重新进入就绪状态,重新抢夺CPU资源。
这时候,我们可能会产生一个疑问,如何跳出阻塞过程呢?
由以上几种可能造成线程阻塞的情况来看,都是存在一个时间限制的,
- 当sleep()方法的睡眠时长过去后,线程就自动跳出了阻塞状态,
- 第二种则是在返回了一个参数之后,在获取到了等待的通知时,就自动跳出了线程的阻塞过程。
1.线程池概念
有时候,系统需要处理非常多的执行时间很短的请求,如果每一个请求都开启一个新线程的话,系统就要不断的进行线程的创建和销毁,有时花在创建和销毁线程上的时间会比线程真正执行的时间还长。而且当线程数量太多时,系统不一定能受得了。
概念:线程池是一种多线程处理形式,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程。每个线程都使用默认的堆栈大小,以默认的优先级运行,并处于多线程单元中。我们将任务添加到任务队列中,线程池得知有任务到来后,会唤醒线程,如若所有线程都在执行任务,则线程会处理完当前任务后,在处理任务队列中的线程。
解决的问题:
- 重用线程池中的线程,来减少每个线程创建和销毁的性能开销。
- 对线程进行一些维护和管理,比如可以限制线程的个数,动态新增线程等。每个
ThreadPoolExecutor
也保留了一些基本的统计数据,比如当前线程池完成的任务数目等。
2.什么场景下适合使用线程池
在实际开发中,如果需要创建5个以上的线程,那么就可以使用线程池来管理
二.线程池的详解
1.ThreadPoolExecutor参数说明
无论是创建那种类型线程池(FixedThreadPool、CachedThreadPool…
),均会调用 ThreadPoolExecutor构造函数
public ThreadPoolExecutor(
int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler
)
参数作用:
-
corePoolSize: 线程池核心线程数最大值,通俗点来讲就是,
线程池中常驻线程的最大数量
线程池在创建完时,里面并没有线程,只有当任务到来时再去创建线程。
-
maximumPoolSize: 线程池最大线程数大小(
非核心线程数,也叫工作线程数
,包括核心线程和非核心线程) -
keepAliveTime: 线程池中
空闲线程(即非核心线程)
所能存活的最长时间
-
unit: 线程池中
空闲线程(即非核心线程
)存活时间单位
,与keepAliveTime
配合使用 -
workQueue: 存放任务的阻塞队列
-
threadFactory: 用于设置创建线程的工厂,
可以给创建的线程设置有意义的名字
,可方便排查问题。 -
handler: 线程池的饱和策略事件,主要有四种类型。
2.核心线程数(corePoolSize)和最大线程数(maximumPoolSize)
- corePoolSize:
核心线程数
,线程池在初始化后,默认情况下不会创建任何线程
,会等有任务来的时候才去创建线程核心线程。核心线程会一直存活,即使处于空闲状态也不会受存keepAliveTime
限制。除非将allowCoreThreadTimeOut设置为true
。
/**
* 线程池线程池创建时是否会初始化所有核心线程
*/
@Test
public void testThreadPoolSize() throws InterruptedException {
//创建一个固定长度为50的线程池
ExecutorService fixedThreadPool = Executors.newScheduledThreadPool(5);
ThreadPoolExecutor tpe = (ThreadPoolExecutor) fixedThreadPool;
log.info("线程池初始化=>当前队列任务数={},当前活跃任务数={},执行完成任务数={},总任务数={}", tpe.getQueue().size(), tpe.getActiveCount(), tpe.getCompletedTaskCount(), tpe.getTaskCount());
for (int i = 0; i < 2; i++) {
tpe.execute(() -> {
System.out.println(Thread.currentThread().getName() + "=>执行任务");
log.info("任务执行=>当前队列任务数={},当前活跃任务数={},执行完成任务数={},总任务数={}", tpe.getQueue().size(), tpe.getActiveCount(), tpe.getCompletedTaskCount(), tpe.getTaskCount());
});
}
//防止主线程直接结束
Thread.sleep(5000);
//根据结果判定:线程池初始化时是不会默认创建多个线程,而是在提交任务时才创建
log.info("线程池初始化=>当前队列任务数={},当前活跃任务数={},执行完成任务数={},总任务数={}", tpe.getQueue().size(), tpe.getActiveCount(), tpe.getCompletedTaskCount(), tpe.getTaskCount());
}
执行结果
- maximumPoolSize:
最大线程数
。当阻塞队列已满,并且已创建的线程数大于核心线程数且小于最大线程数
,则会创建新的线程(非核心线程)去执行任务。因此这个参数只有在阻塞队列满的情况下才有意义
。对于无界队列
,这个参数将会失去效果。
线程总数 = 核心线程数 + 非核心线程数。
增减线程特点
-
将
corePoolSize
和maximumPoolSize
设置为相同的值,那么就会创建固定大小的线程池。 -
如果将线程池的
maximumPoolSize
参数设置为很大的值,例如Integer.MAX_VALUE
,可以允许线程池容纳任意数量的并发任务。 -
线程池
只有在队列满了的时候才会去创建大于corePoolSize的线程
,如果使用了无界队列
(如:LinkedBlockingQueue)就不会创建到超过corePoolSize的线程数
。
3.keepAliveTime(空闲时间)
- 线程池线程数多于
corePoolSize
后创建的线程叫非核心线程
,非核心线程空闲时间
超过keepAliveTime
,就会被回收 - keepAliveTime取值不能小于0,
设置为0表示非核心线程线程在空闲时立即终止
- 如果设置
allowCoreThreadTimeOut = true
,则keepAliveTime
也会作用于核心线程注意: keepAliveTime是针对
大于核心线程数,小于最大线程数的那部分非核心线程来说的
。如果是任务数量特别多的情况下,可以适当增加keepAliveTime的大小。以保证在下个任务到来之前,此线程不会立即销毁,从而避免线程的重新创建。
.
一个线程执行完了一个任务后,会去阻塞队列里面取新的任务,在取到任务之前它就是一个闲置的线程。
.
tips:
核心线程跟创建的先后没有关系,而是跟工作线程的个数有关
,如果当前工作线程的个数大于核心线程数,那么所有的线程都可能是“非核心线程”,都有被回收的可能。
@Test
public void testThreadPoolKeepAliveTime() {
ThreadPoolExecutor executor = new ThreadPoolExecutor(1, 2, 0, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(1));
for (int i = 0; i < 3; i++) {
int finalI = i;
executor.execute(() -> {
System.out.println("i=" + finalI + " Thread = " + Thread.currentThread().getName());
if (finalI >= 1) {
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i=" + finalI + " sleep 1 s结束");
} else {
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("i=" + finalI + " sleep 3 s结束");
}
});
}
while (true) {
System.out.println("总线程数:" + executor.getPoolSize() + "当前活跃线程数:" + executor.getActiveCount());
try {
TimeUnit.SECONDS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
keepAliveTime=0
时非核心线程马上就被销毁了
keepAliveTime=10
时非核心线程处于空闲时间超过10秒后就被销毁了
非核心线程存活时间
那么什么是“非核心线程”呢?是不是先创建的线程就是核心线程,后创建的就是非核心线程呢?
- 其实
核心线程跟创建的先后没有关系,而是跟工作线程的个数有关
,如果当前工作线程的个数大于核心线程数,那么所有的线程都可能是“非核心线程”,都有被回收的可能。
Worder线程取任务的方法有两种
- 是通过
take()
一直阻塞直到取出任务 - 是通过
poll(keepAliveTime,timeUnit)
在一定时间内取出任务或者超时,如果超时这个线程就会被回收(请注意核心线程一般不会被回收。)
那么怎么保证核心线程不会被回收呢?
- 跟
工作线程的个数
有关,每一个线程在取任务
的时候,线程池会比较当前的工作线程个数与核心线程数:
-
如果
工作线程数小于当前的核心线程数
,则使用take()
取任务,也就是没有超时回收,这时所有的工作线程都是 “核心线程”,他们不会被回收; -
如果
工作线程数大于当前的核心线程数
,则使用poll(keepAliveTime,timeUnit)
取任务,一旦超时就回收,所以并没有绝对的核心线程,只要这个线程没有在存活时间内取到任务去执行就会被回收。
-
所以每个线程想要保住自己“核心线程”的身份,必须充分努力,尽可能快的获取到任务去执行,这样才能逃避被回收的命运。
核心线程一般不会被回收,但是也不是绝对的,如果我们·设置了允许
核心线程超时被回收的话
,那么就没有核心线程这种说法了,所有的线程都会通过poll(keepAliveTime, timeUnit)
来获取任务,一旦超时获取不到任务,就会被回收
,一般很少会这样来使用,除非该线程池需要处理的任务非常少
,并且频率也不高
,不需要将核心线程一直维持着。
4.Unit(空闲时间单位)
描述存活时间的时间单位。可以使用TimeUnit里边的枚举值。
在TimeUnit
类中有7种静态属性:
属性 | 名称 |
---|---|
TimeUnit.DAYS | 天 |
TimeUnit.HOURS | 小时 |
TimeUnit.MINUTES | 分钟 |
TimeUnit.SECONDS | 秒 |
TimeUnit.MILLISECONDS | 毫秒 |
TimeUnit.MICROSECONDS | 微妙 |
TimeUnit.NANOSECONDS | 秒 |
5.threadFactory(线程工厂)
当线程池需要新的线程时,会用threadFactory来生成新的线程。
新的线程
由ThreadFactory创建,默认使用Executors.defaultThreadFactory()
,创建出来的线程都在同一个线程组
,拥有同样的NORM_PRIORITY优先级
并且都不是守护线程
。- 通常情况下直接使用
defaultThreadFactory
就行。 - 如果自己指定ThreadFactory,那么就可以改变
线程名
、线程组
、优先级
、是否是守护线程
等。 - ThreadFactory接口,只有一个方法,可以通过
实现 ThreadFactory自定义生成线程的规则
public interface ThreadFactory { Thread newThread(Runnable r); }
如默认的Executors.defaultThreadFactory()源码
static class DefaultThreadFactory implements ThreadFactory {
private static final AtomicInteger poolNumber = new AtomicInteger(1);
private final ThreadGroup group;
private final AtomicInteger threadNumber = new AtomicInteger(1);
private final String namePrefix;
DefaultThreadFactory() {
SecurityManager var1 = System.getSecurityManager();
this.group = var1 != null?var1.getThreadGroup():Thread.currentThread().getThreadGroup();
this.namePrefix = "pool-" + poolNumber.getAndIncrement() + "-thread-";
}
public Thread newThread(Runnable var1) {
Thread var2 = new Thread(this.group, var1, this.namePrefix + this.threadNumber.getAndIncrement(), 0L);
if(var2.isDaemon()) {
var2.setDaemon(false);
}
if(var2.getPriority() != 5) {
var2.setPriority(5);
}
return var2;
}
}
6.workQueue(工作队列)
表示阻塞队列,存储所有等待执行的任务。当核心线程数满了后
还有任务继续提交
到线程池的话,就先进入workQueue。
-
直接交接(SynchronousQueue):任务不多时,只需要用队列进行简单的任务中转,这种队列无法存储任务,在使用这种队列时,需要将
maximumPoolSize
设置的大一点。 -
无界队列(LinkedBlockingQueue):如果使用无界队列当作workQueue,将
maximumPoolSize
设置的多大都没有用,不会创建新的非核心线程。使用无界队列的优点是可以防止流量突增
,缺点是如果处理任务的速度 < 提交任务的速度
,会导致无界队列中的任务越来越多,从而导致OOM异常。
无界队列只是相对来说没有限制,大小是int的最大值。
- 有界队列(ArrayBlockingQueue):使用有界队列可以
设置队列大小
,让线程池的maximumPoolSize
有意义,在有界工作队列已满情况下,继续提交的任务会创建新的线程来执行,但创建的数量不会超过maximumPoolSize
,如果超过了maximumPoolSize
,就需要采取相应的拒绝策略 RejectedExecutionHandler
来应对队列饱和的情况
7.handler(拒绝策略)
表示拒绝策略。当线程池的有界工作队列排满
,且没有空闲的工作线程
,如果继续提交任务,必须采取一种策略处理该任务。一共有四种策略可供选择,分别对应四个内部类。
虽然我们有了阻塞队列来对任务进行缓存,这从一定程度上为线程池的执行提供了缓冲期,但是如果是有界的阻塞队列,那就存在队列满的情况,也存在工作线程的数据已经达到最大线程数的时候。如果这时候再有新的任务提交时,显然线程池已经心有余而力不足了,因为既没有空余的队列空间来存放该任务,也无法创建新的线程来执行该任务了,所以这时我们就需要有一种拒绝策略,即 handler。
.
拒绝策略是一个 RejectedExecutionHandler 类型的变量,用户可以自行指定拒绝的策略,如果不指定的话,线程池将使用默认的拒绝策略:抛出异常。
拒绝的时机
- 当
Executor关闭
时,新提交的任务
会被拒绝(如:执行shutdown()
还在继续提交任务)。 - 当Executor
使用有界队列
且工作队列已满
时线程池线程数大于最大线程数
拒绝策略分类
策略 | 描述 |
---|---|
AbortPolicy | (中断策略,默认策略 ):直接抛出异常进行拒绝默认采用的是AbortPolicy,遇到上面两种的情况,线程池直接抛出异常:RejectedExecutionException |
DiscardPolicy | (丢弃策略):不会得到通知,直接丢弃任务 |
DiscardOldestPolicy | (丢弃最老的):由于队列中存储了很多任务,这个策略会丢弃在队列中存在时间最久的任务。且将当前这个任务继续提交给线程池 |
CallerRunsPolicy | (交给线程池调用所在的线程进行处理,即主线程中执行任务 ) , 比如主线程给线程池提交任务,但是线程池已经满了,在这种策略下会让提交任务的线程去执行。 |
个人认为这4中策略不友好,最好自己定义拒绝策略,实现
RejectedExecutionHandler
接口
RejectedExecutionHandler
可以根据不同场景实现RejectedExecutionHandler接口
(接口内只有一个方法),自定义拒绝策略,如记录日志或持久化存储不能处理的任务。
//RejectedExecutionHandler接口
//当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用`RejectedExecutionHandler的rejectedExecution`方法。
public interface RejectedExecutionHandler {
void rejectedExecution(Runnable var1, ThreadPoolExecutor var2);
}
8.生成线程规则
当一个任务被添加进线程池时:
- 线程池刚启动的时候
工作线程
为0 - 线程池中
工作线程 < corePoolSize
时,即使工作线程处于空闲状态,也会创建一个新线程来执行新任务 - 线程池中
工作线程 >=corePoolSize
时,新提交任务将被放入workQueue
中,等待线程池中任务调度
执行 。 - 当
workQueue已满
,且工作线程 < maximumPoolSize
时,新提交任务会创建新线程(非核心线程)
执行任务。 - 当
workQueue已满
,且工作线程 > maximumPoolSize
时,新提交任务直接采取拒绝策略
- 当线程池中的
非核心线程
空闲时间达到
keepAliveTime`时,将回收这些线程。 - 当设置
allowCoreThreadTimeOut(true)
时,线程池中核心线程达到keepAliveTime
也将被回收。
线程池里的线程数量设置多少比较合适?
类型 | 描述 |
---|---|
CPU密集型(加密、计算hash等) | 最佳线程数设置为CPU核心数的1—2倍。 |
耗时I/O型(读写数据库、文件、网络读写等) | 最佳线程数一般会大于CPU核心数很多倍 ,以JVM监控显示繁忙情况为依据,保证线程空闲可以衔接上。参考Brain Goezt推荐的计算方法: 线程数=CPU核心数 × (1+平均等待时间/平均工作时间) |
9.停止线程池的正确方法
9.1.shutdown()
shutdown(): 调用了shutdown()
方法不一定会立即停止,会拒绝接受新的任务,同时等现有任务执行完后才退出线程池
,
- 因为线程池中的线程有
可能正在运行
,并且队列中也有待处理的任务
,不可能说停就停。所以每当调用该方法时,线程池会把正在执行的任务
和队列中等待的任务
都执行完毕再关闭
,并且在此期间如果接收到新的任务
会被拒绝
。
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
//使用lambda的方式提交任务
int finalI = i;
executorService.execute( () -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(finalI +"=>"+Thread.currentThread().getName());
});
}
//主线程休眠1.5s后在关闭线程池
Thread.sleep(1500);
//关闭线程池
executorService.shutdown();
//关闭线程池再次提交任务,线程池会把正在执行的任务和队列中等待的任务都执行完毕再关闭,关闭后接收到新的任务会被拒绝,抛出java.util.concurrent.RejectedExecutionException
executorService.execute( () -> {
System.out.println("再次提交任务=>"+Thread.currentThread().getName());
});
}
结论: 关闭线程池后,线程池会把正在执行的任务和队列中等待的任务都执行完毕
再关闭,而关闭后接收到新的任务
会被拒绝
,并抛出java.util.concurrent.RejectedExecutionException
异常
9.2.shutdownNow()
shutdownNow(): 调用了这个方法时,线程池会立即终止
,并返回没有被处理完的任务。如果需要继续执行这里的任务可以再次让线程池执行这些返回的任务。
尝试停止所有正在执行的任务,停止等待任务的处理,并返回等待执行的任务列表。从此方法返回时,将从任务队列中删除这些任务。
执行当前后不管现在线程池的运行状况,直接一刀切全部停掉,这样可能会导致任务丢失
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
//使用lambda的方式提交任务
int finalI = i;
executorService.execute(() -> {
try {
Thread.sleep(500);//当线程在活动之前或活动期间处于正在等待、休眠或占用状态且该线程被中断时,抛出该异常。
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(finalI + "=>" + Thread.currentThread().getName());
});
}
Thread.sleep(1500);
System.out.println("isShutDown1=>" + executorService.isShutdown());
//关闭线程池,并返回没有被处理完的任务
//该方法使得主线程强行打断子线程的sleep状态,因此抛出此异常:java.lang.InterruptedException: sleep interrupted,根据实际情况,shutdownNow()这个不合理的方法,可以解决该异常。
List<Runnable> unfinishedTask = executorService.shutdownNow();
for (Runnable runnable : unfinishedTask) {
new Thread(runnable,"重新执行").start();
}
System.out.println("unfinishedTask=>"+unfinishedTask.size());
System.out.println("isShutDown2=>" + executorService.isShutdown());
}
9.3.isShutdown(), isTerminated(),awaitTermination()
isShutdown():可以用于判断线程池是否被shutdown了,是true,否则false
isTerminated():可以判断线程是否被完全终止了,如果线程池关闭后所有任务都已完成返回true,否则返回false
awaitTermination():传入等待时间,等待时间达到时判断是否停止了,主要用于检测。
public static void main(String[] args) throws InterruptedException {
ExecutorService executorService = Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
//使用lambda的方式提交任务
int finalI = i;
executorService.execute( () -> {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(finalI +"=>"+Thread.currentThread().getName());
});
}
//主线程休眠1.5s后在关闭线程池
Thread.sleep(1500);
System.out.println("isShutDown1=>"+executorService.isShutdown());
//关闭线程池
executorService.shutdown();
System.out.println("isShutDown2=>"+executorService.isShutdown());
System.out.println("isTerminated=>"+executorService.isTerminated());
}
上图关闭线程池后
isTerminated=false
,说明有正在执行的任务,以及队列中正在等待执行的任务, 导致线程池没有立刻关闭.
在代码中调用主线程休眠10s后在关闭线程池,保证线程池所有正在运行的任务,以及队列中的任务执行完毕
10.线程池回调函数
线程池ThreadPoolExecutor为了提供扩展,提供了protected的两个方法beforeExecute和 afterExecute
,每个任务执行前后都会调用这两个方法
,相当于对线程任务的执行做了一个切面
{
static class Customer extends Thread{
private String name;
public Customer(String name){
this.name = name;
}
@Override
public void run() {
System.out.println(this.name+":加入线程池");
try {
Thread.sleep(ThreadLocalRandom.current().nextInt(10000)*6);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public static void main(String[] args) {
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(2,4,5,
TimeUnit.SECONDS,new ArrayBlockingQueue<>(9),new ThreadPoolExecutor.DiscardOldestPolicy()){
/**
*
* @param t 执行任务的线程
* @param r 将要被执行的任务
*/
@Override
protected void beforeExecute(Thread t, Runnable r) {
System.out.println(((Customer)r).name + ":beforeExecute将要被执行");
}
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println(((Customer)r).name + ":afterExecute已经执行完毕");
}
};
for (int i = 0; i < 10; i++) {
poolExecutor.execute(new Customer("customer-"+i));
}
poolExecutor.shutdown();
}
}
/**
* 演示每个任务执行的前后放钩子函数
*/
public class PauseableThreadPool extends ThreadPoolExecutor {
private final ReentrantLock lock = new ReentrantLock();
private boolean isPaused;
private Condition unPaused = lock.newCondition();
public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory);
}
public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, handler);
}
public PauseableThreadPool(int corePoolSize, int maximumPoolSize, long keepAliveTime,
TimeUnit unit, BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory, RejectedExecutionHandler handler) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, handler);
}
public static void main(String[] args) throws InterruptedException {
PauseableThreadPool pauseableThreadPool = new PauseableThreadPool(10, 20, 10L,
TimeUnit.SECONDS, new LinkedBlockingQueue<>());
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
for (int i = 0; i < 100; i++) {
pauseableThreadPool.execute(runnable);
}
Thread.sleep(1500);
pauseableThreadPool.pause();
System.out.println("线程池被暂停了");
Thread.sleep(1500);
pauseableThreadPool.resume();
System.out.println("线程池被恢复了");
Thread.sleep(1500);
System.out.println("关闭线程池");
pauseableThreadPool.shutdown();
}
/**
* 线程执行之前
*/
@Override
protected void beforeExecute(Thread t, Runnable r) {
super.beforeExecute(t, r);
lock.lock();
System.out.println(Thread.currentThread().getName()+"=>beforeExecute");
try {
while (isPaused) {
unPaused.await();
}
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
/**
* 线程执行之后
*/
@Override
protected void afterExecute(Runnable r, Throwable t) {
System.out.println(Thread.currentThread().getName()+"=>afterExecute");
super.afterExecute(r, t);
}
/**
* 线程暂停
*/
private void pause() {
lock.lock();
System.out.println(Thread.currentThread().getName()+"=>pause");
try {
isPaused = true;
} finally {
lock.unlock();
}
}
/**
* 线程被唤醒
*/
public void resume() {
lock.lock();
System.out.println(Thread.currentThread().getName()+"=>resume");
try {
isPaused = false;
//唤醒全部
unPaused.signalAll();
} finally {
lock.unlock();
}
}
/**
* 线程池关闭后
*/
@Override
protected void terminated() {
System.out.println(Thread.currentThread().getName()+"=>terminated");
super.terminated();
}
}
/**
* @param t 执行任务的线程
* @param r 将要被执行的任务
*/
protected void beforeExecute(Thread t, Runnable r) {
}
/**
* @param r 将要被执行的任务
* @param t 异常信息
*/
protected void afterExecute(Runnable r, Throwable t) {
}
11.线程池状态
线程池有这几个状态:RUNNING,SHUTDOWN,STOP,TIDYING,TERMINATED。 对应java.util.concurrent.ThreadPoolExectutor的5种状态
private static final int RUNNING = -1 << COUNT_BITS;//运行
private static final int SHUTDOWN = 0 << COUNT_BITS;//关闭
private static final int STOP = 1 << COUNT_BITS;//停止
private static final int TIDYING = 2 << COUNT_BITS;//
private static final int TERMINATED = 3 << COUNT_BITS;//终止
线程池各个状态切换图
RUNNING(运行状态)
- 该状态的线程池会接收新任务,并处理阻塞队列中的任务;
- 调用线程池的
shutdown()
方法,可以切换到SHUTDOWN
状态; - 调用线程池的
shutdownNow()
方法,可以切换到STOP
状态;接受新任务并处理排队任务
SHUTDOWN(待关闭状态)
- 该状态的线程池不会接收新任务,但会处理阻塞队列中的任务;
队列为空,并且线程池中执行的任务也为空
,进入TIDYING
状态;当阻塞队列中的任务为空,并且工作线程数为0时,进入 TIDYING 状态
不接受新的任务但是处理排队任务
STOP(停止状态)
- 该状态的线程不会接收新任务,也不会处理阻塞队列中的任务,而且会中断正在运行的任务,
- 线程池中
执行的任务为空
,进入TIDYING
状态;当工作线程数为0时,进入 TIDYING 状态
就是调用shutdownNow()
带来的效果
TIDYING(整理状态)
- 该状态表明
所有的任务已经运行终止
,记录的任务数量为0
。 terminated()
执行完毕,进入TERMINATED状态任务都已经终止,
workerCount
为0
时,线程会转换到TIDYING
状态,并将运行terminate()
钩子方法
TERMINATED(终止状态)
- 该状态表示线程池
彻底终止
,并完成了所有资源的释放terminate()
运行完成
12.线程状态和工作线程数量
线程池是有状态的,不同状态下线程池的行为是不一样的,而且控制线程资源合理高效的使用,必须控制工作线程的个数,所以线程池内部需要保存当前线程池中工作线程的个数与状态
。
看到这里,你是线程池内部是用两个变量
来保存线程池的状态和线程池中工作线程的个数
呢?
- 在
ThreadPoolExecutor
中只用了一个AtomicInteger 型
的变量就保存了这两个属性的值,那就是ctl
。private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
ctl
的高3位
用来表示线程池的状态(runState),低29位
用来表示工作线程的个数(workerCnt)
为什么要用3位来表示线程池的状态呢?
原因是线程池一共有5种状态
,而2位只能表示出4种情况
,所以至少需要3位
才能表示得了5种状态。
三.线程池执行流程
上图是一张线程池工作的精简图,实际的过程比这个要复杂的多,不过这些应该能够完全覆盖到线程池的整个工作流程了。
整个过程可以拆分成以下几个部分:
1.提交任务
当向线程池提交一个新的任务时,线程池有三种处理情况
,分别是:1.创建一个工作线程来执行该任务、2.将任务加入阻塞队列、3.拒绝该任务。
提交任务的过程也可以拆分成以下几个部分:
- 当工作线程数小于核心线程数时,直接创建新的核心工作线程
- 当工作线程数不小于核心线程数时,就需要尝试将任务添加到阻塞队列中去
- 如果能够加入成功,说明队列还没有满,那么需要做以下的
二次验证
来保证添加进去的任务能够成功被执行- 验证当前线程池的运行状态,如果是
非RUNNING
状态,则需要将任务从阻塞队列中移除,然后拒绝该任务 - 验证当前线程池中的工作线程的个数,如果为
0
,则需要主动添加一个空工作线程来执行刚刚添加到阻塞队列中的任务
- 验证当前线程池的运行状态,如果是
- 如果加入失败,则说明队列已经满了,那么这时就需要创建新的
“临时”
工作线程来执行任务 - 如果创建成功,则直接执行该任务
- 如果创建失败,则说明工作线程数已经等于最大线程数了,则只能拒绝该任务了
整个过程可以用下面这张图来表示:
2.创建工作线程
创建工作线程需要做一系列的判断,需要确保当前线程池可以创建新的线程之后,才能创建。
- 首先,当线程池的状态是
SHUTDOWN
或者STOP
时,则不能创建新的线程。 - 当
线程工厂创建线程失败
时,也不能创建新的线程。 - 当前工作线程的数量与核心线程数、最大线程数进行比较,如果前者大于后者的话,也不允许创建。
- 除此之外,会尝试通过
CAS
来自增工作线程的个数,如果自增成功
了,则会创建新的工作线程,即 Worker 对象。然后加锁进行二次验证
是否能够创建工作线程,最后如果创建成功,则会启动该工作线程。
3.启动线程
- 当
工作线程
创建成功后,也就是Worker 对象
已经创建好了,这时就需要启动该工作线程,让线程开始干活了,Worker 对象中关联着一个Thread
,所以要启动工作线程的话,只要通过worker.thread.start()
来启动该线程即可。 - 启动完了之后,就会执行
Worker
对象的run 方法
,因为 Worker 实现了Runnable 接口
,所以本质上 Worker 也是一个线程。 - 通过线程
start()
开启之后就会调用到 Runnable 的run 方法
,在 worker 对象的run 方法
中,调用了runWorker(this)
方法,也就是把当前对象传递给了runWorker
方法,让他来执行。
4.获取任务并执行
-
在
runWorker 方法
被调用之后,就是执行具体的任务了,首先需要拿到一个可以执行的任务,而 Worker 对象中默认绑定了一个任务,如果该任务不为空的话,那么就是直接执行。 -
执行完了之后,就会去阻塞队列中获取任务来执行,而获取任务的过程,需要考虑
当前工作线程的个数
。- 如果工作线程数大于核心线程数,那么就需要通过
poll
来获取,因为这时需要对闲置的线程进行回收; - 如果工作线程数小于等于核心线程数,那么就可以通过
take
来获取了,因此这时所有的线程都是核心线程,不需要进行回收,前提是没有设置allowCoreThreadTimeOut
- 如果工作线程数大于核心线程数,那么就需要通过
四.线程池的工作队列
1.ArrayBlockingQueue
ArrayBlockingQueue: (有界队列)是一个用数组
实现的有界阻塞队列
,特性先进先出(FIFO)
。
@Test
public void testArrayBlockingQueue() throws InterruptedException {
ArrayBlockingQueue<String> queue = new ArrayBlockingQueue(5);
//生产者(添加元素)
new Thread( () -> {
while (true) {
try {
String data = UUID.randomUUID().toString();
queue.put(data);
System.out.println("Put: " + data+"——size:"+queue.size());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//消费者1(取出元素)
new Thread( () -> {
while (true) {
try {
String data = queue.take();
System.out.println(Thread.currentThread().getName() + " take(): " + data+"——size:"+queue.size());
Thread.sleep(1200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//休眠100S防止主线程直接结束
Thread.sleep(100000);
}
执行结果
2.LinkedBlockingQueue
LinkedBlockingQueue: (可设置容量队列)基于链表结构
的阻塞队列,按FIFO排序任务,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列
,最大长度和默认长度为Integer.MAX_VALUE
,吞吐量通常要高于ArrayBlockingQuene
newFixedThreadPool线程池使用了这个队列
@Test
public void testLinkedBlockingQueue() throws InterruptedException {
LinkedBlockingQueue<String> queue = new LinkedBlockingQueue(5);
//生产者(添加元素)
new Thread( () -> {
while (true) {
try {
String data = UUID.randomUUID().toString();
queue.put(data);
System.out.println("Put: " + data+"——size:"+queue.size());
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//消费者1(取出元素)
new Thread( () -> {
while (true) {
try {
String data = queue.take();
System.out.println(Thread.currentThread().getName() + " take(): " + data+"——size:"+queue.size());
Thread.sleep(1200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//休眠100S防止主线程直接结束
Thread.sleep(100000);
}
执行效果:
3.DelayQueue
DelayQueue: (延迟队列)是一个任务定时周期
的延迟执行的队列。根据指定的执行时间从小到大排序
,否则根据插入到队列的先后排序。传入的对象必须实现Delay接口。
newScheduledThreadPool线程池使用了这个队列。
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
队列中每个元素均有过期时间,当从队列获取元素时,只有过期元素才会出队列。队列头元素是最块要过期的元素。
过期元素才会出队列。队列头元素是最块要过期的元素。
具体实例
//DelayQueue保存的元素
public class Item implements Delayed {
String name;
//触发时间
private long time;
public Item(String name, long time, TimeUnit unit) {
this.name = name;
this.time = System.currentTimeMillis() + (time > 0 ? unit.toMillis(time) : 0);
}
@Override
public long getDelay(TimeUnit unit) {
return time - System.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
Item item = (Item) o;
long diff = this.time - item.time;
if (diff <= 0) {
// 改成>=会造成问题
return -1;
} else {
return 1;
}
}
@Override
public String toString() {
return "Item{" +
"time=" + time +
", name='" + name + '\'' +
'}';
}
@Test
public void testDelayQueue() throws InterruptedException {
Item item1 = new Item("item1", 5, TimeUnit.SECONDS);
Item item2 = new Item("item2", 10, TimeUnit.SECONDS);
Item item3 = new Item("item3", 15, TimeUnit.SECONDS);
DelayQueue<Item> queue = new DelayQueue<>();
queue.put(item1);
queue.put(item2);
queue.put(item3);
System.out.println("begin time:" + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
for (int i = 0; i < 3; i++) {
Item take = queue.take();
System.out.format("name:{%s}, time:{%s}\n", take.name, LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
}
/*
* begin time:2020-07-07T17:19:53.038
* name:{item1}, time:{2020-07-07T17:19:57.982}
* name:{item2}, time:{2020-07-07T17:20:02.982}
* name:{item3}, time:{2020-07-07T17:20:07.982}
*/
}
}
4.PriorityBlockingQueue
PriorityBlockingQueue:(优先级队列):使用平衡二叉树堆
实现的具有优先级
的无界阻塞队列,传入的对象必须实现Comparable接口,也可以构造方法传入比较器Comparator。
@Test
public void testPriorityBlockingQueue() throws InterruptedException {
//使用默认排序方式,即按自然顺序排序(即从小到大),可以通过元素实现Comparable接口或者构造时传入Comparator进行自定义排序
PriorityBlockingQueue<Integer> queue = new PriorityBlockingQueue(5);
queue.put(6);
queue.put(4);
queue.put(3);
queue.put(1);
queue.put(2);
queue.put(7);
System.out.println(queue.poll());//1
System.out.println(queue.poll());//2
}
执行结果
5.SynchronousQueue
SynchronousQueue:(同步队列)一个不存储元素
的阻塞队列,每个插入操作必须等到另一个线程调用移除操作
,否则 插入操作一直处于阻塞状态
,吞吐量通常要高于LinkedBlockingQuene
newCachedThreadPool线程池使用了这个队列。
@Test
public void testSynchronousQueue() throws InterruptedException {
SynchronousQueue<String> queue = new SynchronousQueue();
//生产者(添加元素)
new Thread( () -> {
while (true) {
try {
String data = UUID.randomUUID().toString();
System.out.println("Put: " + data);
queue.put(data);
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
//消费者1(取出元素)
new Thread( () -> {
while (true) {
try {
String data = queue.take();
System.out.println(Thread.currentThread().getName()
+ " take(): " + data);
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
Thread.sleep(100000);
}
执行结果
7.有界队列与无界队列区别
有界队列: 就是有设置固定大小的队列
。比如设定了固定大小的 LinkedBlockingQueue、又或者大小为 0,只是在生产者和消费者中做中转用的 SynchronousQueue。
无界队列: 指的是没有设置固定大小的队列
。这些队列的特点是可以直接入列,直到溢出
。当然现实几乎不会有到这么大的容量(超过 Integer.MAX_VALUE
),所以从使用者的体验上,就相当于“无界”
。比如没有设定固定大小的 LinkedBlockingQueue。
7.1.有界队列
- 当工作线程数<corePoolSize,提交的任务,会直接做为new一个Thread的参数,立马执行 。
- 当工作线程数>corePoolSize,会将提交的任务到一个阻塞队列中中,。
- 有界队列满了之后,如果当前总线程数 < maximumPoolsize时,会尝试new 一个Thread的进行救急处理,立马执行对应的runnable任务。
- 如果3中也无法处理了,就会走到第四步执行reject操作(拒绝提交)。
7.2.无界队列
- 与有界队列相比,除非
系统资源耗尽
,否则无界队列不存在任务入队失败的
情况。 - 当有新的任务到来,当前线程数小于corePoolSize时,则新建线程执行任务。当达到corePoolSize后,就不会继续新建线程了,若后面还有新的任务提交,而且没有空闲的线程资源,则任务直接进入队列等待。
- 若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。
对于无界队列来讲,当核心线程数满了后,任务优先进入等待队列。如果等待队列也满了后,才会去创建新的非核心线程 。
所以即使线程池的maximumPoolSize的设置的再大对于线程的执行是没有影响的。
7.3.个人理解
队列为有界无界,需要看创建创建队列时有没有指定容量
,有设置大小的为有界队列,没有设置大小的为无界队列(Integer.MAX_VALUE)
(
如果队列是无界队列,任务来了可以直接入队,几乎不会满,除非系统资源耗尽了
)
五.常用的线程池
1.newFixedThreadPoo
固定数量线程的线程池
- 构造方法
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
return new ThreadPoolExecutor(
nThreads,
nThreads,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>(),
threadFactory
);
}
1.1.特点
- 核心线程数和最大线程数大小一样 (
没有非核心线程
)- 没有所谓的非空闲时间,即
keepAliveTime=0
(内部的核心线程全部不销毁)- 阻塞队列为无界队列
LinkedBlockingQueue
- 核心线程数等于最大线程数,所以线程池中
只有核心线程
, 除非线程池被关闭,否则核心线程线程不会被回收- 当所有的线程都处于活动状态时,新的任务都会处于等待状态,直到有线程空闲出来。
- 阻塞队列是无界队列,不会执行拒绝策略,可能会在任务队列中堆集无限的请求,导致
OOM
1.2.工作机制
- 提交任务
- 如果线程数 < 核心线程,创建核心线程执行任务
- 如果线程数 = 核心线程,把任务添加到LinkedBlockingQueue阻塞队列
- 如果线程执行完任务,去阻塞队列取任务,继续执行。
1.3.结构图
-
如果当前运行的线程数少于corePoolSize,则创建新线程来执行任务。
-
在线程数目达到corePoolSize后,将新任务放到LinkedBlockingQueue阻塞队列中。
-
线程执行完(1)中任务后,会在循环中反复从LinkedBlockingQueue获取任务来执行。
1.4.实例代码
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
executor.execute(()->{
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
//do nothing
}
});
IDEA 指定JVM参数:
-Xmx8m -Xms8m
run以上代码,会抛出OOM:
面试题:使用无界队列的线程池会导致内存飙升吗?
答案 :会的,newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长(比如,上面demo设置了10秒),会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终导致OOM。
1.5.使用场景
FixedThreadPool
适用于处理CPU密集型
的任务,确保CPU在长期被工作线程使用的情况下,尽可能的少的分配线程,即适用执行长期的任务。
2.newCachedThreadPool
可缓存,无界的,自动回收多余线程线程的线程池。
它没有需要维护的核心线程数,每当需要线程的时候就进行创建,因为它的线程存活时间是60秒,所以它也凭借着这个参数实现了自动回收的功能。
- 构造方法
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(
0,
Integer.MAX_VALUE,
60L,
TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory
);
}
2.1.特点
- 核心线程数为0
(没有核心线程,全部是非核心线程)
- 最大线程数为
Integer.MAX_VALUE
- 阻塞队列是
SynchronousQueue
keepAliveTime=60
非核心线程空闲存活时间为60秒
核心线程数为0,总线程数量阈值为Integer.MAX_VALUE,即可以创建无限的非核心线程
- 当提交任务的速度大于处理任务的速度时,每次提交一个任务,就必然会创建一个线程。极端情况下会创建过多的线程,耗尽 CPU 和内存资源。由于空闲 60 秒的线程会被终止,长时间保持空闲的 CachedThreadPool 不会占用任何资源。
- 采用SynchronousQueue,
每当提交一个任务,都会超过阻塞队列的长度,导致创建线程
,所以说:每当提交一个任务,都会创建一个线程,可能造成OOM。- 有空闲线程则复用空闲线程,若无空闲线程则新建线程,超过60s则销毁线程(非核心线程),一定程度减少频繁创建/销毁线程的系统开销
2.2.工作机制
- 提交任务
- 因为没有核心线程,所以任务直接加到SynchronousQueue队列。
- 判断是否有空闲线程,如果有,就去取出任务执行。
- 如果没有空闲线程,就新建一个线程执行。
- 执行完任务的线程,还可以存活60秒,如果在这期间,接到任务,可以继续活下去;否则,被销毁。
2.3.结构图
2.4.实例代码
ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 5; i++) {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName()+"正在执行");
});
}
运行结果:
2.5.使用场景
- 用于并发执行大量短期的小任务。因为
最大线程数为Integer.MAX_VALUE
,所以提交任务的速度 > 线程池中线程处理任务的速度
就会不断创建新线程;每次提交任务,都会立即有线程去处理,因此CachedThreadPool适用于处理大量耗时少的任务
3.newSingleThreadExecutor
单线程的线程池,全程只以1条线程执行任务
- 构造方法
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService(
new ThreadPoolExecutor(
1,
1,
0L,
TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()
)
);
}
3.1.特点
- 核心线程数为1
- 最大线程数也为1
- 阻塞队列是无界队列 LinkedBlockingQueue
- keepAliveTime为0
- 所有任务按照指定顺序执行,即遵循队列的入队出队规则
- 缺点和固定线程池一样,可能会在任务队列中堆集无限的请求,导致OOM
和一个线程的区别
newSingleThreadExecutor | Thread |
---|---|
任务执行完成后,不会自动销毁,可以复用 | 任务执行完成后,会自动销毁 |
可以将任务存储在阻塞队列中,逐个执行 | 无法存储任务,只能执行一个任务 |
3.2.工作机制
- 提交任务
- 线程池是否有一条线程在,如果没有,新建线程执行任务
- 如果有,讲任务加到阻塞队列
- 当前的唯一线程,从队列取任务,执行完一个,再继续取,一个人(一条- 线程)夜以继日地干活。
3.3.结构图
- 当线程池中没有线程时,会创建一个新线程来执行任务。
- 当前线程池中有一个线程后,将新任务加入
LinkedBlockingQueue
- 线程
执行完第一个任
务后,会在一个无限循环
中反复从LinkedBlockingQueue 获取任务来执行。
3.4.实例代码
ExecutorService executor = Executors.newSingleThreadExecutor();
for (int i = 0; i < 5; i++) {
executor.execute(() -> {
System.out.println(Thread.currentThread().getName()+"正在执行");
});
}
运行结果:
3.5.使用场景
- 适用于
串行执行任务
的场景,一个任务一个任务
地执行。
4.newScheduledThreadPool
定时及周期执行的线程池
- 构造方法
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
return new ThreadPoolExecutor(
0,
Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>(),
threadFactory
);
}
4.1.特点
- 最大线程数为
Integer.MAX_VALUE
- 阻塞队列是
DelayedWorkQueue
keepAliveTime
为0
- scheduleAtFixedRate :按照固定速率周期执行
- scheduleWithFixedDelay:上个任务延迟固定时间后执行
4.2.工作机制
- 添加一个任务
- 线程池中的线程从 DelayQueue 中取任务
- 线程从 DelayQueue 中获取 time 大于等于当前时间的task
- 执行完后修改这个 task 的 time 为下次被执行的时间
- 这个 task 放回DelayQueue队列中
4.3.实例代码
scheduleAtFixedRate()方法
@Test
public void scheduleAtFixedRate() throws InterruptedException {
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
// 1秒后开始执行定时任务,每3秒执行一次:
scheduledExecutorService.scheduleAtFixedRate(() -> {
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("current Time" + System.currentTimeMillis());
System.out.println(Thread.currentThread().getName() + "正在执行");
}, 1, 3, TimeUnit.SECONDS);
;
//防止主线程结束
Thread.sleep(100000);
//关闭线程池
scheduledExecutorService.shutdown();
}
执行结果
结论:scheduleAtFixedRate(commod,initialDelay,period,unit)
,这个是以period
为固定周期时间,按照一定频率来重复执行任务,initialDelay
说系统启动后,需要等待多久才开始执行。例如:如果设置了period
为5秒,线程启动之后执行了大于5秒,线程结束之后,立即启动线程的下一次,如果线程启动之后只执行了3秒就结束了那执行下一次,需要等待2秒再执行。这个是优先保证任务执行的频率
scheduleWithFixedDelay()方法
@Test
public void scheduleWithFixedDelay() throws InterruptedException {
/*
* 创建一个给定初始延迟的间隔性的任务,之后的下次执行时间是上一次任务从执行到结束所需要的时间 + 给定的间隔时间
*/
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
long startTime = System.currentTimeMillis();
//5秒后开始执行定时任务,以3秒为间隔执行:
scheduledExecutorService.scheduleWithFixedDelay(() -> {
System.out.println("current Time=>" + System.currentTimeMillis());
System.out.println(Thread.currentThread().getName() + "正在执行");
}, 5, 3, TimeUnit.SECONDS);
//防止主线程结束
Thread.sleep(100000);
//关闭线程池
scheduledExecutorService.shutdown();
}
执行结果
结论:scheduleWithFixedDelay(commod,initialDelay,delay,unit)
,这个是以delay
为固定延迟时间,按照一定的等待时间来执行任务,initialDelay
也是说系统启动后,需要等待多久才开始执行。例如:设置了delay为5秒,线程启动之后不管执行了多久,结束之后都需要等待5秒,才能执行下一次。这个是优先保证任务执行的间隔。
schedule(commod,delay,unit)
这个方法是说系统启动后,需要等待多久执行,delay是等待时间。只执行一次,没有周期性。
4.4.使用场景
周期性执行任务的场景,需要限制线程数量的场景
回到面试题:说说几种常见的线程池及使用场景?
回答这四种经典线程池 :newFixedThreadPool,newSingleThreadExecutor,newCachedThreadPool,newScheduledThreadPool,分线程池特点,工作机制,使用场景分开描述,再分析可能存在的问题,比如newFixedThreadPool内存飙升问题 即可
除了newScheduledThreadPool
的内部实现特殊一点之外,其它几个线程池都是基于ThreadPoolExecutor类
实现的。
5.直接调用JDK封装好的线程池会带来的问题
使用无界队列的线程池会导致内存飙升吗?
答案 :会的,newFixedThreadPool使用了无界的阻塞队列LinkedBlockingQueue,如果线程获取一个任务后,任务的执行时间比较长,会导致队列的任务越积越多,导致机器内存使用不停飙升, 最终导致OOM。
六.任务提交两种方式
1.execute()和submit()
线程池框架提供了两种方式提交任务,根据不同的业务需求选择不同的方式。
-
Executor.execute()
通过Executor.execute()
方法提交的任务,必须实现Runnable接口
,该方式提交的任务不能获取返回值
,因此无法判断任务是否执行成功。 -
ExecutorService.submit()
通过ExecutorService.submit()
方法提交的任务,可以获取任务执行完的返回值。ctrl加鼠标左键 进入
submit
,查看AbstractExecutorService,发现submit底层调用的还是execute
,但是提交的任务不是task
,而是在task的基础上封装了一层FutureTask
public Future<?> submit(Runnable task) { if (task == null) throw new NullPointerException(); RunnableFuture<Void> ftask = newTaskFor(task, null); execute(ftask); return ftask; }
2.execute()执行流程
2.1.执行流程图
线程池执行流程,即对应execute()
方法:
- 提交一个任务,线程池里存活的
核心线程数小于corePoolSize时
,线程池会创建一个核心线程
去处理提交的任务。 - 如果线程池
核心线程数已满
,即线程数已经等于corePoolSize
,一个新提交的任务
,会被放进任务队列workQueue
排队等待执行。 - 当线程池里面
存活的线程数已经 等于 corePoolSize了
,并且任务队列workQueue也满了,判断线程数是否达到maximumPoolSize,即最大线程数是否已满
,如果没到达,创建一个非核心线程执行提交的任务
。 - 如果当前的线程数达到了
maximumPoolSize
,还有新的任务过来的话,直接采用拒绝策略处理
。
2.2.结构流程图
2.3.举例说明
为了形象描述线程池执行,我打个比喻:
- 核心线程比作公司正式员工
- 非核心线程比作外包员工
- 阻塞队列比作需求池
- 提交任务比作提需求
- 当产品提个需求,正式员工(
核心线程
)先接需求(执行任务
) - 如果正式员工都有需求在做,即
核心线程数已满
),产品就把需求先放需求池(阻塞队列
)。 - 如果需求池(
阻塞队列
)也满了,但是这时候产品继续提需求,怎么办呢?那就请外包(非核心线程
)来做。 - 如果所有员工(
最大线程数也满了
)都有需求在做了,那就执行拒绝策略
。 - 如果外包员工把需求(
任务
)做完了,它经过一段(keepAliveTime
)空闲时间,就离开公司了。
3.submit和execute区别
3.1.可以接受的任务类型
execute()
public void execute(Runnable command)
- execute只能接受Runnable类型的任务
submit()
<T> Future<T> submit(Callable<T> task);
<T> Future<T> submit(Runnable task, T result);
Future<?> submit(Runnable task);
- submit()不管是
Runnable
还是Callable
类型的任务都可以接受,但是Runnable
返回值均为void
,所以使用Future的get()
获得的还是null
3.2.返回值
由Callable
和Runnable
的区别可知:
- execute没有返回值
- submit有返回值,所以需要返回值的时候必须使用submit
3.3.异常
1.execute()中抛出异常
- execute()中的是
Runnable接口
的实现,所以只能使用try-catch
来捕获CheckedException
或者 通过实现UncaughtExceptionHande接口
处理UncheckedException
,即和普通线程的处理方式完全一致
2.submit()中抛出异常
- 不管提交的是
Runnable
还是Callable
类型的任务,如果不对返回值Future调用get()方法,都会吃掉异常
Callable接口
//call能够抛出Exception异常,所以不管是CheckedException还是UncheckedException,直接抛出即可
public interface Callable<V> {
V call() throws Exception;
}
测试代码
import java.util.concurrent.*;
public class ThreadExceptionTest {
public static void main(String[] args) {
ExecutorService executor = Executors.newCachedThreadPool();
Future<Boolean> future = executor.submit(new CallableTask());
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
executor.shutdown();//关闭线程池必须不能忘,否则主线程会一直阻塞
}
}
class CallableTask implements Callable<Boolean> {
public Boolean call() throws Exception {
// InputStream in = new FileInputStream(new File("xx.pdf"));
int num = 3 / 0;
return false;
}
}
七.线程池异常处理
在使用线程池处理任务的时候,任务代码可能抛出RuntimeException
,抛出异常后,线程池可能捕获它,也可能创建一个新的线程来代替异常的线程
,我们可能无法感知任务出现了异常,因此我们需要考虑线程池异常情况。
1.在run方法中捕获代码可能抛出的所有异常
execute提交任务会抛出异常,submit提交任务不会抛出异常, 使用submit时需要使用try-catch捕获可能要产生的异常
@Test
public void testThreadPoolSubmitExceptionHandle() throws IOException, InterruptedException {
ExecutorService singleThreadPool = Executors.newFixedThreadPool(5);
for (int i = 0; i < 5; i++) {
singleThreadPool.submit(() -> {
try {
Object object = null;
System.out.println(object.toString());
System.out.println("当前线程:" + Thread.currentThread().getName());
} catch (Exception e) {
System.out.println("当前线程:" + Thread.currentThread().getName()+"发生异常");
}
});
}
//原因是因为做单元测试时跟WEB项目不同,线程还没有开始启动,主线程已经关闭,只要我们加入一段代码,让主线程不关闭,这样就可以跑子线程的方法了
Thread.sleep(2000);
singleThreadPool.shutdown();//gracefully shutdown
}
执行结果:
2.通过Future对象的get方法接收抛出的异常
使用submit执行任务,可以利用返回的Future对象的get
方法接收抛出的异常,然后进行处理
2.1.了解线程池submit()的执行流程
submit提交任务的关键代源码
//构造feature对象
/**
* @throws RejectedExecutionException {@inheritDoc}
* @throws NullPointerException {@inheritDoc}
*/
public Future<?> submit(Runnable task) {
if (task == null) throw new NullPointerException();
RunnableFuture<Void> ftask = newTaskFor(task, null);//使用Future包裹Runnable
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Runnable runnable, T value) {
return new FutureTask<T>(runnable, value);
}
public FutureTask(Runnable runnable, V result) {
this.callable = Executors.callable(runnable, result);
this.state = NEW; // ensure visibility of callable
}
public static <T> Callable<T> callable(Runnable task, T result) {
if (task == null)
throw new NullPointerException();
return new RunnableAdapter<T>(task, result);
}
//线程池执行
public void execute(Runnable command) {
if (command == null)
throw new NullPointerException();
int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
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);
}
else if (!addWorker(command, false))
reject(command);
}
//捕获异常
public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);//设置异常
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
通过以上分析
- submit在执行过程中与execute不一样,不会抛出异常而是把异常保存在成员变量中,在
FutureTask.get
阻塞获取的时候再把异常抛出来 execute直接抛出异常
之后线程就死掉了,submit保存异常线程没有死掉
,因此execute的线程池可能会出现任务丢失情况,因为线程没有得到重用。而submit不会出现这种情况
2.2.举例说明
@Test
public void testThreadPoolSubmitExceptionHandle() throws IOException, InterruptedException {
//创建一个固定长度的线程池
ExecutorService fixedThreadPool = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());
List<Future> futureList = new ArrayList<>();
for (int i = 0; i < 5; i++) {
futureList.add(fixedThreadPool.submit(() -> {
System.out.println("当前线程:" + Thread.currentThread().getName());
Object object = null;
System.out.println(object.toString());
}));
}
//原因是因为做单元测试时跟WEB项目不同,线程还没有开始启动,主线程已经关闭,只要我们加入一段代码,让主线程不关闭,这样就可以跑子线程的方法了
boolean flag = false;
do {
flag = false;
for (Future future : futureList) {
//如果任务未完成,继续循环=>任务完成可能是 任务正常终止/异常/取消
if (!future.isDone()) {
flag = true;
}
try {
future.get();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
//果任务未完成,休眠10秒继续循环
Thread.sleep(10);
} while (flag);
fixedThreadPool.shutdown();
}
执行结果:
3.设置UncaughtExceptionHandler (不推荐)
为工作线程设置UncaughtExceptionHandler,在uncaughtException方法中处理异常
不推荐重写UncaughtExceptionHandler
,因为UncaughtExceptionHandler 只有在execute.execute()方法中才生效
,在execute.submit中是无法捕获到异常的
。
@Test
public void testSetUncaughtExceptionHandler() {
//创建一个固定长度的线程池
ExecutorService fixedThreadPool = new ThreadPoolExecutor(1, 1, 0L, TimeUnit.SECONDS, new LinkedBlockingQueue<>(30), r -> {
Thread t = new Thread(r);
t.setUncaughtExceptionHandler(
(t1, e) -> {
System.out.println(t1.getName() + "-线程抛出的异常" + e);
});
return t;
});
fixedThreadPool.execute(() -> {
System.out.println(Thread.currentThread().getName());
Object object = null;
System.out.print("result## " + object.toString());
});
fixedThreadPool.shutdown();
}
执行结果:
4.重写ThreadPoolExecutor
重写ThreadPoolExecutor的afterExecute方法,处理传递的异常引用
import java.util.concurrent.*;
/**
* 重写ThreadPoolExecutor实现afterExecute方法处理异常
*/
public class CustomThreadPoolExecutorDemo {
public static void main(String args[]) {
//相当于 Executors.newFixedThreadPool(10)
CustomThreadPoolExecutor service = new CustomThreadPoolExecutor();
service.submit(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName());
Object object = null;
System.out.println(object.toString());
}
});
service.shutdown();
}
}
class CustomThreadPoolExecutor extends ThreadPoolExecutor {
public CustomThreadPoolExecutor() {
//调用父类构造方法实例化线程对象
super(1, 10, 60, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(1000));
}
@Override
protected void afterExecute(Runnable runnable, Throwable throwable) {
super.afterExecute(runnable, throwable);
if (throwable == null && runnable instanceof Future<?>) {
try {
Object result = ((Future<?>) runnable).get();
System.out.println(result);
} catch (CancellationException ce) {
throwable = ce;
} catch (ExecutionException ee) {
throwable = ee.getCause();
} catch (InterruptedException ie) {
Thread.currentThread().interrupt(); // ignore/reset
}
}
if (throwable != null) {
throwable.printStackTrace();
}
}
}
执行结果:
5. 处理线程池异常的4种方法
- 在我们提供的Runnable的
run方法中捕获任务代码可能抛出的所有异常
- 使用
submit
执行任务,利用返回的Future对象的get方法
接收抛出的异常,然后进行处理 - 重写
ThreadPoolExecutor
在afterExecute
方法,处理传递到afterExecute方法中的异常 - 为线程设置
UncaughtExceptionHandler
,在uncaughtException方法中处理异常(submit不推荐)
八.线程池原理浅析
1.线程池组成部分
- 线程池管理器
- 工作线程
- 任务队列
- 任务
2.Executor家族
-
Executor: 它是一个顶层接口,其他接口以及类都继承或实现于它,只有一个方法
void execute(Runnable command);
-
ExecutorService: 它继承于Executor,是Executor的子接口,增加了一些常用的对线程的控制方法,之后使用线程池主要也是使用这些方法。
如 shutdown(),isShutdown(),shutdownNow()
-
AbstractExecutorService: 是一个抽象类。ThreadPoolExecutor就是实现了这个类。
-
Executors: 这个类是一个工具类,里面包含一些创建线程池的方法
Java 里面线程池的
顶级接口是 Executor
,但是严格意义上讲 Executor 并不是一个线程池,而只是一个执行线程的工具
。真正的线程池接口是 ExecutorService
。
3.线程池的线程复用的原理
源码分析
public void execute(Runnable command) {
// 判断任务是否为空,为空就抛出异常
if (command == null)
throw new NullPointerException();
int c = ctl.get();
// 如果当前线程数小于核心线程数,就增加Worker
if (workerCountOf(c) < corePoolSize) {
// command就是任务,点击addWorker方法
// 第二个参数用于判断当前线程数是否小于核心线程数
if (addWorker(command, true))
return;
c = ctl.get();
}
// 此时线程数大于等于核心线程数
// 判断线程池是不是正在运行并将任务放到工作队列中
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);
}
// 如果任务无法添加或者大于最大线程数就拒绝任务
else if (!addWorker(command, false))
reject(command);
}
因为要查看的是Worker
所以进入到addWorker()
方法后点击Worker
类查看runWorker()
方法
w = new Worker(firstTask);
Worker 是ThreadPoolExecutor的内部类
private final class Worker extends AbstractQueuedSynchronizer implements Runnable
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
// 获取到任务
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 只要任务不为空或者能够获取到任务就执行下面的方法
while (task != null || (task = getTask()) != null) {
w.lock();
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
beforeExecute(wt, task);
Throwable thrown = null;
try {
// task是一个Runnable类型,调用run()方法就是运行线程
task.run();
} catch (RuntimeException x) {
thrown = x; throw x;
} catch (Error x) {
thrown = x; throw x;
} catch (Throwable x) {
thrown = x; throw new Error(x);
} finally {
afterExecute(task, thrown);
}
} finally {
task = null;
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
总结: 核心原理就是获取到task
,如果task不为空
就调用run()
方法,这样就实现了线程的复用,达到让相同的线程执行不同任务的目的。
4.使用线程池的注意点
- 避免任务的堆积(堆积容易产生内存溢出)
- 避免线程数过多增加(缓存线程池会导致线程数过度增加)
- 排查线程泄漏(线程已经执行完毕却无法被回收)
5.线程池特性
5.1.规则描述
线程池的线程执行规则跟任务队列有界或无界有很大
的关系。
假设任务队列大小没有限制:
- 如果工作线程数量<=核心线程数量,直接启动一个核心线程来执行任务,不会放入队列中。
- 如果工作线程数量>核心线程数,但<=最大线程数,并且任务队列是
LinkedBlockingDeque
时,会将超过核心线程数的任务会放在任务队列中排队。 - 如果工作线程数量>核心线程数,但<=最大线程数,并且任务队列是
SynchronousQueue
时,线程池会创建非核心线程执行任务,且任务也不会被放在任务队列中。在任务完成后,空闲时间达到了超时时间就会被清除。 - 如果工作线程数量>核心线程数,并且>最大线程数,当任务队列是
LinkedBlockingDeque
,会将超过核心线程的任务放在任务队列中排队。也就是当任务队列是没有设置大小的LinkedBlockingDeque,线程池的最大线程数设置是无效的,他的线程数最多不会超过核心线程数。 - 如果工作线程数量>核心线程数,并且>最大线程数,当任务队列是SynchronousQueue时,会因为线程池拒绝添加任务而抛出异常(RejectedExecutionException)。
假设任务队列大小有限制时:
- 如果工作线程数量>核心线程数,且任务队列是
LinkedBlockingDeque
时, 会将超过核心线程数的任务会放在任务队列中排队, 当任务队列塞满
时,新增的任务会直接创建新线程(非核心线程)来执行,当创建的线程数量超过最大线程数量时会抛异常(RejectedExecutionException)。 SynchronousQueue没有数量限制
。因为他根本不保存这些任务,而是直接交给线程池去执行。当任务数量超过最大线程数时会直接抛异常(RejectedExecutionException)。
5.2.规则验证
前提
下面所有的任务都是下面这样的,睡眠两秒后打印一行日志:
Runnable runnable = new Runnable() {
@Override
public void run() {
try {
Thread.sleep(2000);
System.out.println(Thread.currentThread().getName() + " run");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
所有验证过程都是下面这样,先
执行三个任务,打印线程池信息,然后
再执行三个任务,打印线程池信息,最后
线程休眠8秒后,打印线程池信息.
executor.execute(runnable);
executor.execute(runnable);
executor.execute(runnable);
System.out.println("---先开三个---");
System.out.println("核心线程数=>" + executor.getCorePoolSize() + ",线程池数=>" + executor.getPoolSize() + ",队列任务数=>" + executor.getQueue().size());
executor.execute(runnable);
executor.execute(runnable);
executor.execute(runnable);
System.out.println("---再开三个---");
System.out.println("核心线程数=>" + executor.getCorePoolSize() + ",线程池数=>" + executor.getPoolSize() + ",队列任务数=>" + executor.getQueue().size());
Thread.sleep(8000);
System.out.println("----8秒之后----");
System.out.println("核心线程数=>" + executor.getCorePoolSize() + ",线程池数=>" + executor.getPoolSize() + ",队列任务数=>" + executor.getQueue().size());
//关闭线程池,不关闭的话由于主线程内一直存在6个核心线程,虚拟机不会关闭
executor.shutdown();
5.2.1.验证1
核心线程数为6,最大线程数为10,超时时间为5秒,队列是SynchronousQueue
ThreadPoolExecutor executor = new ThreadPoolExecutor(6, 10, 5, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
执行结果
结论: 因为SynchronousQueue
不保存任务,收到一个任务就去创建新线程。每个任务都是是直接启动一个线程来执行任务,一共创建了6个线程。8秒后线程池没有因为空闲时间被还是6个线程,因此核心线程默认情况下不会被回收,不受超时时间影响。
5.2.2.验证2
核心线程数为3,最大线程数为6。超时时间为5秒,队列是LinkedBlockingDeque
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 6, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
执行结果
结论:当任务数超过核心线程数时,会将超出的任务放在队列中,只会创建3个线程重复利用。
5.2.3.验证3
核心线程数为3,最大线程数为6,超时时间为5秒,队列是SynchronousQueue
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 6, 5, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
执行结果
结论:当队列是SynchronousQueue时,不会保存任务
,超出核心线程的任务会创建新的线程来执行,看到一共有6个线程。但是这些线程中有3个是非核心线程
,受超时时间影响,在任务完成后空闲超过5秒就会被回收。所以最后看到线程池还是只有三个线程。
5.2.4.验证4
- 核心线程数是3,最大线程数是4,超时时间为5秒,队列是LinkedBlockingDeque
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 4, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>());
执行结果
结论:LinkedBlockingDeque
根本不受最大线程数影响
但是当
LinkedBlockingDeque
有大小限制时就会受最大线程数影响了(例2会说明
)
- 核心线程数是3,最大线程数是4,队列是长度为2的LinkedBlockingDeque
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 4, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(2));
执行结果
结论: 首先为三个任务开启了三个核心线程1,2,3,然后第四个任务和第五个任务加入到队列中,第六个任务因为队列满了,就直接创建一个新线程4,这是一共有四个线程,没有超过最大线程数。8秒后,非核心线程受到超时时间影响被回收了,因此线程池只剩3个线程。
当队列中的任务满了后,创建的线程 > 线程池最大线程数,默认情况下会拒绝处理任务,抛出RejectedExecutionException(
例3会说明
)
- 将队列大小设置为1
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 4, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(1));
执行结果
异常信息详细如下
结论:直接出错在第6个execute方法上。因为核心线程是3个,当加入第四个任务的时候,就把第四个放在队列中。加入第五个任务时,因为队列满了,就创建新线程执行,创建了线程4。加入第六个线程时,也会尝试创建线程,但是因为已经达到了线程池最大线程数,所以直接抛异常RejectedExecutionException
5.2.5.验证5
核心线程数是3 ,最大线程数是4,队列是SynchronousQueue
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 4, 5, TimeUnit.SECONDS, new SynchronousQueue<Runnable>());
执行结果
异常信息详细如下
结论:这次在添加第五个任务时就报错了,因为SynchronousQueue
不保存任务,收到一个任务就去创建新线程。所以第五个就会抛异常了。