我以前对线程的理解是:
线程池中的线程池分为核心线程和非核心线程,核心线程在完成后会被回收(丢掉其中的任务,继续执行别的任务) ,非核心线程空闲超过允许时间后就被杀死,线程中抛出的异常都会被直接打印在控制台上
现在看来:
所有线程在执行完成后都会在存储线程的HashSet中被removed,但线程池会维持corePoolSize条线程存活持续等待任务。当任务队列满了,但活跃线程数小于maximumPoolSize,线程池就会去创建额外的线程去执行任务。当任务队列空了,线程池只会保留corePoolSize条线程,额外线程就会被杀死。
submit
提交给线程的任务的异常只有在get是才能获取到,execute
则会直接打印在控制台上。
前言
在项目开发中,使用多线程,将实现了Callable
的类的的任务提交给线程池后,在get
结果时才打印异常,觉得很奇怪,理论上应该在控制台上打印异常才对,后来在网上搜索后才了解到原因,给线程池提交任务分为submit
和execute
方法, 它们对于异常的获取是有区别的。
验证代码
excute
方法异常打印
简易代码:
Java
public class ThreadPoolTest {
public static void main(String[] args) {
ThreadPoolExecutor executorService = buildThreadPoolTaskExecutor();
executorService.execute(() -> run("execute方法"));
executorService.submit(() -> run("submit方法"));
}
/**
* 线程需要执行的方法,测试异常抛出日志打印
*
* @param name 线程名称
*/
private static void run(String name) {
String printStr = "【thread-name:" + Thread.currentThread().getName() + ",执行方式:" + name + "】";
System.out.println(printStr);
throw new RuntimeException(printStr + ",出现异常");
}
/**
* 创建一个线程池
*
* @return 线程池
*/
private static ThreadPoolExecutor buildThreadPoolTaskExecutor() {
return new ThreadPoolExecutor(2,
5,
10,
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
new ThreadPoolExecutor.AbortPolicy());
}
}
观看执行结果,可以看出
excute
提交的任务打印了异常submit
提交的方式没有异常信息。
submit
方法异常打印
修改代码中的main方法如下:
Java
public static void main(String[] args) {
ThreadPoolExecutor executorService = buildThreadPoolTaskExecutor();
// executorService.execute(() -> run("execute方法"));
Future<?> future = executorService.submit(() -> run("submit方法"));
try {
future.get();
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
观看执行结果,可以看出submit
提交任务抛出的异常页成功打印。
现在问题来了,为啥execute
直接抛出异常,submit
没有呢?
原因
execute
和submit
的区别
知道区别有助于更好的找到原因:
execute
没有返回值。可以执行任务,但无法判断任务是否成功完成。一般提交实现Runnable
接口的任务。submit
返回一个future
。可以用这个future
来判断任务是否成功完成。一般提交实现Callable
接口的任务。
源码
前情提要
java.util.concurrent.ThreadPoolExecutor#addWorker
方法注释机翻,我一开始以为core
参数区分创建核心线程还是非核心线程,其实只是在方法中根据core
进行对比, 如果为 true,方法中关于则使用 corePoolSize 作为限制与当前工作线程数作比较,否则使用 maximumPoolSize。
Java
/**
* 检查是否可以根据当前线程池状态和给定的限制(核心线程池或最大线程池)添加新工作线程。
* 如果可以,工作线程数量将相应调整,并且如果可能,将创建并启动一个新工作线程,
* 执行 firstTask 作为其第一个任务。如果线程池已停止或有资格关闭,则该方法返回 false。
* 如果线程工厂在请求时未能创建线程,也会返回 false。如果线程创建失败,可能是由于线程工厂返回 null,
* 或由于异常(通常是 OutOfMemoryError 在 Thread.start() 中),我们会进行干净的回滚。
*
* @param firstTask 新线程应首先运行的任务(如果没有则为 null)。
* 在 execute() 方法中创建的工作线程具有初始任务,以绕过排队,
* 当线程数少于 corePoolSize 时(在这种情况下我们始终启动一个),
* 或者当队列已满时(在这种情况下必须绕过队列)。
* 初始空闲线程通常通过 prestartCoreThread 创建,或用来替换其他死亡的工作线程。
* @param core 如果为 true,则使用 corePoolSize 作为限制,
* 否则使用 maximumPoolSize。这里使用布尔指示器而不是值,以确保在检查其他池状态后读取新值。
* @return true if successful
*/
private boolean addWorker(Runnable firstTask, boolean core) {
return false;
}
execute
方法相关源码
以java.util.concurrent.ThreadPoolExecutor#execute(Runnable command)
为例:
Java
public void execute(Runnable command) {
// 1. 空指针检查
if (command == null)
throw new NullPointerException();
// 2. 获取当前线程池的状态
int c = ctl.get();
// 3. 如果当前线程数量小于 `corePoolSize`(核心线程数),则尝试创建一个新线程来执行任务。
if (workerCountOf(c) < corePoolSize) {
if (addWorker(command, true))
return;
c = ctl.get();
}
// 4. 线程数大于等于 `corePoolSize` 或者核心线程创建失败
// a. 如果线程池正在工作,则将任务放入等待队列中
if (isRunning(c) && workQueue.offer(command)) {
// b. 再次检查
int recheck = ctl.get();
// c. 如果线程池停止则且移除等待队列成功,则拒绝任务
if (! isRunning(recheck) && remove(command))
reject(command);
// d. 如果线程池在运行,但工作线程数又为0,则创建一个新的空闲线程
else if (workerCountOf(recheck) == 0)
addWorker(null, false);
}
// 5. 线程池停止工作或放入等待队列失败
// 尝试新线程去执行,失败拒绝任务
else if (!addWorker(command, false))
reject(command);
}
从上面的方法可以知道,excute
将任务提交给了Worker
, 现在我们去看java.util.concurrent.ThreadPoolExecutor.Worker
类,。
可以看到它实现了Runnable
接口,所以我们查看它的java.util.concurrent.ThreadPoolExecutor.Worker#run
方法,在run
方法中,它调用了java.util.concurrent.ThreadPoolExecutor#runWorker
方法
Java
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
Runnable task = w.firstTask;
w.firstTask = null;
w.unlock(); // allow interrupts
boolean completedAbruptly = true;
try {
// 这里就是线程可以重用的原因,循环+条件判断,不断从队列中取任务
// 还有一个问题就是非核心线程的超时删除是怎么解决的
// 主要就是getTask方法()
while (task != null || (task = getTask()) != null) {
w.lock();
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);
}
}
上述代码中可以看到,task.run()
发生异常被捕获后直接抛出,这也是我们能直接看到execute
提交任务后异常的原因。
至此,execute
直接打印异常的原因找到了,接下来处理submit
。
submit
方法源码
以java.util.concurrent.AbstractExecutorService
类的submit
方法为例,它是一个抽象类,java.util.concurrent.ThreadPoolExecutor
继承了它。
Java
public <T> Future<T> submit(Callable<T> task) {
if (task == null) throw new NullPointerException();
RunnableFuture<T> ftask = newTaskFor(task);
execute(ftask);
return ftask;
}
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
return new FutureTask<T>(callable);
}
我们可以看见,submit
方法首先将任务封装成了FutureTask
对象,然后再调用execute
方法,所以其也会将任务提交给Worker
,再由Worker
去执行,Worker#run -> Worker#runWorker -> task.run
,所以我们定位到FutureTask
的run
等相关方法:
Java
private volatile int state;
private static final int NEW = 0;
private static final int COMPLETING = 1;
private static final int NORMAL = 2;
private static final int EXCEPTIONAL = 3;
private static final int CANCELLED = 4;
private static final int INTERRUPTING = 5;
private static final int INTERRUPTED = 6;
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 {
// 调用Callable接口的call方法
result = c.call();
ran = true;
} catch (Throwable ex) {
result = null;
ran = false;
// 捕获异常后并没有抛出,而是保存起来
setException(ex);
}
if (ran)
set(result);
}
} finally {
runner = null;
int s = state;
if (s >= INTERRUPTING)
handlePossibleCancellationInterrupt(s);
}
}
protected void setException(Throwable t) {
// 通过CAS操作确保只有一个线程能够将任务状态从 `NEW` 更新为 `COMPLETING`,表示任务正在完成。
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
// 异常值赋值给了 outcome属性
outcome = t;
// 更新state的状态为EXCEPTIONAL
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
/**
* 获取执行结果
*/
public V get() throws InterruptedException, ExecutionException {
// 获取任务状态
int s = state;
if (s <= COMPLETING)
// 如果任务没有完成,阻塞线程等待任务完成
s = awaitDone(false, 0L);
// 任务已经完成返回结果
return report(s);
}
private V report(int s) throws ExecutionException {
// 之心结果
Object x = outcome;
if (s == NORMAL)
// 任务正常完成
return (V)x;
if (s >= CANCELLED)
// 任务被取消了或被中断,抛出指定异常
throw new CancellationException();
// 只剩下EXCEPTIONAL状态,抛出存储的异常
throw new ExecutionException((Throwable)x);
}
上述代码可以知道submit
方式提交的任务并没有直接执行execute
方法,而是先封装成FutureTask
对象,再执行execute
方法,由Worker
去执行,Worker#run -> Worker#runWorker -> FutureTask#run -> Callable#call
, 再FutureTask#run
中捕获Callable#call
的异常,存到属性outcome
, FutureTask#get
时再抛出异常。
至此submit
提交的任务再get时才获取的原因也清楚了。
总结和思考
总结
简而言之:
execute
提交的任务在Worker#run
中被捕获后直接抛出。submit
提交的任务在FutureTask#run
中被捕获后存储到了属性中,get时才会抛出异常。
思考
为什么submit
提交的任务要get
时才会获取结果?
Callable
提交的任务,我们只需要关系其结果即可,不用关系其过程,主线程只需要在处理完自己的任务后获取一个或一批任务的状态和结果,让开发者决定何时检查任务是否成功完成,以及如何处理异常。- 而
Runnable
我们一般不要获取返回值,只需要提交任务即可,如果不直接抛出异常就会导致任务失败了但是没有感知,尤其是在任务中catch
遗漏的一些没有预料到异常且未设置UncaughtExceptionHandler
的时候。 - 虽然
Runnable
任务虽然没有返回值,但也可以submit
, 然后通过Future.get()
了解到其成功或失败的状态。
线程抛出了异常,这个线程会被怎么处理?
Java
final void runWorker(Worker w) {
Thread wt = Thread.currentThread();
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);
}
}
在java.util.concurrent.ThreadPoolExecutor#runWorker
方法中,我们可以看到最终都会调用processWorkerExit(w, completedAbruptly)
方法。
Java
private void processWorkerExit(Worker w, boolean completedAbruptly) {
if (completedAbruptly) // If abrupt, then workerCount wasn't adjusted
// 如果线程是异常退出, 调整工作线程的数量
decrementWorkerCount();
final ReentrantLock mainLock = this.mainLock;
mainLock.lock();
try {
// 累加该线程池完成的任务数
completedTaskCount += w.completedTasks;
// 当某个线程退出时,需要将它从 `workers` 中移除
workers.remove(w);
} finally {
mainLock.unlock();
}
// 尝试终止线程池,前提:所有的任务都已完成,并且线程池已经处于关闭状态
tryTerminate();
int c = ctl.get();
// 线程池没有被停止
if (runStateLessThan(c, STOP)) {
if (!completedAbruptly) {
// 如果是线程是正常结束
// min = 0 (允许核心线程超时关闭) 或 核心线程数
int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
if (min == 0 && ! workQueue.isEmpty())
min = 1;
if (workerCountOf(c) >= min)
return; // replacement not needed
}
// 创建一个新线程
addWorker(null, false);
}
}
答案是 所有运行完成的线程都会被remove,然后就会尝试创建一个新的线程来顶替。
关于UNSAFE
Java
protected void setException(Throwable t) {
if (UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)) {
outcome = t;
UNSAFE.putOrderedInt(this, stateOffset, EXCEPTIONAL); // final state
finishCompletion();
}
}
上述代码我有个问题,为啥UNSAFE.compareAndSwapInt(this, stateOffset, NEW, COMPLETING)
等能修改属性state
的值呢,参数明明传入的stateOffset
呀?
在网上搜索后了解到:
Unsafe
是位于sun.misc
包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升 Java 运行效率、增强 Java 语言底层资源操作能力方面起到了很大的作用。Unsafe
提供的这些功能的实现需要依赖本地方法(Native Method)。可以将本地方法看作是 Java 中使用其他编程语言编写的方法。
所以stateOffset
是state
属性在内存地址相对于此对象的内存地址的偏移量。
Java
Class<?> k = FutureTask.class;
stateOffset = UNSAFE.objectFieldOffset
(k.getDeclaredField("state"));
详情可见 Java 魔法类 Unsafe 详解 及其参考链接。
参考链接:
[1] Java 魔法类 Unsafe 详解 及其参考链接
[2] 深入分析java线程池的实现原理