ThreadPoolExecutor
线程池详解项目最近的迭代中使用到了ThreadPoolExecutor
线程池,之前都只是知道怎么用,没有了解过线程池的底层原理,项目刚上线,有时间整理一下线程池的用法,学习一下线程池的底层实现与工作原理。
ThreadPoolExecutor
工作原理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;
}
worker
/**
* @author itender
* @date 2023/8/7 14:41
* @desc
*/
public class Worker implements Runnable {
private String command;
public Worker(String s) {
this.command = s;
}
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + command + " startTie = " + DateUtil.now());
processCommand();
System.out.println(Thread.currentThread().getName() + command + " endTime = " + DateUtil.now());
}
private void processCommand() {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + command +" 处理任务逻辑。。。。。。。。");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
e.printStackTrace();
}
}
@Override
public String toString() {
return this.command;
}
}
/**
* @author itender
* @date 2023/8/7 14:37
* @desc
*/
public class ThreadPoolExecutorDemo {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 100;
private static final Long KEEP_ALIVE_TIME = 1L;
public static void main(String[] args) {
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
// 核心线程数 5
CORE_POOL_SIZE,
// 最大线程数 10
MAX_POOL_SIZE,
// 超过核心线程数,线程最大存活时间
KEEP_ALIVE_TIME,
// 时间单位
TimeUnit.MINUTES,
// 工作队列最大值
new ArrayBlockingQueue<>(QUEUE_CAPACITY),
// 线程工厂,创建线程的时候使用
r -> {
Thread thread = new Thread(r);
thread.setName("pool-");
return thread;
},
new ThreadPoolExecutor.CallerRunsPolicy()
);
for (int i = 0; i < 10; i++) {
// 创建任务
Worker myRunnable = new Worker("" + i);
// 执行任务
threadPoolExecutor.execute(myRunnable);
}
// 种植线程池,不接受新任务,但是有工作线程处理队列中的任务
threadPoolExecutor.shutdown();
while (!threadPoolExecutor.isTerminated()) {
}
System.out.println("Finished All Threads!");
}
}
corePoolSize
时,如果没有新任务提交,核心线程外的线程不会立即销毁,而是等待,直到等待的时间超过了keepAliveTime
才会被销毁回收。keepAliveTime
参数的时间单位。如果当前同时运行的线程数量达到最大线程数量并且队列也已经被放满了任务时,ThreadPoolTaskExecutor
定义一些策略:
RejectExecutionException
来拒绝新任务的处理。ctl
// ctl本质是 Integer 型变量,进行了原子性的封装
// ctl表示两种状态:
// 高3位:线程池当前的状态
// 低29位:线程池当前工作线程的数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
// COUNT_BITS 的值为 29(整型Integer.SIZE = 32 位);
private static final int COUNT_BITS = Integer.SIZE - 3;
// CAPACITY = (1 << 29) - 1; 1左移29位,减去1;即1*2^29-1;
// 0001 1111 1111 1111 1111 1111 1111 1111
// 低29位用来表示线程池的最大线程容量
private static final int CAPACITY = (1 << COUNT_BITS) - 1;
// 高3位用来表示线程池5种状态
// 111 运行状态
private static final int RUNNING = -1 << COUNT_BITS;
// 000 shutdown状态
private static final int SHUTDOWN = 0 << COUNT_BITS;
// 001 停止状态
private static final int STOP = 1 << COUNT_BITS;
// 010 过渡状态
private static final int TIDYING = 2 << COUNT_BITS;
// 011 中介状态
private static final int TERMINATED = 3 << COUNT_BITS;
// 根据ctl的值,计算当前线程池的状态
// 计算方式:c 与 非capacity
private static int runStateOf(int c) { return c & ~CAPACITY; }
// 根据ctl的值,计算线程池当前运行的线程的容量
private static int workerCountOf(int c) { return c & CAPACITY; }
// 通过运行状态和工作线程数计算ctl的值,或运算
private static int ctlOf(int rs, int wc) { return rs | wc; }
private static boolean runStateLessThan(int c, int s) {
return c < s;
private static boolean runStateAtLeast(int c, int s) {
return c >= s;
}
private static boolean isRunning(int c) {
return c < SHUTDOWN;
}
/**
* Attempts to CAS-increment the workerCount field of ctl.
*/
private boolean compareAndIncrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect + 1);
}
/**
* Attempts to CAS-decrement the workerCount field of ctl.
*/
private boolean compareAndDecrementWorkerCount(int expect) {
return ctl.compareAndSet(expect, expect - 1);
}
/**
* Decrements the workerCount field of ctl. This is called only on
* abrupt termination of a thread (see processWorkerExit). Other
* decrements are performed within getTask.
*/
private void decrementWorkerCount() {
do {} while (! compareAndDecrementWorkerCount(ctl.get()));
}
shutdown()
方法会使线程池进入到该状态。(finalize()
方法在执行过程中也会调用 shutdown()
方法进入该状态)。shutdownNow()
方法会使线程池进入到该状态。workerCount
(有效线程数) 为0,线程池进入该状态后会调用 terminated()
方法进入 TERMINATED 状态。terminated()
方法执行完后进入该状态,默认 terminated()
方法中什么也没有做。execute
方法public void execute(Runnable command) {
// 判断任务是否为空,如果任务为空,抛出空指针异常
if (command == null)
throw new NullPointerException();
// 获取ctl属性
int c = ctl.get();
// 判断当前工作线程数量是否小于核心线程的数量
if (workerCountOf(c) < corePoolSize) {
// 工作线程数小于核心线程数,创建一个核心线程执行command任务
if (addWorker(command, true))
// 创建核心线程成功,直接返回
return;
// 并发情况下添加核心线程失败,需要重新获取ctl属性
c = ctl.get();
}
// 创建核心线程失败,当前工作线程数量大于或等于核心线程数量corePoolSize
// 判断线程池的状态是否为running,如果是添加任务到工作队列中(放入任务失败返回false)
if (isRunning(c) && workQueue.offer(command)) {
// 任务添加到队列成功,再次获取ctl属性
int recheck = ctl.get();
// 二次检查,判断线程池的状态是否为running,如果不是队列中移除刚刚添加的任务
if (!isRunning(recheck) && remove(command))
// 执行拒绝策略
reject(command);
// 1.任务添加到队列
// 2.线程池可能是running状态
// 3.传入的任务可能从任务队列中移除失败(移除失败的唯一可能就是任务已经被执行了)
// 判断工作线程数量是否为0
else if (workerCountOf(recheck) == 0)
// 工作线程数量为0
// 工作队列中有任务在排队,添加一个空任务,创建非核心线程执行队列中等待的任务
addWorker(null, false);
}
// 创建核心线程失败,
// 线程池状态不是running状态
// 线程池可能是running状态,但是任务队列已经满了
// 添加任务到工作队列失败,创建非核心线程执行任务
else if (!addWorker(command, false))
// 创建非核心线程失败,执行拒绝策略
reject(command);
}
第一点核心:通过execute方法源码可以看出线程池具体的执行流程,以及一些避免并发情况的判断。
第二点核心:线程池为什么会添加空任务非核心线程到线程池。
这里是一个疑惑点:为什么需要二次检查线程池的运行状态,当前工作线程数量为0,尝试创建一个非核心线程并且传入的任务对象为null?这个可以看API注释:
如果一个任务成功加入任务队列,我们依然需要二次检查是否需要添加一个工作线程(因为所有存活的工作线程有可能在最后一次检查之后已经终结)或者执行当前方法的时候线程池是否已经shutdown了。所以我们需要二次检查线程池的状态,必须时把任务从任务队列中移除或者在没有可用的工作线程的前提下新建一个工作线程。
addWorker
方法private boolean addWorker(Runnable firstTask, boolean core) {
// for循环标识
// 对线程池当前状态和当前工作线程数量的判断
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);
// 1. 如果传入的core为true,表示将要创建核心线程,通过wc和corePoolSize判断,如果wc >= corePoolSize,则返回false表示创建核心线程失败
// 2. 如果传入的core为false,表示将要创非建核心线程,通过wc和maximumPoolSize判断,如果wc >= maximumPoolSize,则返回false表示创建非核心线程失败
// core参数为false说明工作队列已经满了,线程池大小变为maximumPoolSize最大线程数
if (wc >= CAPACITY ||
wc >= (core ? corePoolSize : maximumPoolSize))
return false;
// CAS更新工作线程数wc,原子操作将workCount的数量加1,更新成功则直接跳出最外层循环
if (compareAndIncrementWorkerCount(c))
break retry;
// CAS更新工作线程数失败,判断线程池的状态是否从running编程shutdown,如果线程池的状态改变了在执行上面的操作
c = ctl.get(); // Re-read ctl
// 如果线程池状态已经变成shutdown,跳过最外层本次循环,执行下一次循环
if (runStateOf(c) != rs)
continue retry;
// 如果线程池状态依然是RUNNING,CAS更新工作线程数wc失败说明有可能是并发更新导致的失败,则在内层循环重试即可
// else CAS failed due to workerCount change; retry inner loop
}
}
// 工作线程是否启动成功
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 {
// 获取线程池状态
int rs = runStateOf(ctl.get());
//rs < SHUTDOWN 如果线程池状态依然为RUNNING,并且线程的状态是存活的话,就会将工作线程添加到工作线程集合中
//(rs=SHUTDOWN && firstTask == null)如果线程池状态小于STOP,也就是RUNNING或者SHUTDOWN状态下,同时传入的任务实例firstTask为null,则需要添加到工作线程集合和启动新的Worker
// 对于2,换言之,如果线程池处于SHUTDOWN状态下,同时传入的任务实例firstTask不为null,则不会添加到工作线程集合和启动新的Worker
// 这一步其实有可能创建了新的Worker实例但是并不启动(临时对象,没有任何强引用),这种Worker有可能成功下一轮GC被收集的垃圾对象
// firstTask == null证明只新建线程而不执行任务
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();
}
// 如果成功添加工作线程,则调用Worker内部的线程实例t的Thread#start()方法启动真实的线程实例
if (workerAdded) {
// 启动线程,标识线程启动成功
t.start();
workerStarted = true;
}
}
} finally {
// 线程启动失败,需要从工作线程中移除对应的Worker
if (!workerStarted)
addWorkerFailed(w);
}
return workerStarted;
}
execute()
和submit()
的区别execute()
方法用于提交不需要返回值的任务,所以无法判断任务是否被线程池执行成功与否;submit()
方法用于提交需要返回值的任务。线程池会返回一个 Future
类型的对象,通过这个 Future
对象可以判断任务是否执行成功,并且可以通过 Future
的 get()
方法来获取返回值,get()
方法会阻塞当前线程直到任务完成,而使用 get(long timeout,TimeUnit unit)
方法的话,如果在 timeout
时间内任务还没有执行完,就会抛出 java.util.concurrent.TimeoutException
。一般的队列只能是有限长度的缓冲区,一旦超出缓冲长度,就无法保留了。阻塞队列通过阻塞可以保留住当前想要继续入队的任务。
阻塞队列可以在队列中没有任务时,阻塞想要获取任务的线程,使其进入wait状态,释放cpu资源。
阻塞队列带有阻塞和唤醒的功能,不需要额外处理,无任务执行时,线程池利用阻塞队列的take方法挂起,从而维持核心线程的存活,不至于一直占用cpu资源。
在创建新线程的时候,是要获取全局锁的,这时候其他线程会被阻塞,影响整体效率。
在核心线程已满时,如果任务继续增加那么放在队列中,等队列满了而任务还在增加那么就要创建临时线程了,这样代价低。
https://www.throwx.cn/2020/08/23/java-concurrency-thread-pool-executor/
https://javaguide.cn/java/concurrent/java-thread-pool-summary.html#%E7%BA%BF%E7%A8%8B%E6%B1%A0%E5%8E%9F%E7%90%86%E5%88%86%E6%9E%90