知识点(第八、九章)
1、CountDownLatch
基本的用法就是定义计数器N,并通过await方法阻塞线程,直到N变成0。和join最直观的使用区别就是不用手动的去挂起了,一两个线程可能并不会显得多方便,但是当线程数量较大时,区别还是很明显的。
2、CyclicBarrier
这个我也没用过,其字面意思就是可循环使用的屏障,让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会开门,所有被屏障拦截的线程才会继续运行。
默认的构造方法为CyclicBarrier(int parties),参数表示屏障拦截的线程数量,各自线程调用await方法告诉CyclicBarrier我已经到达屏障,然后当前线程被阻塞。同时提供更高级的构造函数CyclicBarrier(int parties, Runnable barrierAction),用于在线程到达屏障时优先执行barrierAction。跟CountDownLatch的最大区别即CyclicBarrier可以重用。
3、Semaphore
Semaphore翻译过来就是信号量,用于控制同时访问特定资源的线程数量,通过acquire和release获取信号量(有点类似lock的底层实现),其构造方法也是一个整形的数字,代表可用的许可证书,用于控制并发数量。
4、Exchanger
在看这本书前,这个我甚至都没听过,它是一个用于线程间协作的工具类,用于进行线程间的数据交换,通过exchange方法执行线程之间的数据交换。
5、线程池
面试必问点,使用线程池有以下三个好处:
- 降低资源消耗:通过重复利用已经创建的线程降低线程创建和销毁造成的消耗,这一点将在后面介绍,线程池是如何复用线程的;
- 提高响应速度:当任务到达时,可以不用等到线程创建就能立即执行(这里没明白执行具体是指什么);
- 提高线程的可管理性:实现线程的统一分配、调优和监控;
实现原理(书上原图):
可以看到有如下三个判断(参数前面已经介绍过):
1)核心线程(corePoolSize)是否已满,如果不是,则创建一个新的工作线程来执行任务,如果核心线程池中的线程都在执行任务,则进入下一个判断;
2)判断线程池的工作队列(runnableTaskQueue)是否已满,如果没有,则将任务入队列,如果满了,则进入下一个判断;
3)线程池判断线程池的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务,如果满了,则交给饱和策略(RejectedExecutionHandler)来处理这个任务;
因此ThreadPoolExecutor的executor方法包括以下四种情况,用图来表示如下:
这里猜想下前文中提高响应速度的意思,在执行execute方法时,通过线程池的调度,尽可能的避免全局锁,因为当线程数量大于corePoolSize时,任务加入等待队列,而这个步骤是不需要获取锁的,作者说的“执行”应该是指加入等待队列吧,通过避免锁而提高吞吐量,但实际上其线程中的业务并没有真正开始执行。这里贴一下核心的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))
//3
reject(command);
else if (workerCountOf(recheck) == 0)
//4
addWorker(null, false);
}
//5
else if (!addWorker(command, false))
//6
reject(command);
}
1对应第一种情况,2对应第二种情况,3的作用是检查线程池状态,如果此时线程池不可用或非运行,则将这个加入的任务删掉,并交由拒绝Handler处理,其实仔细思考一下,这里为什么不将所有的任务都交由拒绝Handler处理,因为线程池的状态是可恢复的,此时要做的仅仅应该是拒绝新的任务,4发现没有worker,则补充一个worker,5对应第三种情况,6则对应第四种情况,这就是线程池的实现原理,线程的复用其实是worker的复用,线程池创建线程时,会将线程封装成工作线程worker,这里再贴一下worker的run源码:
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
//1
while (task != null || (task = getTask()) != null) {
w.lock();
//2
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
//3
beforeExecute(wt, task);
Throwable thrown = null;
try {
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;
//4
w.completedTasks++;
w.unlock();
}
}
completedAbruptly = false; //5
} finally {
//6
processWorkerExit(w, completedAbruptly);
}
}
1进入循环,从getTask获取要执行的任务,直到返回null。这里达到了线程复用的效果,让线程处理多个任务。2保证了线程池在STOP状态下线程是中断的,非STOP状态下线程没有被中断。3调用了run方法,任务的真正执行即在这一步。执行前后提供了beforeExecute和afterExecute两个空方法,可以由子类实现。4中completedTasks统计worker执行了多少任务,最后累加进completedTaskCount变量,可以调用相应方法返回一些统计信息。5的变量completedAbruptly表示worker是否异常终止,执行到这里代表执行正常,后续的方法需要这个变量,6调用processWorkerExit结束。
这里涉及到一个关键的方法:getTask,这个方法负责从等待队列中获取任务,再贴一下源码:
private Runnable getTask() {
boolean timedOut = false; // Did the last poll() time out?
for (;;) {
int c = ctl.get();
int rs = runStateOf(c);
//1
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
decrementWorkerCount();
return null;
}
int wc = workerCountOf(c);
//2
// Are workers subject to culling?
boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
if ((wc > maximumPoolSize || (timed && timedOut))
&& (wc > 1 || workQueue.isEmpty())) {
if (compareAndDecrementWorkerCount(c))
return null;
continue;
}
//3
try {
Runnable r = timed ?
workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
workQueue.take();
if (r != null)
return r;
timedOut = true;
} catch (InterruptedException retry) {
timedOut = false;
}
}
}
可以看到,通过一个无限的循环不停的执行以下三个步骤:
1、检查线程池状态,rs>=stop代表线程池为不可用的状态,empty即等待队列为空,这个判断的意思就是如果线程池为shutdown状态且等待队列不为空,则继续执行完等待队列里的任务,如果为不可用状态(STOP、TIDYING、TERMINATED)则不再执行剩余的任务。
2、这个判断绕了我很久,大致的意思就是三个判断:(1)空闲是否超时,超时则终止;(2)在运行过程中worker数量是否大于maxpoolsize(setMaxPoolSize方法导致最大线程数量在运行时变更);(3)等待队列为空或worker数量大于等于1,即在等待队列非空时,至少保留一个worker。这三个判断只要有一个成立都不能处理等待队列中的任务。
3、2不成立则调到该步骤,从等待队列中取任务(take为阻塞式,前面已经介绍过)。
再来看看前面finally块中的processWorkerExit方法:
private void processWorkerExit(Worker w, boolean completedAbruptly) {
//1
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
decrementWorkerCount();
//2
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
completedTaskCount += w.completedTasks;
workers.remove(w);
} finally {
mainLock.unlock();
}
//3
tryTerminate();
int c = ctl.get();
//4
if (runStateLessThan(c, STOP)) {
if (!completedAbruptly) {
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
addWorker(null, false);
}
}
1判断worker是否被中断,如果是,则对workerCount的减一,如果没有的话,在getTask里面已经对wc做了减一操作;2计算已处理的worker数量,并且在集合workers中去除;3尝试终止线程池;4处理线程池还是RUNNING或SHUTDOWN状态时,如果worker是异常结束,那么会直接addWorker。如果allowCoreThreadTimeOut=true,并且等待队列有任务,至少保留一个worker;如果allowCoreThreadTimeOut=false,workerCount不少于corePoolSize。
线程池的关闭:可以通过shutdown或shutdownNow两种方法来关闭线程池,它们的原理都是遍历线程池中的工作线程,逐个调用interrupt方法,无法响应中断的任务可能永远无法终止,两种方法区别如下:
- shutdown:不能再提交任务,已经提交的任务可继续运行;
- shutdownNow:不能再提交任务,已经提交但未执行的任务不能运行,在运行的任务可继续运行,但会被中断,返回已经提交但未执行的任务。
线程池大小的配置:
实习的时候听讲座的人说过一句话印象特别深刻:大池小队列,小池无队列;即判断线程池中的任务特性,如果是IO密集型则用大池小队列,让它多去切换提高吞吐量,如果是CPU密集型,则使用小池无队列,尽量减少线程的切换导致执行效率降低。CPU密集型任务线程池大小一般配置为:N+1,IO密集型配置为2N,N为CPU核数,扩展下,Netty中的EventLoopGroup线程池的默认大小即为2N。
线程池的监控:
taskCount:线程池需要执行的任务数量;
completedTaskCount:线程池在运行过程中已完成的任务数量,小于或者等于taskCount;
largestPoolSize:线程池里曾经创建过的最大线程数量。通过这个数据可以知道线程池是否曾经满过;
getPoolSize:线程池的线程数量,如果线程池不销毁,线程也不会销毁,因此这个大小只会递增;
getActiveCount:获取活动的线程数;