使用线程池的风险
死锁:虽然死锁可能发生在任何多线程程序中,但线程池引入了另一种死锁情况,其中所有正在执行的线程都在等待等待队列阻塞线程的结果,因为执行的线程不可用。
简言之:业务线程在占用了线程池内所有的资源后又向线程池提交了新的任务,并且要等这些任务完成后才释放资源,而这些新提交的任务根本就没机会被完成
线程泄漏:如果从池中删除线程以执行任务但在任务完成时未返回线程,则会发生线程泄漏。例如,如果线程抛出异常并且池类没有捕获此异常,则线程将简单地退出,从而将线程池的大小减小一。如果重复多次,则池最终将变为空,并且没有线程可用于执行其他请求。
资源抖动:如果线程池大小非常大,则在线程之间的上下文切换中浪费时间。如上所述,拥有比最佳数量更多的线程可能导致饥饿问题导致资源颠簸。
线程池自己引发的死锁
查看ThreadPoolExecutor的java doc:
工作队列的一个很好的默认选择是SynchronousQueue,它将任务交给线程而不另外保存它们
使用无界队列(例如,没有预定义容量的LinkedBlockingQueue)将导致新任务在所有corePoolSize线程忙时在队列中等待。因此,只会创建corePoolSize线程。 (并且设置的maximumPoolSize的值会失效。)
解决死锁:
方法一:去掉threadPoolExecutor的内部依赖,每一层的future用各自的threadPoolExecutor(各自的线程池使用完后需要shutdown,释放线程池资源)
正在执行的线程都在等待等待队列阻塞线程的结果而死锁的例子,它证明了单个线程池使用不当时也会引发死锁。假设你有一个 ExecutorService
,和之前一样,按照下面的方式运行。
1
2
3
4
5
6
7
8
9
10
|
ExecutorService pool = Executors.newFixedThreadPool( 10 ); pool.submit(() -> { try { log.info( "First" ); pool.submit(() -> log.info( "Second" )).get(); log.info( "Third" ); } catch (InterruptedException | ExecutionException e) { log.error( "Error" , e); } }); |
看起来没什么问题 —— 所有信息按照预期的样子呈现在屏幕上:
1
2
3
|
INFO [pool-1-thread-1]: First INFO [pool-1-thread-2]: Second INFO [pool-1-thread-1]: Third |
注意我们用 get()
阻塞线程,在显示“Third
”之前必须等待内部线程(Runnable
)运行完成。这是个大坑!等待内部任务完成意味着需要从线程池额外获取一个线程来执行任务。然而,我们已经使用到了一个线程,所以内部任务在获取到第二个线程前将一直阻塞。当前我们的线程池足够大,运行没问题。让我们稍微改变一下代码,将线程池缩减到只有一个线程,另外关键的一点是我们移除 get()
方法:
1
2
3
4
5
6
|
ExecutorService pool = Executors.newSingleThreadExecutor(); pool.submit(() -> { log.info( "First" ); pool.submit(() -> log.info( "Second" )); log.info( "Third" ); }); |
代码正常运行,只是有些乱:
1
2
3
|
INFO [pool-1-thread-1]: First INFO [pool-1-thread-1]: Third INFO [pool-1-thread-1]: Second |
两点需要注意:
- 所有代码运行在单个线程上(毫无疑问)
- “Third”信息显示在“Second”之前
顺序的改变完全在预料之内,没有涉及线程间的竞态条件(事实上我们只有一个线程)。仔细分析一下发生了什么:我们向线程池提交了一个新任务(打印“Second
”的任务),但这次我们不需要等待这个任务完成。因为线程池中唯一的线程被打印“First
”和“Third
”的任务占用,所以这个外层任务继续执行,并打印“Third
”。当这个任务完成时,将单个线程释放回线程池,内部任务最终开始执行,并打印“Second
”。那么死锁在哪里?来试试在内部任务里加上 get()
方法:
1
2
3
4
5
6
7
8
9
10
|
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() 方法无限等待,无法获取线程
- 死锁
这是否意味单线程的线程池是不好的?并不是,相同的问题会在任意大小的线程池中出现,只不过是在高负载情况下才会出现,这维护起来更加困难。你在技术层面上可以使用一个无界线程池,但这样太糟糕了。
https://www.breakyizhan.com/java/4965.html
http://www.importnew.com/30277.html