Java并发编程 --- 遇见线程池
为何需要线程池?
首先,创建和销毁线程是比较耗时间的;
此次,创建线程也是需要内存空间的,如果无节制的创建,那么就会造成内存空间的不足;
最后,使用线程池也能提高响应时间,不用再向之前一定得先自己创建一个线程,然后再用它进行执行。
最重要的,线程池可以进行统一的分配,调优和监控。
线程池使用了享元模式,通过复用线程来避免频繁创建线程和销毁线程,大大提升了运行的效率。
走进线程池
When a job come to Pool
场景:现在有一个任务走进线程池。
情况:
①当线程池的当前线程数小于核心线程数时,会创建一个核心线程去执行该任务。
②当线程池的当前线程数等于核心线程数时
(1)当有核心线程空闲时,由空闲的核心线程来执行该任务
(2)当核心线程都在忙时,该任务会尝试去阻塞队列。
③当尝试进入阻塞队列时
(1)若阻塞队列未满,则进入阻塞队列
(2)若阻塞队列已满,那么则尝试去创建临时线程来执行该任务
④当尝试去创建临时线程时
(1)若当前线程数小于最大线程数,则允许创建线程
(2)若当前线程数已经等于最大线程数,则执行拒绝策略
⑤走到这里,该任务已经得执行该线程池的拒绝策略了。
配置
上述我们通过一个场景来描述了一个任务进入线程池根据不同的情况所出现了各种情况。里面的相关参数我来做一个粗略的定义:
核心线程数 → 线程池中固定活跃的线程数。
阻塞队列 → 当前线程数等于核心线程且核心线程都在繁忙时,会将任务存储在阻塞队列中。作用:存储任务。
最大线程数 → 线程池最大可以存在的线程数。
临时线程存活时间 → 临时线程最大空闲时间到时,就会被销毁。
拒绝策略 → 极端场景下线程池无法处理任务的方案。
拒绝策略
拒绝策略触发时机请参考When a job come to Pool的第五步操作。这里主要探讨的是线程池提供了哪些拒绝策略。
-
AbortPolicy:直接抛出异常
-
DiscardPolicy:直接抛弃这个任务
-
DiscardOldestPolicy:抛弃掉阻塞队列中最老的任务,为当前任务腾出位置
-
CallerRunsPolicy:直接由任务提交者去执行这个任务
如何合理设置核心线程数和最大线程数
主要讨论IO密集型任务和CPU密集型任务两个场景。
IO密集型
IO操作主要时间花在等待IO操作上,如网络请求,文件读取等。CPU处于空闲的时间较多。
一般设置如下:
核心线程数 = CPU核心数 * 2
最大线程数 = 核心线程数 * (1.5 ~ 2)
CPU密集型
CPU密集型主要是进行大量的计算,会持续占有CPU资源。
一般设置如下:
核心线程数 = CPU核心数 (这样可以保证每个CPU核心都有一个线程在执行任务,避免线程过多导致的上下文切换开销)
最大线程数 = CPU核心数 + (1/2) (主要是应对突发的任务)
阻塞队列(任务队列)
阻塞队列又有两种:有界队列和无界队列
-
有界队列:有固定大小的队列,比如设定了固定大小的 LinkedBlockingQueue,又或者大小为 0
-
无界队列:没有设置固定大小的队列,这些队列可以直接入队,直到溢出(超过 Integer.MAX_VALUE),所以相当于无界
java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:FIFO 队列
-
ArrayBlockingQueue:由数组结构组成的有界阻塞队列
-
LinkedBlockingQueue:由链表结构组成的无界(默认大小 Integer.MAX_VALUE)的阻塞队列
-
PriorityBlockingQueue:支持优先级排序的无界阻塞队列
-
DelayedWorkQueue:使用优先级队列实现的延迟无界阻塞队列
-
SynchronousQueue:不存储元素的阻塞队列,每一个生产线程会阻塞到有一个 put 的线程放入元素为止
-
LinkedTransferQueue:由链表结构组成的无界阻塞队列
-
LinkedBlockingDeque:由链表结构组成的双向阻塞队列
异常处理
现在我们来思索一个问题,那就是如果当线程池中的工作线程出现异常了,那线程池会怎么办呢?
首先,对于这个出现异常的线程,线程池会把它先移除掉,然后会创建一个新的工作线程来代替它。
execute() & submit()
执行execute()会直接抛出异常;而submit()会把异常设置在FutureTask中,需要通过get()方法才能获取。
优雅做法:在创建线程池时,设置相应的ThreadFactory来创建线程,并通过setUncaughtExceptionHandler()来指定捕获到异常的业务处理。
ThreadFactory threadFactory = new ThreadFactoryBuilder()
.setUncaughtExceptionHandler(((t, e) -> {
// t为线程 e为异常
// 可以设置相应的处理 如日志记录,报警等。
})).build();
原理
创建方式
首先推荐使用构造器的方式来创建线程池,因为这样你能够更好的了解你设置参数的各种信息。
当然,也可以用Excutors框架来进行线程池的创建。
submit()
该方法是将任务提交到线程池,由线程池线程进行执行,将任务提交和任务执行进行解耦操作。
public Future<?> submit(Runnable task) {
// 防止传入空任务
if (task == null) throw new NullPointerException();
// 把 Runnable 封装成未来任务对象,执行结果就是 null,也可以通过参数指定 FutureTask#get 返回数据
RunnableFuture<Void> ftask = newTaskFor(task, null);
// 执行方法
execute(ftask);
return ftask;
}
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
// 把 Callable 封装成未来任务对象
RunnableFuture<T> ftask = newTaskFor(task);
// 执行方法
execute(ftask);
// 返回未来任务对象,用来获取返回值
return ftask;
}
// 线程池的线程执行逻辑
public void execute(Runnable command) {
// 预防null
if (command == null)
throw new NullPointerException();
// ctl为原子Integer,记录的是当前线程数
int c = ctl.get();
// 当当前线程数小于核心线程数时,直接新建一个核心线程到线程池
if (workerCountOf(c) < corePoolSize) {
// 第二个参数true表示创建的线程类型为核心线程
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);
}
// CAPACITY为线程池已经设置好的一个最大允许创建的线程数,为536870911
// 可以通过该方法获取线程
private static int workerCountOf(int c) { return c & CAPACITY; }
addWorker()
作用:创建工作线程
private boolean addWorker(Runnable firstTask, boolean core) {
retry: //循环回到的游标点
for (;;) {
// 获取当前线程数
int c = ctl.get();
// 获取线程池运行状态
int rs = runStateOf(c);
// Check if queue empty only if necessary.
if (rs >= SHUTDOWN &&
! (rs == SHUTDOWN &&
firstTask == null &&
! workQueue.isEmpty()))
return false;
for (;;) {
int wc = workerCountOf(c);
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
if (compareAndIncrementWorkerCount(c))
break retry;
c = ctl.get(); // Re-read ctl
if (runStateOf(c) != rs)
continue retry; // 游标
}
}
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;
// 添加线程和逻辑
try {
w = new Worker(firstTask);
final Thread t = w.thread;
if (t != null) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// Recheck while holding lock.
// Back out on ThreadFactory failure or if
// shut down before lock acquired.
int rs = runStateOf(ctl.get());
if (rs < SHUTDOWN ||
(rs == SHUTDOWN && firstTask == null)) {
if (t.isAlive()) // precheck that t is startable
throw new IllegalThreadStateException();
workers.add(w);
int s = workers.size();
if (s > largestPoolSize)
largestPoolSize = s;
workerAdded = true;
}
} finally {
mainLock.unlock();
}
if (workerAdded) {
t.start();
workerStarted = true;
}
}
} finally {
if (! workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
关闭线程池
shutdown()
提供了优雅的线程池关闭,当执行线程池关闭时,会把所有任务队列中所有的任务全部执行完再进行关闭。
public void shutdown() {
// 使用ReentrantLock进行同步
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 双重判定
checkShutdownAccess();
// 设置线程池状态为SHUTDOWN
advanceRunState(SHUTDOWN);
// 给worker线程发中断标识
interruptIdleWorkers();
onShutdown(); // hook for ScheduledThreadPoolExecutor
} finally {
mainLock.unlock();
}
tryTerminate();
}
// 把当前线程数设置为0
private void advanceRunState(int targetState) {
for (;;) {
int c = ctl.get();
// cas操作设置线程池状态为targetState
if (runStateAtLeast(c, targetState) ||
ctl.compareAndSet(c, ctlOf(targetState, workerCountOf(c))))
break;
}
}
private void interruptIdleWorkers() {
interruptIdleWorkers(false);
}
//去打断
private void interruptIdleWorkers(boolean onlyOne) {
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
for (Worker w : workers) {
Thread t = w.thread;
// 中断那些未被中断且空闲的线程
// 每个线程自身都有一把锁,在执行任务时会lock,执行完会unlock;所以当这里获取到锁时,代表Worker的任务执行完了
if (!t.isInterrupted() && w.tryLock()) {
try {
// 中断工作线程,其实工作线程仍会继续走一遍循环,主要判断线程池状态和阻塞队列任务数来决定是否销毁
t.interrupt();
} catch (SecurityException ignore) {
} finally {
w.unlock();
}
}
// 返回true时代表中断所有的线程,可以循环了
if (onlyOne)
break;
}
} finally {
mainLock.unlock();
}
}
shutdownNow()
暴力直接关停线程池,不管任务执行完成与否。
线程运行 --- run();
public void run() {
runWorker(this);
}
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
// 把任务当地拎出来的作用:help GC 防止任务与线程强绑定 造成内存溢出
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
//获取任务
while (task != null || (task = getTask()) != null) {
// 执行任务时先上锁
w.lock();
// If pool is stopping, ensure thread is interrupted;
// if not, ensure thread is not interrupted. This
// requires a recheck in second case to deal with
// shutdownNow race while clearing interrupt
if ((runStateAtLeast(ctl.get(), STOP) ||
(Thread.interrupted() &&
runStateAtLeast(ctl.get(), STOP))) &&
!wt.isInterrupted())
wt.interrupt();
try {
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;
w.completedTasks++;
// 执行完解锁
w.unlock();
}
}
completedAbruptly = false;
} finally {
processWorkerExit(w, completedAbruptly);
}
}
为什么Worker内置了一把锁(AQS提供)?
利用了锁作为状态量,当Worker线程空闲时,锁可以获取,当Worker线程在工作时,锁不可获取。保证了Worker线程在执行方法时不被突然来的任务所影响。
Tips:也为后续shutdown关闭线程池时,在关闭前保证线程的任务执行完了,再进行关闭处理。

浙公网安备 33010602011771号