我们知道一旦有任务来了,线程池就会分配线程去处理任务,或者把任务放到缓存队列中去。那么线程池是如何高效处理任务的呢?多个任务进入了缓存队列,多线程取任务,这又是如何处理并发问题的呢?线程都有有状态的,那么线程池有状态么?我们一点一点来看看哈。
首先,说下基础的线程池状态属性。
线程池状态:线程池一共有五种状态,并且对这五种状态进行了一定的封装,他们分别是:
- RUNNING:能接受新提交的任务,并且也能处理阻塞队列中的任务,就是在运行。
- SHUTDOWN:关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务。在线程池处于 RUNNING 状态时,调用 shutdown()方法会使线程池进入到该状态。我在上一篇的代码中有用到哦。
- STOP:不能接受新任务,也不处理队列中的任务,会中断正在处理任务的线程。在线程池处于 RUNNING 或 SHUTDOWN 状态时,调用 shutdownNow() 方法会使线程池进入到该状态,最好不要直接调用这个,有些任务没处理完,会抛出异常。
- TIDYING:如果所有的任务都已终止了,workerCount (有效线程数) 为0,线程池进入该状态后会调用 terminated() 方法进入TERMINATED 状态。
- TERMINATED:在terminated() 方法执行完后进入该状态,默认terminated()方法中什么也没有做。
我们进入代码中看一看:
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// 计数位 29
private static final int COUNT_BITS = Integer.SIZE - 3;
// 左移29位然后-1,0001 1111 1111 1111 1111 1111 1111 1111
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// 五种线程状态
//-1左移29位,也就是-536870912
//二进制展示是1010 0000 0000 0000 0000 0000 0000 0000
private static final int RUNNING = -1 << COUNT_BITS;
//二进制展示是0000 0000 0000 0000 0000 0000 0000 0000
private static final int SHUTDOWN = 0 << COUNT_BITS;
//二进制展示是0010 0000 0000 0000 0000 0000 0000 0000
private static final int STOP = 1 << COUNT_BITS;
//二进制展示是0100 0000 0000 0000 0000 0000 0000 0000
private static final int TIDYING = 2 << COUNT_BITS;
//二进制展示是0110 0000 0000 0000 0000 0000 0000 0000
private static final int TERMINATED = 3 << COUNT_BITS;
// 获取线程状态
//~代表非,那么~CAPACITY也就是1110 0000 0000 0000 0000 0000 0000 0000
// c 假设是 1010 0000 0000 0000 0000 0000 0000 0000
// 那么返回的就是 1010 0000 0000 0000 0000 0000 0000 0000
private static int runStateOf(int c) { return c & ~CAPACITY; }
线程池通过ctl来控制线程池的运行状态和线程池中有效线程数量,使用的是线程安全的AtomicInteger ,ctl共有32位,前面三位是状态位,最后面的29位才是线程数量计数,也就是2^29-1=536870912,5亿多,不少了,超出这么多线程,那估计就hold不住了,应该用不了那么多,CPU就GG了,O(∩_∩)O哈哈~。
我们可以看到线程的状态是-1,0,1,2,3左移29位的数字代表的,注意:绝不是-1,0,1,2,3啊,这么一说,就知道你是没看过源码,扫了一眼就哗啦哗啦开始扯了。。贼尴尬
接着,在获取状态的时候, 通过c & ~CAPACITY直接获取,数量的话也可以通过ctl直接获取,这么一看设计是不是厉害的样纸?嘿嘿
下面来看下运行吧。
public void execute(Runnable command) {
// 运行的对象都没有,只能抛出异常了
if (command == null)
throw new NullPointerException();
// 获取ctl的value,线程状态和workerCount都在里面,没它不行啊
int c = ctl.get();
//上面说了获取线程状态。workerCountOf(c) 这个就是获取线程数量了
//小于核心线程数,那就分配线程直接运行呗
if (workerCountOf(c) < corePoolSize) {
// 添加到任务
if (addWorker(command, true))
return;
//添加任务失败,这个时候ctl也许会改变,需要重新获取ctl
c = ctl.get();
}
//如果当前线程是运行状态并且添加任务到列表成功
if (isRunning(c) && workQueue.offer(command)) {
// 获取ctl
int recheck = ctl.get();
//再次判断线程池的运行状态,如果不是运行状态,把刚刚加到workQueue中的command移除
if (! isRunning(recheck) && remove(command))
// 拒绝策略
reject(command);
//有效线程数为0,那就要执行addWork方法了
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
//再次调用addWorker方法,但第二个参数传入为false,
//将线程池的有限线程数量的上限设置为maximumPoolSize;
else if (!addWorker(command, false))
reject(command);
}
上面是线程池运行的执行任务的主要方法,简单的注释已经写了。这段代码主要就是分为三步。
- 如果运行的线程数少于corePoolSize,直接使用线程执行任务
- 如果任务排队成功,那么仍要检查是不是应该加入线程,因为可能刚刚检查的线程已经执行结束了。
- 如果无法进入缓存队列,那就是缓存队列满了,只能重新启动线程了,在最大线程允许的情况下,如果启动线程也失败,那只能拒绝任务了。
我们接着看这个核心的方法addWorker~
private boolean addWorker(Runnable firstTask, boolean core) {
//retry 这个是标志位,就是在循环中continue,break的时候可以进行多层跳出
//个人觉得这段代码使用while处理可能更亲切一点,哈哈
retry:
// 无限循环,不多说
for (;;) {
//获取ctl以及状态
int c = ctl.get();
int rs = runStateOf(c);
// 线程池的状态是int类型,从运行到关闭是逐渐增大的,所以直接使用大于小于对比
// rs >= SHUTDOWN,表示此时不再接收新任务
// rs == SHUTDOWN,这时表示关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务
// 既然不接受新任务,那 firstTask当然要为空
// 如果workQueue不为空,就是处理workQueue里面的任务,没问题,但是加了一个否定,那还处理啥?
if (rs >= SHUTDOWN && !(rs == SHUTDOWN && firstTask == null && !workQueue.isEmpty()))
return false;
for (;;) {
// 工作的线程的计数,就是第几个工作的线程
int wc = workerCountOf(c);
// 超出最大限制(默认2亿多,这个是可以初始化设置的,不会真是这么多)还搞啥?GG,
// 根据core判断,是取corePoolSize还是maximumPoolSize,超过了线程数目,当然有也GG
if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// 尝试增加workerCount,成功的话就跳出循环
if (compareAndIncrementWorkerCount(c))
//跳出retry下面的这个循环
break retry;
// 重新获取ctl
c = ctl.get();
// 获取ctl的状态,如果当前状态已经改变,返回第一个循环继续执行
if (runStateOf(c) != rs)
continue retry;
// else CAS failed due to workerCount change; retry inner loop
}
}
//两个标识位
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
try {
// 新建一个worker
w = new Worker(firstTask);
// 获取worker的线程
//这个ThreadFactory如果没设置的话,会默认使用默认java.util.concurrent.Executors.DefaultThreadFactory
final Thread t = w.thread;
//线程肯定不能为空,为空的话那就增加任务失败,执行不了呗
if (t != null) {
//获取新的ReentrantLock锁(排它锁)
final ReentrantLock mainLock = this.mainLock;
//加锁,不多说哈,不熟悉的可以看下之前锁的讲解
// 因为这是一个线程池,肯定存在多线程竞争的情况,比如同时去取一个任务,同时去执行等等
mainLock.lock();
try {
//获取ctl状态
int rs = runStateOf(ctl.get());
// rs < SHUTDOWN表示是RUNNING
// 如果rs是RUNNING状态或者rs是SHUTDOWN状态并且firstTask为null,继续下面的逻辑
// SHUTDOWN状态下,不会添加新的任务,只会执行缓存列表中的任务
if (rs < SHUTDOWN ||(rs == SHUTDOWN && firstTask == null)) {
//预检查t是否可启动
if (t.isAlive())
throw new IllegalThreadStateException();
// private final HashSet<Worker> workers = new HashSet<Worker>();
// workers是一个HashSet
workers.add(w);
// largestPoolSize 线程池的最大线程数量
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
// 任务增加成功,也分配到线程了
if (workerAdded) {
// 那就启动线程,运行任务呗
t.start();
workerStarted = true;
}
}
} finally {
// 启动失败的话,就把已经加入到workers里面的任务移除掉
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
addWorker只有两个入参,firstTask和core,也就是addworker(增加任务)只关心任务和线程池的大小,而线程池的大小是根据core的传参确定的。
进入这个方法,首先是对状态进行过滤,线程池有5中状态,除了RUNNING和SHOWDOWN,其他的状态都不会再执行任务了,而SHUTDOWN状态,也只是执行缓存队列中的任务,所以addWorker首先对这个做了判断处理。
接着,就是对线程数量的判断,线程的数量肯定不能超过方法允许的最大线程数,超过了就没有线程可以处理任务了,也就是会直接GG,返回false。
当然如果执行的中途状态改变,比如,调用shutdown或者调用shutdownNow方法,就要重新走一遍for循环。
好了上面检查都没问题,就要开始加任务了。
新建任务,获取线程,然后就是加锁,一个排它锁。因为线程池是多线程,存在并发现象,而且workers这个HashSet并不是线程安全的,所以要加锁。
加锁之后,worker成功加入到workers中,就代表加入任务成功,之后启动便可。
在启动的时候,是直接t.start()。大家可以看下这个t是怎么来的。t是从worker中取的,worker的构造方法
Worker(Runnable firstTask) {
setState(-1); // inhibit interrupts until runWorker
this.firstTask = firstTask;
this.thread = getThreadFactory().newThread(this);
}
this.thread = getThreadFactory().newThread(this); 这个线程中的runnable,其实就是传进来的firstTask!所以t.strat(),也就是运行firstTask。
大致的流程就是这样哈。
差不多了,别的我们下一次再看哈~
No sacrifice,no victory~