写在开头
面试官:“小伙子,线程池使用过吗,来聊一聊它吧!”
我:“好的,然后巴拉巴拉一顿输出之前看过的build哥线程池十八问…”
面试官满意的点了点头,紧接着问道:“那你知道如何优雅的关闭线程池吗?”
我:“知道知道,直接调用shutdownNow()方法就好了呀!”
面试官脸色一变,微怒道:“粗鲁!你给我滚出去!!!”
哈哈,上面的场景是build哥臆想出来的面试画面,我们现在步入正题,来看一看在线程池使用完成后如何优雅的关闭线程池。
在JDK 1.8 中,Java 并发工具包中 java.util.concurrent.ExecutorService 提供了 shutdown()、shutdownNow()这两种接口方法去关闭线程池,我们分别看一下。
shutdown()
public void shutdown() { | |
final ReentrantLock mainLock = this.mainLock; // ThreadPoolExecutor的主锁 | |
mainLock.lock(); // 加锁以确保独占访问 | |
try { | |
checkShutdownAccess(); // 检查是否有关闭的权限 | |
advanceRunState(SHUTDOWN); // 将执行器的状态更新为SHUTDOWN | |
interruptIdleWorkers(); // 中断所有闲置的工作线程 | |
onShutdown(); // ScheduledThreadPoolExecutor中的挂钩方法,可供子类重写以进行额外操作 | |
} finally { | |
mainLock.unlock(); // 无论try块如何退出都要释放锁 | |
} | |
tryTerminate(); // 如果条件允许,尝试终止执行器 | |
} |
在shutdown的源码中,会启动一次顺序关闭,在这次关闭中,执行器不再接受新任务,但会继续处理队列中的已存在任务,当所有任务都完成后,线程池中的线程会逐渐退出。我们写一个小的demo来使用shutdown():
public class TestService{ | |
public static void main(String[] args) { | |
//创建固定 3 个线程的线程池,测试使用,工作中推荐ThreadPoolExecutor | |
ExecutorService threadPool = Executors.newFixedThreadPool(3); | |
//向线程池提交 10 个任务 | |
for (int i = 1; i <= 10; i++) { | |
final int index = i; | |
threadPool.submit(() -> { | |
System.out.println(“正在执行任务 ” + index); | |
//休眠 3 秒,模拟任务执行 | |
try { | |
Thread.sleep(3000); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
}); | |
} | |
//休眠 4 秒 | |
try { | |
Thread.sleep(4000); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
//关闭线程池 | |
threadPool.shutdown(); | |
} | |
} |
在这段测试代码中,我们构造了一个包含固定3线程数的线程池,循环提交10个任务,每个任务休眠3秒,但主程序休眠4秒后,会掉用shutdown方法,理论上,在第二个时间循环中,线程池被停止,所以最多执行完6个任务,但从输出中,我们丝毫感受不好线程何时被停止了。输出:
正在执行任务 1 | |
正在执行任务 3 | |
正在执行任务 2 | |
正在执行任务 4 | |
正在执行任务 5 | |
正在执行任务 6 | |
正在执行任务 7 | |
正在执行任务 8 | |
正在执行任务 9 | |
正在执行任务 10 |
shutdownNow()
/** | |
* 尝试停止所有正在执行的任务,停止处理等待的任务, | |
* 并返回等待处理的任务列表。 | |
* | |
* @return 从未开始执行的任务列表 | |
*/ | |
public List<Runnable> shutdownNow() { | |
List<Runnable> tasks; // 用于存储未执行的任务的列表 | |
final ReentrantLock mainLock = this.mainLock; // ThreadPoolExecutor的主锁 | |
mainLock.lock(); // 加锁以确保独占访问 | |
try { | |
checkShutdownAccess(); // 检查是否有关闭的权限 | |
advanceRunState(STOP); // 将执行器的状态更新为STOP | |
interruptWorkers(); // 中断所有工作线程 | |
tasks = drainQueue(); // 清空队列并将结果放入任务列表中 | |
} finally { | |
mainLock.unlock(); // 无论try块如何退出都要释放锁 | |
} | |
tryTerminate(); // 如果条件允许,尝试终止执行器 | |
return tasks; // 返回队列中未被执行的任务列表 | |
} |
与shutdown不同的是shutdownNow会尝试终止所有的正在执行的任务,清空队列,停止失败会抛出异常,并且返回未被执行的任务列表。由于shutdownNow会有返回值,所以我们将上面的测试案例稍作改动后输出结果为:
这种会在控制台抛出异常的方式,同样也不优雅,所以我们继续往下看!
shutdown()+awaitTermination(long timeout, TimeUnit unit)
awaitTermination(long timeout, TimeUnit unit)是可以允许我们在调用shutdown方法后,再设置一个等待时间,如设置为5秒,则表示shutdown后5秒内线程池彻底终止,返回true,否则返回false;
这种方式里,我们将shutdown()结合awaitTermination(long timeout, TimeUnit unit)方法去使用,注意在调用 awaitTermination() 方法时,应该设置合理的超时时间,以避免程序长时间阻塞而导致性能问题,而且由于这个方法在超时后也会抛出异常,因此,我们在使用的时候要捕获并处理异常!
public class TestService{ | |
public static void main(String[] args) { | |
//创建固定 3 个线程的线程池 | |
ExecutorService threadPool = Executors.newFixedThreadPool(3); | |
//向线程池提交 10 个任务 | |
for (int i = 1; i <= 10; i++) { | |
final int index = i; | |
threadPool.submit(() -> { | |
System.out.println(“正在执行任务 ” + index); | |
//休眠 3 秒 | |
try { | |
Thread.sleep(3000); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
}); | |
} | |
//关闭线程池,设置等待超时时间 3 秒 | |
System.out.println(“设置线程池关闭,等待 3 秒…”); | |
threadPool.shutdown(); | |
try { | |
boolean isTermination = threadPool.awaitTermination(3, TimeUnit.SECONDS); | |
System.out.println(isTermination ? “线程池已停止” : “线程池未停止”); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
//再等待超时时间 20 秒 | |
System.out.println(“再等待 20 秒…”); | |
try { | |
boolean isTermination = threadPool.awaitTermination(20, TimeUnit.SECONDS); | |
System.out.println(isTermination ? “线程池已停止” : “线程池仍未停止,请检查!”); | |
} catch (InterruptedException e) { | |
e.printStackTrace(); | |
} | |
} | |
} |
输出:
设置线程池关闭,等待 3 秒… | |
正在执行任务 1 | |
正在执行任务 2 | |
正在执行任务 3 | |
正在执行任务 4 | |
正在执行任务 5 | |
线程池未停止 | |
再等待 20 秒… | |
正在执行任务 6 | |
正在执行任务 7 | |
正在执行任务 8 | |
正在执行任务 9 | |
正在执行任务 10 | |
线程池已停止 |
从输出中我们可以看到,通过将两种方法结合使用,我们监控了整个线程池关闭的全流程,实现了优雅的关闭!