线程池是个好东西,最大线程数限制了服务无限制使用宝贵的操作系统线程,最大队列保护内存溢出,完美!
但是线程池使用不当也会导致死锁。这种死锁,要是不知道原理,死都不知道咋死的,并且非常难定位。大家知道,死锁一般都是由于资源征用引起的。而线程池引起的死锁,可能连个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 }
运行后结果,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