hread-1]: Third
INFO [pool-1-thread-1]: Second
两点需要注意:
- 所有代码运行在单个线程上(毫无疑问)
- “Third”信息显示在“Second”之前
顺序的改变完全在预料之内,没有涉及线程间的竞态条件(事实上我们只有一个线程)。仔细分析一下发生了什么:我们向线程池提交了一个新任务(打印“Second
”的任务),但这次我们不需要等待这个任务完成。因为线程池中唯一的线程被打印“First
”和“Third
”的任务占用,所以这个外层任务继续执行,并打印“Third
”。当这个任务完成时,将单个线程释放回线程池,内部任务最终开始执行,并打印“Second
”。那么死锁在哪里?来试试在内部任务里加上 get()
方法:
ExecutorService pool = Executors.newSingleThreadExecutor();
pool.submit(() -> {
try {
log.info("First");
pool.submit(() -> log.info("Second")).get();
log.info("Third");
} catch (InterruptedException | ExecutionException e) {
log.error("Error", e);
}
});
死锁出现了!我们来一步一步分析:
- 打印“First”的任务被提交到只有一个线程的线程池
- 任务开始执行并打印“First”
- 我们向线程池提交了一个内部任务,来打印“Second”
- 内部任务进入等待任务队列。没有可用线程因为唯一的线程正在被占用
- 我们阻塞住并等待内部任务执行结果。不幸的是,我们等待内部任务的同时也在占用着唯一的可用线程
- get() 方法无限等待,无法获取线程
- 死锁
这是否意味单线程的线程池是不好的?并不是,相同的问题会在任意大小的线程池中出现,只不过是在高负载情况下才会出现,这维护起来更加困难。你在技术层面上可以使用一个无界线程池,但这样太糟糕了。
Reactor/RxJava
请注意,这类问题也会出现在上层库,比如 Reactor
:
Scheduler pool = Schedulers.fromExecutor(Executors.newFixedThreadPool(10));
Mono
.fromRunnable(() -> {
log.info("First");
Mono
.fromRunnable(() -> log.info("Second"))
.subscribeOn(pool)
.block(); //VERY, VERY BAD!
log.info("Third");
})
.subscribeOn(pool);
当你部署代码,它似乎可以正常工作,但很不符合编程习惯。根源的问题是相通的,最后一行的 subscribeOn()
表示外层任务(Runnable
)请求了线程池(pool
)中一个线程,同时,内部任务(Runnable
)也试图获取一个线程。如果把基础的线程池换成只包含单个线程的线程池,会发生死锁。对于 RxJava/Reactor 来说,解决方案很简单——用异步操作替代阻塞操作。
Mono
.fromRunnable(() -> {
log.info("First");
log.info("Third");
})
.then(Mono
.fromRunnable(() -> log.info("Second"))
.subscribeOn(pool))
.subscribeOn(pool)
防患于未然
并没有彻底避免死锁的方法。试图解决问题的技术手段往往会带来死锁风险,比如共享资源和排它锁。如果无法根治死锁(或死锁并不明显,比如使用线程池),还是试着保证代码质量、监控线程池和避免无限阻塞。我很难想象你情愿无限等待程序运行完成,如同 get()
方法和 block()
方法在没有设定超时时间的情况下执行。
感谢阅读!
原文链接:
dzone 翻译:
ImportNew.com -
一杯哈希不加盐
译文链接:
http://www.importnew.com/30277.html
[
转载请保留原文出处、译者和译文链接。]