线程池是个好东西,最大线程数限制了服务无限制使用宝贵的操作系统线程,最大队列保护内存溢出,完美!

但是线程池使用不当也会导致死锁。这种死锁,要是不知道原理,死都不知道咋死的,并且非常难定位。大家知道,死锁一般都是由于资源征用引起的。而线程池引起的死锁,可能连个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

 

 posted on 2018-08-11 23:23  谷堆曲线  阅读(4203)  评论(3编辑  收藏  举报