api windows 线程加锁_从 0 到 1 实现一个线程池(二)实现篇

一、前言

前面的帖子《从 0 到 1 实现一个线程池(一)基础原理篇》,已经介绍过进程、线程、线程池等基本原理和相关源码,算是对线程池主流程的一个总体介绍,让我们基本理解线程池的原理和总体框架,也是自己对这块知识点的复习和记忆强化。
为了加深理解,本篇就要动手从 0 到 1 实现一个简易版线程池,简易不代表功能缺失,核心流程和功能要一应俱全。建议先看到本篇的朋友或者对线程池不熟悉的朋友,先看下《从 0 到 1 实现一个线程池(一)基础原理篇》。

二、实现

2.1 创建线程池

首先,我们自定义一个类,CustomThreadPoolExecutor ,作为线程池的核心类,他的工作原理如下:8f4d6aa0b780fd9818a57034c72d1fd4.png
简单来说就是将待处理的任务放入往线程池,该任务要么立即执行,要么进入阻塞队列等待执行。线程池中存放的是多个线程,这些线程会不断的从阻塞队列里获取任务并执行。接下来看具体实现。

2.1.1 构造方法

下面是这个线程池的构造方法的代码:

/**
* 构造方法
*
* @param miniSize 最小线程数
* @param maxSize 最大线程数
* @param keepAliveTime 线程保活时间
* @param unit 时间单位
* @param workQueue 阻塞队列
* @param notify 通知接口
*/
public CustomThreadPoolExecutor(int miniSize, int maxSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, Notify notify) {
this.miniSize = miniSize;
this.maxSize = maxSize;
this.keepAliveTime = keepAliveTime;
this.unit = unit;
this.workQueue = workQueue;
this.notify = notify;

workers = new ConcurrentHashSet<>();
}

Copy


有以下几个核心参数:

miniSize:最小线程数,等同于 ThreadPoolExecutor 中的核心线程数;
maxSize:最大线程数,等同于 ThreadPoolExecutor 中的最大线程数;
keepAliveTime:线程保活时间,等同于 ThreadPoolExecutor 中的线程保活时间;
workQueue:阻塞队列,等同于 ThreadPoolExecutor 中的阻塞队列。
notify:通知接口,用于线程池关闭通知。


大致上都和 ThreadPoolExecutor 中的参数相同,并且作用也是类似的。需要注意的是最后初始化了一个 workers 成员变量:

/**
* 存放线程池
*/
private volatile Set<Worker> workers;

Copy

workers 存放的是线程池中运行的线程,在 java.util.concurrent 源码中是一个 HashSet ,所以对它的所有操作都需要加锁。

/**
* Set containing all worker threads in pool. Accessed only when
* holding mainLock.
*/
private final HashSet<Worker> workers = new HashSet<Worker>();

Copy

这里为了简单起见,自己定义了一个线程安全的 ConcurrentHashSet

/**
* 内部存放工作线程容器,并发安全
*
* @param
*/
private final class ConcurrentHashSet<T> extends AbstractSet<T> {

// 实际存放线程的容器
private ConcurrentHashMap<T, Object> map = new ConcurrentHashMap<>();

// value
private final Object PRESENT = new Object();

// 统计结果
private AtomicInteger count = new AtomicInteger();

@Override
public Iterator<T> iterator() {
return map.keySet().iterator();
}

@Override
public boolean add(T t) {
count.incrementAndGet();
return map.put(t, PRESENT) == null;
}

@Override
public boolean remove(Object o) {
count.decrementAndGet();
return map.remove(o) == PRESENT;
}

@Override
public int size() {
return count.get();
}
}

Copy


原理和 HashSet 类似,也是借助于 HashMap 来存放数据,利用 KEY 的不可重复的特性来实现 SET ,只是这里的 HashMap 是用并发安全的 ConcurrentHashMap 来实现的,这样就能保证对它的写入、删除都是线程安全的。
不过由于 ConcurrentHashMap 的 size() 函数并不准确,所以这里单独利用了一个 AtomicInteger 来统计容器大小。

实际上,这里的 ConcurrentHashSet 也存在问题,就是其 add() 和 remove() 方法,插入和删除重复元素的时候,也会对原子 count 进行增加和减少,最终会导致size不对。
但是本场景下,要么是插入核心线程,要么是插入非核心线程,删除也是一样,一定是非重复元素,所以不会导致该问题,如果有同学使用这段代码,需要注意该问题。

2.1.2 创建核心线程

往线程池中丢进一个任务后,要做的事情很多,单最重要的事情莫过于创建线程存放到线程池中了。当然我们不能无限制的创建线程,不然线程池就没任何意义了。
于是 miniSize 和 maxSize 这两个参数就有了重要的意义。那接下来,我们就根据这两个参数和线程池源码,实现一个简版的任务调度流程。
流程图如下:f451dbb399c6120ffce5843e816c6ed9.png
第一步需要判断当前工作线程数是否小于核心线程数,如果是则创建核心线程。代码如下:

public void execute(Runnable runnable) {

//提交的线程 计数
totalTask.incrementAndGet();

//小于最小线程数时新建线程
if (workers.size() < miniSize) {
addWorker(runnable);
return;
}
}

Copy

这里实际上对应 ThreadPoolExecutor 的 execute() 方法的源码。这里的 miniSize 和 maxSize 由于会在多线程场景下使用,所以也用 volatile 关键字来保证可见性。

/**
* 最小线程数,也叫核心线程数
*/
private volatile int miniSize;

/**
* 最大线程数
*/
private volatile int maxSize;

Copy

2.1.3 阻塞队列

结合 2.1.2 中的流程图,第二步要做的是判断队列是否可以存放任务(即阻塞队列是否已满)。创建完核心线程执行任务后,接下来的任务优先会往阻塞队列里存放。

public void execute(Runnable runnable) {

// ......

boolean offer = workQueue.offer(runnable);
}

Copy

2.1.4 非核心线程

一旦写入阻塞队列失败,则会判断当前线程池的大小是否大于最大线程数,如果没有则继续创建线程执行。否则尝试阻塞写入队列,但是 ThreadPoolExecutor 源码会在这里执行拒绝策略。

public void execute(Runnable runnable) {

// ......

boolean offer = workQueue.offer(runnable);
//写入队列失败
if (!offer) {

//创建新的线程执行
if (workers.size() < maxSize) {
addWorker(runnable);
return;
} else {
LOGGER.error("超过最大线程数");
try {
//会阻塞
workQueue.put(runnable);
} catch (InterruptedException e) {

}
}
}
}

Copy

2.1.5 注意事项

79791c103bb2424be59a503aaf5b6e3a.png
上图中的红框这两步会直接创建新的线程。这个过程相比于将任务直接写入阻塞队列的开销要大的多,主要有以下两个原因:

  • 创建线程会加锁,虽说最终用的是 ConcurrentHashMap 的写入函数,但依然大概率存在加锁的可能

  • 会创建新的线程,创建线程需要调用操作系统的 API ,开销很大


所以理想情况下我们应该避免这两步,尽量让丢入线程池中的任务进入阻塞队列中。
整体创建线程部分代码如下:

/**
* 执行任务
*
* @param runnable 需要执行的任务
*/
public void execute(Runnable runnable) {

if (runnable == null) {
throw new NullPointerException("runnable nullPointerException");
}

if (isShutDown.get()) {
LOGGER.info("线程池已经关闭,不能再提交任务!");
return;
}

//提交的线程 计数
totalTask.incrementAndGet();

//小于最小线程数时新建线程
if (workers.size() < miniSize) {
addWorker(runnable);
return;
}

boolean offer = workQueue.offer(runnable);
//写入队列失败
if (!offer) {

//创建新的线程执行
if (workers.size() < maxSize) {
addWorker(runnable);
return;
} else {
LOGGER.error("超过最大线程数");
try {
//会阻塞
workQueue.put(runnable);
} catch (InterruptedException e) {

}
}
}
}

Copy

2.2 执行任务

通过上述步骤,已经将任务添加进来了,那这些任务接下来要如何执行呢?接下来,我们去实现 addWorker() 方法。

/**
* 添加任务,需要加锁
*
* @param runnable 任务
*/
private void addWorker(Runnable runnable) {
Worker worker = new Worker(runnable, true);
worker.startTask();
workers.add(worker);
}

Copy


在创建线程执行任务的时候会创建 Worker 对象,利用它的 startTask() 方法来执行任务。所以我们需要先定义一个 Worker 对象的数据结构,如下:

/**
* 工作线程
*/
private final class Worker extends Thread {

private Runnable task;

private Thread thread;
/**
* true --> 创建新的线程执行
* false --> 从队列里获取线程执行
*/
private boolean isNewTask;

public Worker(Runnable task, boolean isNewTask) {
this.task = task;
this.isNewTask = isNewTask;
thread = this;
}

public void startTask() {
thread.start();
}

public void close() {
thread.interrupt();
}

// ....
}

Copy


其实它本身也是一个线程,将接收到的待执行的任务存放到成员变量 task 处。而其中最为关键的则是执行任务 worker.startTask() 这一步骤,其实就是运行了 worker 线程自己:

public void startTask() {
thread.start();
}

Copy


由于Work对象本身就是一个线程,所以需要继承线程类 Thread ,实现 Runnable 类,重写 run() 方法:

@Override
public void run() {

Runnable task = null;

if (isNewTask) {
task = this.task;
}

boolean compile = true;

try {
while ((task != null || (task = getTask()) != null)) {
try {
//执行任务
task.run();
} catch (Exception e) {
compile = false;
throw e;
} finally {
//任务执行完毕
task = null;
int number = totalTask.decrementAndGet();
if (number == 0) {
synchronized (shutDownNotify) {
shutDownNotify.notify();
}
}
}
}

} finally {
//释放线程
boolean remove = workers.remove(this);
if (!compile) {
addWorker(null);
}
tryClose(true);
}
}

Copy


首先,将创建线程时传过来的任务执行 task.run() ,接着会不断的从阻塞队列里面获取任务执行,直到获取不到新任务了。
然后,任务执行完毕后将内置的计数器 -1,方便后面任务全部执行完毕进行通知。
最后,worker 线程获取不到任务后退出,需要将自己从线程池中释放掉 workers.remove(this)
其实 getTask() 也是非常关键的一个方法,它封装了从阻塞队列中获取的任务,同时对不需要保活的线程进行回收,代码如下:

/**
* 从队列中获取任务
*
* @return
*/
private Runnable getTask() {

//关闭标识及任务是否全部完成
if (isShutDown.get() && totalTask.get() == 0) {
return null;
}

lock.lock();

try {
Runnable task = null;
if (workers.size() > miniSize) {
//大于核心线程数时需要用保活时间获取任务
task = workQueue.poll(keepAliveTime, unit);
} else {
task = workQueue.take();
}

if (task != null) {
return task;
}

} catch (InterruptedException e) {
return null;
} finally {
lock.unlock();
}

return null;
}

Copy


核心作用就是从阻塞队列里获取任务,但有两个地方需要注意:

  • 当线程数超过核心线程数时,在获取任务的时候需要通过保活时间从队列里获取

  • 一旦获取不到任务则队列肯定是空的,这样返回 null 之后在上文的 run() 中就会退出这个线程,从而达到了回收线程的目的,也就是我们之前演示的效果

  • 需要加锁处理,加锁的原因是这里肯定会出现并发情况,不加锁会导致 workers.size() > miniSize 条件多次执行,从而导致线程被全部回收完毕。

2.3 关闭线程池

最后,我们来实现线程池的关闭功能。如果就上述实现来说,不关闭线程池,程序也不会退出,不退出的原因是 Worker 线程还一直阻塞在 task = workQueue.take() 处,即便是线程缩容了也不会小于核心线程数。
而关闭线程通常又有以下两种:

  • 立即关闭:执行关闭方法后不管现在线程池的运行状况,直接一刀切全部停掉,这样会导致任务丢失。

  • 等待关闭:不接受新的任务,同时等待现有任务执行完毕后退出线程池。

2.3.1 立即关闭

代码如下:

/**
* 立即关闭线程池,会造成任务丢失
*/
public void shutDownNow() {
isShutDown.set(true);
tryClose(false);
}

/**
* 关闭线程池
*
* @param isTry true 尝试关闭 --> 会等待所有任务执行完毕
* false 立即关闭线程池--> 任务有丢失的可能
*/
private void tryClose(boolean isTry) {
if (!isTry) {
closeAllTask();
} else {
if (isShutDown.get() && totalTask.get() == 0) {
closeAllTask();
}
}
}

/**
* 关闭所有任务
*/
private void closeAllTask() {
for (Worker worker : workers) {
worker.close();
}
}

// 关闭线程
public void close() {
thread.interrupt();
}

Copy


立即关闭,就是遍历线程池里所有的 worker 线程,执行他们的中断函数。

2.3.2 等待关闭

 /**
* 任务执行完毕后关闭线程池
*/
public void shutdown() {
isShutDown.set(true);
tryClose(true);
}

Copy


这里会多一个判断,需要所有任务都执行完毕之后,即 totalTask.get() == 0 后才会去中断线程。

2.3.3 回收线程

一旦执行了 shutdown() / shutdownNow() 方法都会将线程池的状态置为关闭状态,这样只要 worker 线程尝试从队列里获取任务时就会直接返回空,导致 worker 线程被回收。
一旦线程池大小超过了核心线程数就会使用保活时间来从队列里获取任务,所以一旦获取不到返回 null 时就会触发回收。

2.4 任务完成后通知

线程池中的任务执行完毕后再通知主线程做其他事情,比如一批任务都执行完毕后再执行下一批任务,如果遇到这样的需求,如何实现呢?首先,我们定义一个通知接口

public interface Notify {

/**
* 回调
*/
void notifyListen() ;
}

Copy


然后,在自定义线程池构造方法中初始化该对象

/**
* 构造方法
*
* @param miniSize 最小线程数
* @param maxSize 最大线程数
* @param keepAliveTime 线程保活时间
* @param unit 时间单位
* @param workQueue 阻塞队列
* @param notify 通知接口
*/
public CustomThreadPoolExecutor(int miniSize, int maxSize, long keepAliveTime, TimeUnit unit,
BlockingQueue<Runnable> workQueue, Notify notify) {
this.miniSize = miniSize;
this.maxSize = maxSize;
this.keepAliveTime = keepAliveTime;
this.unit = unit;
this.workQueue = workQueue;
this.notify = notify;

workers = new ConcurrentHashSet<>();
}

Copy


然后,在使用线程池的地方,创建线程池的时候,重写 notifyListen() ,去实现自己的回调通知,那什么时候应该回调这个接口呢?只要我们记录提交到线程池中的任务及完成的数量,他们两者的差为 0 时就认为线程池中的任务已执行完毕,这时便可回调这个接口。所以在往线程池中写入任务时我们需要记录任务数量:

/**
* 执行任务
*
* @param runnable 需要执行的任务
*/
public void execute(Runnable runnable) {

// ......

//提交的线程 计数
totalTask.incrementAndGet();

// ......
}

Copy

为了并发安全的考虑,这里的计数器采用了原子的 AtomicInteger 。

public void run() {

// ......

try {
while ((task != null || (task = getTask()) != null)) {
try {
// ......
} catch (Exception e) {
// ......
} finally {
//任务执行完毕
task = null;
int number = totalTask.decrementAndGet();
if (number == 0) {
synchronized (shutDownNotify) {
shutDownNotify.notify();
}
}
}
}

} finally {
// ......
}
}

Copy

而在任务执行完毕后就将计数器 -1 ,一旦为 0 时则任务任务全部执行完毕;这时便可回调我们自定义的接口完成通知。

2.4.1 源码中的实现

我们使用 ThreadPoolExecutor 的常规关闭流程如下:

executorService.shutdown();
while (!executorService.awaitTermination(100, TimeUnit.MILLISECONDS)) {
logger.info("thread running");
}

Copy


线程提交完毕后执行 shutdown() 关闭线程池,接着循环调用 awaitTermination()方法,一旦任务全部执行完毕后则会返回 true 从而退出循环。这两个方法的目的和原理如下:

  • 执行 shutdown() 后会将线程池的状态置为关闭状态,这时将会停止接收新的任务同时会等待队列中的任务全部执行完毕后才真正关闭线程池。

  • awaitTermination 会阻塞直到线程池所有任务执行完毕或者超时时间已到。


所有线程执行完毕后再做其他事情,也就是在线程执行完毕之前其实主线程是需要被阻塞的。shutdown() 执行后并不会阻塞,会立即返回,所有才需要后续用循环不停的调用 awaitTermination(),因为这个 api 才会阻塞线程。
其实我们查看源码会发现,ThreadPoolExecutor 中的阻塞依然也是等待通知机制的运用,只不过用的是 LockSupport 的 API 而已。

2.5 带有返回值的线程

2.5.1 实现方式

这个需求也非常常见,比如需要线程异步计算某些数据然后得到结果最终汇总使用,接下来实现它。首先任务是不能实现 Runnable 接口了,毕竟他的 run() 函数是没有返回值的,所以我们改实现一个 Callable 的接口:

public final static class WorkerWithReturnValue implements Callable<Integer> {

private int state;

public WorkerWithReturnValue(int state) {
this.state = state;
}

@Override
public Integer call() throws Exception {
try {
TimeUnit.SECONDS.sleep(1);
LOGGER.info("state={}", state);
return state + 1;
} catch (Exception e) {

}

return 0;
}
}

Copy


同时在提交任务时也稍作改动,从原来的:

public static void main(String[] args) {
BlockingQueue queue = new ArrayBlockingQueue(4);

CustomThreadPoolExecutor pool = new CustomThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS, queue, null);

for (int i = 0; i < 10; i++) {
pool.execute(new Worker(i));
}

pool.shutdown();

pool.mainNotify();
}

Copy

修改为:

public static void main(String[] args) {
BlockingQueue queue = new ArrayBlockingQueue(4);

CustomThreadPoolExecutor pool = new CustomThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS, queue, null);

List<Future<Integer>> futureList = new ArrayList<>();
for (int i = 0; i < 10; i++) {
futureList.add(pool.submit(new Worker(i)));
}

pool.shutdown();

pool.mainNotify();
try {
for (Future<Integer> future : futureList) {
Integer integer = future.get();
LOGGER.info("future={}", integer);
}
} catch (Exception e) {

}
}

Copy

首先是执行任务的函数由 execute()换为了 submit(),同时他会返回一个返回值 Future,通过它便可拿到线程执行的结果。

2.5.2 实现原理

  • 首先受限于 jdk 的线程 api 的规范,要执行一个线程不管是实现接口还是继承类,最终都是执行的 run() 函数。

  • 想要一个线程有返回值无非只能是在执行run() 函数时去调用一个有返回值的方法,再将这个返回值存放起来用于后续使用。


所以这里我们使用了 Callable 的接口,源码如下:

@FunctionalInterface
public interface Callable<V> {
/**
* Computes a result, or throws an exception if unable to do so.
*
* @return computed result
* @throws Exception if unable to compute a result
*/
V call() throws Exception;
}

Copy

它的 call() 函数就是刚才提到的有返回值的方法,如果线程在 run() 函数中去调用它,即可得到返回值,但这只是我们的猜想,后续看源码是不是这样实现的。
接着再利用一个 Future 接口,它的主要作用是获取线程的返回值,也就是再将这个返回值存放起来用于后续使用。既然有了接口那自然就得有它的实现 FutureTask,它实现了 Future 接口用于后续获取返回值。同时实现了 Runnable 接口会把自己变为一个线程,部分源码如下:

public class FutureTask<V> implements RunnableFuture<V> {

private Callable<V> callable;

public FutureTask(Callable<V> callable) {
if (callable == null)
throw new NullPointerException();
this.callable = callable;
this.state = NEW;
}

public void run() {
if (state != NEW ||
!UNSAFE.compareAndSwapObject(this, runnerOffset,
null, Thread.currentThread()))
return;
try {
Callable<V> c = callable;
if (c != null && state == NEW) {
V result;
boolean ran;
try {
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
setException(ex);
}
if (ran)
set(result);
}
} finally {
// runner must be non-null until state is settled to
// prevent concurrent calls to run()
runner = null;
// state must be re-read after nulling runner to prevent
// leaked interrupts
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}

// ......
}

Copy

我们发现,Callable 部分的源码,确实是在 run() 函数中会调用刚才提到的具有返回值的 call() 函数,印证了我们的猜想。
最后,就剩下一个 提交任务 的能力,类似 execute() ,只不过要能够把 Callable 和 Future 结合起来使用,这个就是源码中的 submit() 方法和 get() 方法。

2.5.2.1 submit

带返回值的提交任务 submit() 的源码如下:

public class FutureTask<V> implements RunnableFuture<V> {

// ....

/**
* @throws CancellationException {@inheritDoc}
*/
public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}

/**
* @throws CancellationException {@inheritDoc}
*/
public V get(long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException {
if (unit == null)
throw new NullPointerException();
int s = state;
if (s <= COMPLETING &&
(s = awaitDone(true, unit.toNanos(timeout))) <= COMPLETING)
throw new TimeoutException();
return report(s);
}

public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
}

Copy

submit() 源码也非常简单,将我们丢进来的 Callable 对象转换为一个 FutureTask 对象。FutureTask 本身也是线程,所以可以直接使用 execute() 函数,最后再调用之前的 execute() 将任务丢进线程池,后续的流程就和之前介绍的一样了。

2.5.2.2 get

future.get() 函数中 future 对象由于在 submit() 中返回的真正对象是 FutureTask,所以我们直接看其中的源码就好。

public V get() throws InterruptedException, ExecutionException {
int s = state;
if (s <= COMPLETING)
s = awaitDone(false, 0L);
return report(s);
}

Copy

/**
* Awaits completion or aborts on interrupt or timeout.
*
* @param timed true if use timed waits
* @param nanos time to wait, if timed
* @return state upon completion
*/
private int awaitDone(boolean timed, long nanos)
throws InterruptedException {
final long deadline = timed ? System.nanoTime() + nanos : 0L;
WaitNode q = null;
boolean queued = false;
for (;;) {
if (Thread.interrupted()) {
removeWaiter(q);
throw new InterruptedException();
}

int s = state;
if (s > COMPLETING) {
if (q != null)
q.thread = null;
return s;
}
else if (s == COMPLETING) // cannot time out yet
Thread.yield();
else if (q == null)
q = new WaitNode();
else if (!queued)
queued = UNSAFE.compareAndSwapObject(this, waitersOffset,
q.next = waiters, q);
else if (timed) {
nanos = deadline - System.nanoTime();
if (nanos <= 0L) {
removeWaiter(q);
return state;
}
LockSupport.parkNanos(this, nanos);
}
else
LockSupport.park(this);
}
}

Copy

通过源码,可以看出 get() 在线程没有返回之前是一个阻塞函数,利用这个阻塞的能力,主线程会一直等待所有子线程执行完毕后,在继续执行。

2.6 异常处理

最后是使用线程池很容易踩坑的一个地方,那就是异常处理。如类似于这样的场景:

public static void main(String[] args) {
BlockingQueue queue = new ArrayBlockingQueue(1);

CustomThreadPoolExecutor pool = new CustomThreadPoolExecutor(1, 1, 1, TimeUnit.SECONDS, queue, new Notify() {
@Override
public void notifyListen() {
LOGGER.info("任务执行完毕!");
}
});
pool.execute(new Worker(0));
pool.mainNotify();
}

/**
* 工作线程
*/
private static class Worker implements Runnable {

private int state;

public Worker(int state) {
this.state = state;
}

@Override
public void run() {
try {
TimeUnit.SECONDS.sleep(1);
LOGGER.info("state={}");

while (true) {
state++;
if (state == 1000) {
throw new NullPointerException("NullPointerException");
}
}
} catch (Exception e) {

}
}
}

Copy

创建了只有一个核心线程的线程池,这个线程只做一件事,就是一直不停的 while 循环。但是循环的过程中不小心抛出了一个异常,巧的是这个异常又没有被捕获。你觉得后续会发生什么事情呢?是线程继续运行?还是线程池会退出?591e883011e9dc2c1de0b4e419865182.png
通过现象来看其实哪种都不是,线程既没有继续运行同时线程池也没有退出,会一直卡在这里。当我们 dump 线程快照会发现:f5c67a9e7612f03a0d7ee4701fc5cfd7.png
这时线程池中还有一个线程在运行,通过线程名称会发现这是新创建的一个线程(之前是Thread-0,现在是 Thread-1)。它的线程状态为 WAITING ,通过堆栈发现是卡在了 CustomThreadPool.java:272 处。6443587e26711db8b772eb32c4b28b02.png
就是卡在了从队列里获取任务的地方,由于此时的任务队列是空的,所以他会一直阻塞在这里。其实在线程池内部会对线程的运行捕获异常,但它并不会处理,只是用于标记是否执行成功。一旦执行失败则会回收掉当前异常的线程,然后重新创建一个新的 Worker 线程继续从队列里取任务然后执行。所以最终才会卡在从队列中取任务处。

三、总结

经过这两篇帖子,弄清楚线程池问题不大,总的来看它内部运用了非常多的多线程解决方案,比如:

  • ReentrantLock 重入锁来保证线程写入的并发安全。

  • 利用等待通知机制来实现线程间通信(线程执行结果、等待线程池执行完毕等)。

好记性不如烂笔头,这些常用知识点还是要时常复习和回顾,才能保证在开发过程中临危不乱。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值