线程池是个好东西,最大线程数限制了服务无限制使用宝贵的操作系统线程,最大队列保护内存溢出,完美!
但是线程池使用不当也会导致死锁。这种死锁,要是不知道原理,死都不知道咋死的,并且非常难定位。大家知道,死锁一般都是由于资源征用引起的。而线程池引起的死锁,可能连个synchronize关键字都没有。连同步都没有,资源争用个毛啊。但是对不起,就是死锁了,线程池罢工了。
笔者第一次踩坑就是一次上线发版前有个线程池的脚本,要跑下线上的一千万条数据,每次跑到二三十万时,就跑不动了,看日志没有任何异常,但就是跑不动了。发布日已过六点,运维同学还等着下班。领导和另一位同事焦头烂额的debug。我低血糖,说去楼下便利店买个叉烧。上楼路上,大脑自动把整个脚本运行了一遍,有一个Task内嵌套Task的结构,两边Task使用的是同一个线程池。我也没想清楚,隐隐觉得这里有问题。吞掉整个叉烧,对同事说,把第二个Task改成串行试试。神奇!死锁消失了。
今天重新思考了这个问题,才算明白这次踩坑的缘由。
先说说线程池是怎么会事。代码就不上了,java1.5+和Spring的大差不差,说破天换到C#,也就是那个意思,还是先说个事吧。
话说,在某个小镇,当地只有三个公务员,负责处理镇上所有公民行政诉求,不论是上户口,登记结婚离婚,房产买卖过户。这个镇上的公民素质非常高,去政府办事一定会排队。有一天,张三去给女儿上户口,如下图所示,他经过漫长的排队,终于到了窗口1开始办事,结果公务员1告诉他,他必须提供房产证明。张三一想,这队排了老半天了,也不能重新排啊。就给老婆打电话,让老婆拿着材料来办理。张三老婆来了,保安说,你要办房产证明,得,在最后面排队吧。张三这边,给公务员一说,老婆去办房产证明了,马上就好。公务员也不急,说,那咱等着吧。站三就和公务员1面对面等起来了。后面排队的素质也真高,都不崔。不巧的是,今天李四/王五也都是给孩子上户口,都把自己老婆叫来排队办房产证明。结果当天,这三个公务员到下班也没办成一件事,大家都傻等了一天。
这个故事告诉我们,公务员是靠不住的,不好意思,拿错台词了,这就是线程饥饿死锁的故事。
三个公务员就是三个线程,排队的老百姓就是任务阻塞队列。当张三和公务员1在那傻等时,就是线程1阻塞了。站三叫自己老婆排队来办证明,就是一个Task中嵌套了新的Task,并且等待这个Task的回调模式。当Task嵌套了Task就有可能造成饥饿死锁。
好了,故事讲完,线程池饥饿死锁的故事就讲完了。结论很简单,当使用future模式时,不要再Task中嵌套Task,否则线程就死给你看。
死锁的代码什么样?下面贴段代码吧,大概就长这样。
1 package com.shlugood.mapp.service;
2
3 import org.junit.Before;
4 import org.junit.Test;
5 import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
6
7 import java.util.ArrayList;
8 import java.util.List;
9 import java.util.concurrent.Callable;
10 import java.util.concurrent.ExecutionException;
11 import java.util.concurrent.Future;
12
13 /**
14 * 饥饿死锁测试
15 */
16 public class HungryDeadLockTest {
17
18
19 @Before
20 public void threadPoolTaskExecutor() {
21 executor = new ThreadPoolTaskExecutor();
22 executor.setCorePoolSize(4);
23 executor.setMaxPoolSize(4);
24 executor.setQueueCapacity(10);
25 executor.setThreadNamePrefix("default_task_executor_thread");
26 executor.initialize();
27 }
28
29 private ThreadPoolTaskExecutor executor;
30
31 @Test
32 public void test() throws ExecutionException, InterruptedException {
33 int loop = 0;
34 while (true) {
35 System.out.println("loop start. loop = " + (loop));
36 innerFutureAndOutFuture();
37 System.out.println("loop end. loop = " + (loop++));
38 Thread.sleep(10);
39 }
40 }
41
42 public void innerFutureAndOutFuture() throws ExecutionException, InterruptedException {
43 Callable<String> innerCallable = new Callable<String>() {
44 @Override
45 public String call() throws Exception {
46 Thread.sleep(100);
47 return "inner callable";
48 }
49 };
50
51 Callable<String> outerCallable = new Callable<String>() {
52 @Override
53 public String call() throws Exception {
54 Thread.sleep(10);
55 Future<String> innerFuture = executor.submit(innerCallable);
56 String innerResult = innerFuture.get();
57 Thread.sleep(10);
58 return "outer callable. inner result = " + innerResult;
59 }
60 };
61
62 List<Future<String>> futures = new ArrayList<>();
63 for(int i = 0; i< 10; i++){
64 System.out.println("submit : " + i);
65 Future<String> outerFuture = executor.submit(outerCallable);
66 futures.add(outerFuture);
67 }
68 for(int i = 0; i < 10; i++){
69 String outerResult = futures.get(i).get();
70 System.out.println(outerResult + ":" + i);
71 }
72 }
73 }
View Code
运行后结果,10个任务都提交了,没有任务返回,显示死锁了。
loop start. loop = 0
submit : 0
submit : 1
submit : 2
submit : 3
submit : 4
submit : 5
submit : 6
submit : 7
submit : 8
submit : 9