1、ThreadPoolExecutor的工作状态
1.1 工作状态概述
ThreadPoolExecutor
提供了对于线程生命周期的控制,规定线程池有5种状态,阅读源码注释,发现使用ctl的高3位表示。而它的低29位则表示线程池中的工作线程数。所谓的工作线程数,就是已经被允许start并且不允许被停止的线程。这里先别想这些左移表示是怎么实现的,主要先记住这5种状态,下面会分析具体的运算。
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
private static final int COUNT_BITS = Integer.SIZE - 3;
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// 工作状态存储在高3位中
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(运行,-1):能够接收新任务,也可以处理阻塞队列中的任务
- SHUTDOWN(待关闭,0):不可以接受新任务,继续处理阻塞队列中的任务。
- STOP(停止,1):不接收新任务,不处理阻塞队列中的任务,并且会中断正在处理的任务。
- TIDYING(整理,2):所有的任务已终止,ctl记录的工作线程数为0,线程池会变为状态。当线程池变为
TIDYING
状态时,执行钩子方法terminated()
。对于它的实现,在ThreadPoolExecutor
中什么也没做。使用了模板方法模式,和AQS的tryAquire()一样,需要子类实现。如果想在进入TIDYING
后做点什么,可以对其进行重载。 - TERMINATED(终止,3):完全终止,且完成了所有资源的释放。
1.2 分析一下线程池状态的标记原理
AtomicInteger ctl =new AtomicInteger(ctlOf(RUNNING,0))
很明显,ctl被初始化为运行状态并且工作线程数设置为0。
- COUNT_BIT = 29;
- CAPACITY =(1<< COUNT_BITS)-1,表示工作线程的最大值,2^29-1;
- RUNNING =-1<< COUNT_BITS;
RUNNING =1110 0000 0000 0000 0000 0000 0000 0000,前3位表示-1的补码。 - STOP =1<< COUNT_BITS
STOP = 0010 0000 0000 0000 0000 0000 0000 0000
可以理解成不看后29位,只把前3位转换成补码。正数的原码,反码,补码相同;负数的补码=反码+1。
1.3 工作状态的具体判定
private static int runStateOf(int c) { return c & ~CAPACITY; }
private static int workerCountOf(int c) { return c & CAPACITY; }
private static int ctlOf(int rs, int wc) { return rs | wc; }
- runStateOf(int c):返回的是线程池工作状态,CAPACITY取反再与运算,结果就是高3位不变,将低29位变为0。
- workerCountOf(int c):返回工作线程数,由于CAPACITY高3位都是0,所以结果是高3位变0,低29位不变。
- ctlOf(int rs,int wc):初始化线程池或改变线程池状态时执行,将rs与wc进行或运算,结果刚好是他们拼接后的ctl的值。
1.4 工作状态的转换
下面是源码中的描述:
RUNNING -> SHUTDOWN
:在调用shutDown()时或隐式调用在finalize()中。
(RUNNING or SHUTDOWN) -> STOP
:调用shutdownNow()时。
SHUTDOWN -> TIDYING
:当阻塞和队列和工作线程数都为0时。
STOP -> TIDYING
:当工作线程数为0。
TIDYING -> TERMINATED
:当terminated()钩子方法完成时。
2、ThreadPoolExecutor的重要成员变量
2.1 构造方法
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
2.2 线程工厂(ThreadFactory)
private volatile ThreadFactory threadFactory;
我们可以自定义线程工厂
- 可以设置创建线程时间,统一线程前缀名,优先级,是否为守护线程等信息
- 没有则用默认工厂创建–
Executors.defaultThreadFactory()
2.3 拒绝策略(RejectedExecutionHandler)
ThreadPoolExecutor
的4个内部类就是拒绝策略的实现。
AbortPolicy
、CallerRunsPolicy
、DiscardPolicy
、DiscardOldestPolicy
。
2.4 其他重要成员变量
//线程池一些信息更新时使用,比如largestPoolSize,complectedTaskNum,ctl的状态和线程数更新时。
private final ReentrantLock mainLock = new ReentrantLock();
//工作线程集合,Worker里实现了Runnable,封装了thread,用于执行任务
private final HashSet<Worker> workers = new HashSet<Worker>();
//客户端调用awaitTerminate()时会阻塞,
//当处于terminate状态后,使用condition.signalAll()通知
private final Condition termination = mainLock.newCondition();
//记录线程池运行过程中出现过的最大线程数
private int largestPoolSize;
//记录完成的任务数量
private long completedTaskCount;
//当线程数小于corePoolSize时,是否允许它也遵循keepAliveTime时间限制
private volatile boolean allowCoreThreadTimeOut;
3、盘点一下常见的线程池
常见的线程池有:FixedThreadPool
,CachedThreadPool
,SingleThreadPool
,ScheduledThreadPool
,ForkJoinThreadPool
。
3.1、FixedThreadPool
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
阻塞队列是LinkedBlockingQueue
,默认的容量上限是Integer.MAX_VALUE
(其实工作线程数最高也就2^29-1)。将返回一个核心线程数和最大线程数相等的线程池。
使用场景:大多数情况下使用的线程池首选推荐FixedThreadPool
。OS和硬件是有线程支持上限的,不能随意的无限提供线程池。
常见的线程池容量:pc-200;服务器-1000~10000
并发处理能力≈线程数*(10~18)
3.2、SingleThreadPool
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
阻塞队列是LinkedBlockingQueue
,最大线程数与核心线程数都是1,使用单个worker线程的Executor。
使用场景:保证任务顺序时使用。如游戏大厅中的公共频道聊天,秒杀。
3.3、CachedThreadPool
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
阻塞队列是SynchronousQueue
;核心线程数为0,最大线程数为Integer.MAX_VALUE
;对于新的任务,如果线程池中没有空闲线程,则创建一个新的线程处理任务。
容量管理策略:如果线程池中的线程数量不满足任务执行,每次有新任务无法即时处理的时候,都会创建新的线程。默认线程空闲时间60秒,自动销毁。
使用场景:内部应用或测试应用。内部应用。有条件的内部数据瞬间处理时应用,如电信平台夜间执行数据整理(有把握在短时间内处理完所有工作,且对硬件和软件有足够的信心)。测试应用,在测试的时候,尝试得到硬件或软件最高的负载量,用于提供FixedThreadPool
容量的指导。
3.4、ScheduledThreadPool
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
用于定时完成任务。可以搭配Timer.scheduleAtFixedRate()
使用。
scheduleAtFixedRate(runnable, start_limit, limit, timeunit)
- runnable:要执行的任务
- start_limit:第一次任务执行的间隔
- limit:多次任务执行的间隔
- timeunit:多次任务执行间隔的时间单位
使用场景:计划任务时选用(DelayedQueue)如电信行业的数据整理,每分钟整理,每小时整理,每天整理等。
3.5、ForkJoinThreadPool
Executors.newWorkStealingPool()
采用了工作窃取算法(work-stealing):所有池中线程会尝试找到并执行已被提交到池中的或由其他线程创建的任务。这样很少有线程会处于空闲状态,非常高效。这使得能够有效地处理以下情景:大多数由任务产生大量子任务的情况;从外部客户端大量提交小任务到池中的情况。在实现上,每个工作线程都有自己的任务队列,每次都先找其他工作线程的底部(base)任务,完成后再从顶部(top)开始完成自己的任务。
ForkJoinTask类型提供了两个抽象子类型,RecursiveTask(有返回结果的分支合并任务),RecursiveAction(无返回结果的分支合并任务,可当成Callable与Runnable理解)。
ForkJoinThreadPool没有所谓的容量,默认都是一个线程,根据任务自动的分支新的子线程。当子线程任务结束后,自动合并。所谓自动是根据fork和join方法实现的。
使用场景:主要是做科学计算或天文计算,数据分析。
4、总结
用到的设计模式有工厂模式,模板方法模式,接口适配器模式。读源码看英文注释真的很重要,其次最重要的就是工作状态与拒绝策略。对于工作线程的控制,还有具体的调度,改变量加lock等一系列源码就放在下次吧。
最后思考一下,如何设计一个线程池?
下一章:第十五章 线程池——源码(下)