Java并发专题:线程池
1、线程池的优势
- 降低系统资源消耗。通过重用已存在的线程,降低线程创建和销毁造成的消耗;
- 提高系统响应速度。当有任务到达时,通过复用已存在的线程,无需等待新线程的创建便能立即执行;
- 方便线程并发数的管控。因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换(cpu切换线程是有时间成本的(需要保持当前执行线程的现场,并恢复要执行线程的现场))。
- 提供更强大的功能,延时定时线程池。
2、线程池的主要参数
public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
- corePoolSize(线程池基本大小):当向线程池提交一个任务时,若线程池已创建的线程数小于corePoolSize,即便此时存在空闲线程,也会通过创建一个新线程来执行该任务,直到已创建的线程数大于或等于corePoolSize时,(除了利用提交新任务来创建和启动线程(按需构造),也可以通过 prestartCoreThread() 或 prestartAllCoreThreads() 方法来提前启动线程池中的基本线程。)
- maximumPoolSize(线程池最大大小):线程池所允许的最大线程个数。当队列满了,且已创建的线程数小于maximumPoolSize,则线程池会创建新的线程来执行任务。另外,对于无界队列,可忽略该参数。
- keepAliveTime(线程存活保持时间):当线程池中线程数大于核心线程数时,线程的空闲时间如果超过线程存活时间,那么这个线程就会被销毁,直到线程池中的线程数小于等于核心线程数。
- unit:存活时间的单位
- workQueue(任务队列):用于传输和保存等待执行任务的阻塞队列。
- threadFactory(线程工厂):用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n(m为线程池的编号,n为线程池内的线程编号)。
- handler(线程饱和策略):当线程池和队列都满了,再加入线程会执行此策略。
- AbortPolicy: 直接拒绝所提交的任务,并抛出RejectedExecutionException异常;
- CallerRunsPolicy:只用调用者所在的线程来执行任务;
- DiscardPolicy:不处理直接丢弃掉任务;
- DiscardOldestPolicy:丢弃掉阻塞队列中存放时间最久的任务,执行当前任务
3、线程池流程

1、判断核心线程池是否已满,没满则创建一个新的工作线程来执行任务。已满则。
2、判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
3、判断整个线程池是否已满,没满则创建一个新的工作线程来执行任务,已满则执行饱和策略。
(1、判断线程池中当前线程数是否大于核心线程数,如果小于,在创建一个新的线程来执行任务,如果大于则
2、判断任务队列是否已满,没满则将新提交的任务添加在工作队列,已满则。
3、判断线程池中当前线程数是否大于最大线程数,如果小于,则创建一个新的线程来执行任务,如果大于,则执行饱和策略。)
4、创建线程池
手动创建线程池(推荐)
那么上面说了使用Executors工具类创建的线程池有隐患,那如何使用才能避免这个隐患呢?对症下药,建立自己的线程工厂类,灵活设置关键参数:
//这里默认拒绝策略为AbortPolicy private static ExecutorService executor = new ThreadPoolExecutor(10,10,60L, TimeUnit.SECONDS,new ArrayBlockingQueue(10));
使用guava包中的ThreadFactoryBuilder工厂类来构造线程池:
private static ThreadFactory threadFactory = new ThreadFactoryBuilder().build();
private static ExecutorService executorService = new ThreadPoolExecutor(10, 10, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10), threadFactory, new ThreadPoolExecutor.AbortPolicy());
通过guava的ThreadFactory工厂类还可以指定线程组名称,这对于后期定位错误时也是很有帮助的
ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-pool-d%").build();
5.Springboot中使用线程池
springboot可以说是非常流行了,下面说说如何在springboot中优雅的使用线程池
@Configuration public class ThreadPoolConfig { @Bean(value = "threadPoolInstance") public ExecutorService createThreadPoolInstance() { //通过guava类库的ThreadFactoryBuilder来实现线程工厂类并设置线程名称 ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("thread-pool-%d").build(); ExecutorService threadPool = new ThreadPoolExecutor(10, 16, 60L, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(100), threadFactory, new ThreadPoolExecutor.AbortPolicy()); return threadPool; } } //通过name=threadPoolInstance引用线程池实例 @Resource(name = "threadPoolInstance") private ExecutorService executorService; @Override public void spikeConsumer() { //TODO executorService.execute(new Runnable() { @Override public void run() { //TODO }}); }
6.其它相关
在ThreadPoolExecutor类中有两个比较重要的方法引起了我们的注意:beforeExecute和afterExecute
protected void beforeExecute(Thread var1, Runnable var2) { } protected void afterExecute(Runnable var1, Throwable var2) { }
这两个方法是protected修饰的,很显然是留给开发人员去重写方法体实现自己的业务逻辑,非常适合做钩子函数,在任务run方法的前后增加业务逻辑,比如添加日志、统计等。这个和我们springmvc中拦截器的preHandle和afterCompletion方法很类似,都是对方法进行环绕,类似于spring的AOP,参考下图:
7、Callable和Runnable
Runnable和Callable都可以理解为任务,里面封装这任务的具体逻辑,用于提交给线程池执行,区别在于Runnable任务执行没有返回值,且Runnable任务逻辑中不能通过throws抛出cheched异常(但是可以try catch),而Callable可以获取到任务的执行结果返回值且抛出checked异常。
@FunctionalInterface public interface Runnable { public abstract void run(); } @FunctionalInterface public interface Callable<V> { V call() throws Exception; }
8、Future和FutureTask
Future接口用来表示执行异步任务的结果存储器,当一个任务的执行时间过长就可以采用这种方式:把任务提交给子线程去处理,主线程不用同步等待,当向线程池提交了一个Callable或Runnable任务时就会返回Future,用Future可以获取任务执行的返回结果。Future的主要方法包括:
- get()方法:返回任务的执行结果,若任务还未执行完,则会一直阻塞直到完成为止,如果执行过程中发生异常,则抛出异常,但是主线程是感知不到并且不受影响的,除非调用get()方法进行获取结果则会抛出ExecutionException异常;
- get(long timeout, TimeUnit unit):在指定时间内返回任务的执行结果,超时未返回会抛出TimeoutException,这个时候需要显式的取消任务;
- cancel(boolean mayInterruptIfRunning):取消任务,boolean类型入参表示如果任务正在运行中是否强制中断;
- isDone():判断任务是否执行完毕,执行完毕不代表任务一定成功执行,比如任务执行失但也执行完毕、任务被中断了也执行完毕都会返回true,它仅仅表示一种状态说后面任务不会再执行了;
- isCancelled():判断任务是否被取消;
下面来实际演示Future和FutureTask的用法:
public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(10); Future<Integer> future = executorService.submit(new Task()); Integer integer = future.get(); System.out.println(integer); executorService.shutdown(); } static class Task implements Callable<Integer> { @Override public Integer call() throws Exception { System.out.println("子线程开始计算"); int sum = 0; for (int i = 0; i <= 100; i++) { sum += i; } return sum; } } public static void main(String[] args) throws ExecutionException, InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(10); FutureTask<Integer> futureTask = new FutureTask<>(new Task()); executorService.submit(futureTask); Integer integer = futureTask.get(); System.out.println(integer); executorService.shutdown(); } static class Task implements Callable<Integer> { @Override public Integer call() throws Exception { System.out.println("子线程开始计算"); int sum = 0; for (int i = 0; i <= 100; i++) { sum += i; } return sum; } }
9、线程池为什么需要使用(阻塞)队列?
回到了非线程池缺点中的第3点:
1、因为线程若是无限制的创建,可能会导致内存占用过多而产生OOM,并且会造成cpu过度切换。
另外回到了非线程池缺点中的第1点:
2、创建线程池的消耗较高。
或者下面这个网上并不高明的回答:
2、线程池创建线程需要获取mainlock这个全局锁,影响并发效率,阻塞队列可以很好的缓冲。
10、线程池为什么要使用阻塞队列而不使用非阻塞队列?
阻塞队列可以保证任务队列中没有任务时阻塞获取任务的线程,使得线程进入wait状态,释放cpu资源。
当队列中有任务时才唤醒对应线程从队列中取出消息进行执行。
使得在线程不至于一直占用cpu资源。
(线程执行完任务后通过循环再次从任务队列中取出任务进行执行,代码片段如下
while (task != null || (task = getTask()) != null) {})。
不用阻塞队列也是可以的,不过实现起来比较麻烦而已,有好用的为啥不用呢?
11、如何配置线程池
CPU密集型任务
尽量使用较小的线程池,一般为CPU核心数+1。 因为CPU密集型任务使得CPU使用率很高,若开过多的线程数,会造成CPU过度切换。
IO密集型任务
可以使用稍大的线程池,一般为2*CPU核心数。 IO密集型任务CPU使用率并不高,因此可以让CPU在等待IO的时候有其他线程去处理别的任务,充分利用CPU时间。
混合型任务
可以将任务分成IO密集型和CPU密集型任务,然后分别用不同的线程池去处理。 只要分完之后两个任务的执行时间相差不大,那么就会比串行执行来的高效。
因为如果划分之后两个任务执行时间有数据级的差距,那么拆分没有意义。
因为先执行完的任务就要等后执行完的任务,最终的时间仍然取决于后执行完的任务,而且还要加上任务拆分与合并的开销,得不偿失。
12、java中提供的线程池
Executors类提供了4种不同的线程池:newCachedThreadPool, newFixedThreadPool, newScheduledThreadPool, newSingleThreadExecutor

1、newCachedThreadPool:用来创建一个可以无限扩大的线程池,适用于负载较轻的场景,执行短期异步任务。(可以使得任务快速得到执行,因为任务时间执行短,可以很快结束,也不会造成cpu过度切换)
2、newFixedThreadPool:创建一个固定大小的线程池,因为采用无界的阻塞队列,所以实际线程数量永远不会变化,适用于负载较重的场景,对当前线程数量进行限制。(保证线程数可控,不会造成线程过多,导致系统负载更为严重)
3、newSingleThreadExecutor:创建一个单线程的线程池,适用于需要保证顺序执行各个任务。
4、newScheduledThreadPool:适用于执行延时或者周期性任务。
13、execute()和submit()方法
1、execute(),执行一个任务,没有返回值。
2、submit(),提交一个线程任务,有返回值。
submit(Callable<T> task)能获取到它的返回值,通过future.get()获取(阻塞直到任务执行完)。一般使用FutureTask+Callable配合使用(IntentService中有体现)。
submit(Runnable task, T result)能通过传入的载体result间接获得线程的返回值。
submit(Runnable task)则是没有返回值的,就算获取它的返回值也是null。
Future.get方法会使取结果的线程进入阻塞状态,知道线程执行完成之后,唤醒取结果的线程,然后返回结果。
https://www.cnblogs.com/dolphin0520/p/3949310.html
ThreadPoolExecutor的execute方法执行逻辑请见注释。下图为ThreadPoolExecutor的execute方法的执行示意图:
execute方法执行逻辑有这样几种情况:
- 如果当前运行的线程少于corePoolSize,则会创建新的线程来执行新的任务;
- 如果运行的线程个数等于或者大于corePoolSize,则会将提交的任务存放到阻塞队列workQueue中;
- 如果当前workQueue队列已满的话,则会创建新的线程来执行任务;
- 如果线程个数已经超过了maximumPoolSize,则会使用饱和策略RejectedExecutionHandler来进行处理。
需要注意的是,线程池的设计思想就是使用了核心线程池corePoolSize,阻塞队列workQueue和线程池maximumPoolSize,这样的缓存策略来处理任务,实际上这样的设计思想在需要框架中都会使用。
14、阻塞队列
15. 线程池的关闭
关闭线程池,可以通过shutdown
和shutdownNow
这两个方法。它们的原理都是遍历线程池中所有的线程,然后依次中断线程。shutdown
和shutdownNow
还是有不一样的地方:
shutdownNow
首先将线程池的状态设置为STOP,然后尝试停止所有的正在执行和未执行任务的线程,并返回等待执行任务的列表;shutdown
只是将线程池的状态设置为SHUTDOWN状态,然后中断所有没有正在执行任务的线程
可以看出shutdown方法会将正在执行的任务继续执行完,而shutdownNow会直接中断正在执行的任务。调用了这两个方法的任意一个,isShutdown
方法都会返回true,当所有的线程都关闭成功,才表示线程池成功关闭,这时调用isTerminated
方法才会返回true。
16. 如何合理配置线程池参数?
要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:
- 任务的性质:CPU密集型任务,IO密集型任务和混合型任务。
- 任务的优先级:高,中和低。
- 任务的执行时间:长,中和短。
- 任务的依赖性:是否依赖其他系统资源,如数据库连接。
任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能少的线程数量,如配置Ncpu+1个线程的线程池。IO密集型任务则由于需要等待IO操作,线程并不是一直在执行任务,则配置尽可能多的线程,如2xNcpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()
方法获得当前设备的CPU个数。
优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。
执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。
依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。
并且,阻塞队列最好是使用有界队列,如果采用无界队列的话,一旦任务积压在阻塞队列中的话就会占用过多的内存资源,甚至会使得系统崩溃。
17. ThreadPoolExecutor
构造函数中的core
和max
的区别,在实际应用中,core
和max
的比例为多少合适,分别应用于什么场景?
这个问题其实没有一个具体的数值答案,毕竟不同的场景下,处理的方式并不是一样的。但是却可以通过打印日志展示系统在整个运行情况下的线程使用情况,先设置max值能容纳所有进入系统中的并发数量,然后通过ThreadPoolExecutor
下的getPoolSize()
、getActiveCount()
、 getQueue().size()
、getTaskCount()
这几个方法,每几秒或者几分钟打印一次日志,来确定该线程池在不同情况下的并发数量。
如果并发数量能经常保持在一个稳定的数值,则该数值用于设置core
的值。而max值,则需要对系统有一个预估,预估最高并发情况下的并发数量,用于设置max值。
其实这个max
值也要判断这个线程池中的Worker
类型,是属于CPU
密集型还是IO
密集型,如果是CPU
密集型,则max
可以设置低一些,例如就是core
的值,避免线程频繁切换浪费资源;如果是IO
密集型的,则max
要设置高一些,例如core × 2
、core × 3
或者更多,因为IO
密集型的操作,大部分是等待,所以可以配置多一些线程用于线程等待,而且这些等待也不消耗CPU
时间,所以增加这些线程是利大于弊的。
18. ThreadPoolExecutor
中workQueue
参数都能配置哪些队列,分别应用于什么场景?
workQueue
的类型为BlockingQueue
,该类为接口,其有如下几个常用实现类
ArrayBlockingQueue
LinkedBlockingDeque
DelayQueue
PriorityBlockingQueue
SynchronousQueue
2.1. ArrayBlockingQueue
基于数组的阻塞队列实现,在ArrayBlockingQueue
内部,维护了一个定长数组,以便缓存队列中的数据对象,这是一个常用的阻塞队列,除了一个定长数组外,ArrayBlockingQueue
内部还保存着两个整形变量,分别标识着队列的头部和尾部在数组中的位置。ArrayBlockingQueue
在生产者放入数据和消费者获取数据,都是共用同一个锁对象,由此也意味着两者无法真正并行运行,这点尤其不同于LinkedBlockingQueue
;按照实现原理来分析,ArrayBlockingQueue
完全可以采用分离锁,从而实现生产者和消费者操作的完全并行运行。Doug Lea
之所以没这样去做,也许是因为ArrayBlockingQueue
的数据写入和获取操作已经足够轻巧,以至于引入独立的锁机制,除了给代码带来额外的复杂性外,其在性能上完全占不到任何便宜。 ArrayBlockingQueue
和LinkedBlockingQueue
间还有一个明显的不同之处在于,前者在插入或删除元素时不会产生或销毁任何额外的对象实例,而后者则会生成一个额外的Node
对象。这在长时间内需要高效并发地处理大批量数据的系统中,其对于GC
的影响还是存在一定的区别。而在创建ArrayBlockingQueue
时,我们还可以控制对象的内部锁是否采用公平锁,默认采用非公平锁。
2.2. LinkedBlockingQueue
基于链表的阻塞队列,同ArrayListBlockingQueue
类似,其内部也维持着一个数据缓冲队列(该队列由一个链表构成),当生产者往队列中放入一个数据时,队列会从生产者手中获取数据,并缓存在队列内部,而生产者立即返回;只有当队列缓冲区达到最大值缓存容量时(LinkedBlockingQueue
可以通过构造函数指定该值),才会阻塞生产者队列,直到消费者从队列中消费掉一份数据,生产者线程会被唤醒,反之对于消费者这端的处理也基于同样的原理。而LinkedBlockingQueue
之所以能够高效的处理并发数据,还因为其对于生产者端和消费者端分别采用了独立的锁来控制数据同步,这也意味着在高并发的情况下生产者和消费者可以并行地操作队列中的数据,以此来提高整个队列的并发性能。
作为开发者,我们需要注意的是,如果构造一个LinkedBlockingQueue
对象,而没有指定其容量大小,LinkedBlockingQueue
会默认一个类似无限大小的容量(Integer.MAX_VALUE
),这样的话,如果生产者的速度一旦大于消费者的速度,也许还没有等到队列满阻塞产生,系统内存就有可能已被消耗殆尽了。
ArrayBlockingQueue
和LinkedBlockingQueue
是两个最普通也是最常用的阻塞队列,一般情况下,在处理多线程间的生产者消费者问题,使用这两个类足以。
2.3. DelayQueue
DelayQueue
中的元素只有当其指定的延迟时间到了,才能够从队列中获取到该元素。DelayQueue
是一个没有大小限制的队列,因此往队列中插入数据的操作(生产者)永远不会被阻塞,而只有获取数据的操作(消费者)才会被阻塞。
使用场景:DelayQueue
使用场景较少,但都相当巧妙,常见的例子比如使用一个DelayQueue
来管理一个超时未响应的连接队列。
2.4. PriorityBlockingQueue
基于优先级的阻塞队列(优先级的判断通过构造函数传入的Compator
对象来决定),但需要注意的是PriorityBlockingQueue
并不会阻塞数据生产者,而只会在没有可消费的数据时,阻塞数据的消费者。因此使用的时候要特别注意,生产者生产数据的速度绝对不能快于消费者消费数据的速度,否则时间一长,会最终耗尽所有的可用堆内存空间。在实现PriorityBlockingQueue
时,内部控制线程同步的锁采用的是公平锁。
2.5. SynchronousQueue
一种无缓冲的等待队列,类似于无中介的直接交易,有点像原始社会中的生产者和消费者,生产者拿着产品去集市销售给产品的最终消费者,而消费者必须亲自去集市找到所要商品的直接生产者,如果一方没有找到合适的目标,那么对不起,大家都在集市等待。相对于有缓冲的BlockingQueue
来说,少了一个中间经销商的环节(缓冲区),如果有经销商,生产者直接把产品批发给经销商,而无需在意经销商最终会将这些产品卖给那些消费者,由于经销商可以库存一部分商品,因此相对于直接交易模式,总体来说采用中间经销商的模式会吞吐量高一些(可以批量买卖);但另一方面,又因为经销商的引入,使得产品从生产者到消费者中间增加了额外的交易环节,单个产品的及时响应性能可能会降低。
声明一个SynchronousQueue
有两种不同的方式,它们之间有着不太一样的行为。
公平模式和非公平模式的区别:
- 采用公平模式:
SynchronousQueue
会采用公平锁,并配合一个FIFO
队列来阻塞多余的生产者和消费者,从而体系整体的公平策略; - 非公平模式(
SynchronousQueue
默认):SynchronousQueue
采用非公平锁,同时配合一个LIFO
队列来管理多余的生产者和消费者,而后一种模式,如果生产者和消费者的处理速度有差距,则很容易出现饥渴的情况,即可能有某些生产者或者是消费者的数据永远都得不到处理。
专门一篇博文介绍不同的阻塞队列的实现原理《BlockingQueue源码解析》
19. ThreadPoolExecutor
中的handler
是干嘛用的,默认是什么,JDK
中提供的可选项有哪些,分别应用于什么场景?
handler
类型为RejectedExecutionHandler
,该类是一个接口,常用的实现类,其实都是ThreadPoolExecutor
中定义的内部类。如果ThreadPoolExecutor
的构造函数未指定handler
的话,默认是AbortPolicy
来作为处理类。该参数是用来定义当ThreadPoolExecutor
的队列已满,且新加Worker
之后,线程池的数量超过max
时,所执行的拒绝策略。ThreadPoolExecutor
中有提供如下几个拒绝策略
CallerRunsPolicy
AbortPolicy
DiscardPolicy
DiscardOldestPolicy
RejectedExecutionHandler
中方法描述
void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
该接口中,仅定义了一个方法,其中executor
是当前的线程池,而r
是当前要被拒绝的线程
3.1. CallerRunsPolicy
用于被拒绝任务的处理程序,它直接在execute
方法的调用线程中运行被拒绝的任务;如果执行程序已关闭,则会丢弃该任务。
3.2. AbortPolicy
抛出java.util.concurrent.RejectedExecutionException
异常
3.3. DiscardPolicy
当前任务直接丢弃
3.4. DiscardOldestPolicy
丢弃队列中最老的任务
20. ThreadPoolExecutor
中是如何记录线程池的状态的?
ThreadPoolExecutor
使用AtomicInteger
类型来记录线程池的状态和线程数。
其实AtomicInteger
是一个在做加减时,能够保证原子性的一个Integer
类。Integer
是占32
位的,在ThreadPoolExecutor
的定义中,前3
位是用来定义线程池状态,后29
位用来定义线程池中工作线程的数量的
PS:
ThreadPoolExecutor
中工作线程的最大值是(2^29)-1
,并不是(2^31)-1
PS:
Integer
中的第一位是符号位,但是在定义线程池状态中,该符号位也是用来记录状态的
Integer前三位的值 | ThreadPoolExecutor 的状态 | 状态描述 |
---|---|---|
111 |
RUNNING |
可以接受新的任务,也可以处理阻塞队列里的任务 |
000 |
SHUTDOWN |
不接受新的任务,但是可以处理阻塞队列里的任务 |
001 |
STOP |
不接受新的任务,不处理阻塞队列里的任务,中断正在处理的任务 |
010 |
TIDYING |
过渡状态,也就是说所有的任务都执行完了,当前线程池已经没有有效的线程,这个时候线程池的状态将会TIDYING ,并且将要调用terminated 方法 |
011 |
TERMINATED |
终止状态。terminated 方法调用完成以后的状态 |
21. ThreadPoolExecutor
中是如何保证线程安全的?
ThreadPoolExecutor
唯一进行加锁的操作,是用于记录线程池所有的Worker
及其数量。
1
|
/** 用于记录所有未执行的Worker */
|
在这2
个参数进行设置时,使用ReentrantLock
来进行加锁操作
22. ThreadPoolExecutor
中的Worker
类是如何实现AQS
的?
专门一篇博文来介绍AQS
的实现原理《AQS源码解析及设计思想》
额外需要补充的是,ThreadPoolExecutor
中的Worker
类是使用独占模式进行处理的