线程池中的任务抛出了异常会怎样? 控制台打印异常可就错了

我以前对线程的理解是:

线程池中的线程池分为核心线程和非核心线程,核心线程在完成后会被回收(丢掉其中的任务,继续执行别的任务) ,非核心线程空闲超过允许时间后就被杀死,线程中抛出的异常都会被直接打印在控制台上

现在看来:

所有线程在执行完成后都会在存储线程的HashSet中被removed,但线程池会维持corePoolSize条线程存活持续等待任务。当任务队列满了,但活跃线程数小于maximumPoolSize,线程池就会去创建额外的线程去执行任务。当任务队列空了,线程池只会保留corePoolSize条线程,额外线程就会被杀死。
submit提交给线程的任务的异常只有在get是才能获取到,execute则会直接打印在控制台上。

前言

在项目开发中,使用多线程,将实现了Callable的类的的任务提交给线程池后,在get结果时才打印异常,觉得很奇怪,理论上应该在控制台上打印异常才对,后来在网上搜索后才了解到原因,给线程池提交任务分为submitexecute方法, 它们对于异常的获取是有区别的。

验证代码

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());
    }
}

观看执行结果,可以看出

  1. excute提交的任务打印了异常
  2. 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没有呢?

原因

executesubmit的区别

知道区别有助于更好的找到原因:

  1. execute没有返回值。可以执行任务,但无法判断任务是否成功完成。一般提交实现Runnable接口的任务。
  2. 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,所以我们定位到FutureTaskrun等相关方法:

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时才获取的原因也清楚了。

总结和思考

总结

简而言之:

  1. execute提交的任务在Worker#run中被捕获后直接抛出。
  2. submit提交的任务在FutureTask#run中被捕获后存储到了属性中,get时才会抛出异常。

思考

为什么submit提交的任务要get时才会获取结果?

  1. Callable提交的任务,我们只需要关系其结果即可,不用关系其过程,主线程只需要在处理完自己的任务后获取一个或一批任务的状态和结果,让开发者决定何时检查任务是否成功完成,以及如何处理异常。
  2. Runnable我们一般不要获取返回值,只需要提交任务即可,如果不直接抛出异常就会导致任务失败了但是没有感知,尤其是在任务中catch遗漏的一些没有预料到异常且未设置UncaughtExceptionHandler的时候。
  3. 虽然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 中使用其他编程语言编写的方法。

所以stateOffsetstate属性在内存地址相对于此对象的内存地址的偏移量。

Java 复制代码
Class<?> k = FutureTask.class;  
stateOffset = UNSAFE.objectFieldOffset  
(k.getDeclaredField("state"));

详情可见 Java 魔法类 Unsafe 详解 及其参考链接。

参考链接:

[1] Java 魔法类 Unsafe 详解 及其参考链接

[2] 深入分析java线程池的实现原理

相关推荐
LCG元1 小时前
【面试问题】JIT 是什么?和 JVM 什么关系?
面试·职场和发展
向前看-2 小时前
验证码机制
前端·后端
xlsw_2 小时前
java全栈day20--Web后端实战(Mybatis基础2)
java·开发语言·mybatis
神仙别闹3 小时前
基于java的改良版超级玛丽小游戏
java
黄油饼卷咖喱鸡就味增汤拌孜然羊肉炒饭3 小时前
SpringBoot如何实现缓存预热?
java·spring boot·spring·缓存·程序员
暮湫3 小时前
泛型(2)
java
超爱吃士力架4 小时前
邀请逻辑
java·linux·后端
南宫生4 小时前
力扣-图论-17【算法学习day.67】
java·学习·算法·leetcode·图论
转码的小石4 小时前
12/21java基础
java
李小白664 小时前
Spring MVC(上)
java·spring·mvc