线程池是个好东西,最大线程数限制了服务无限制使用宝贵的操作系统线程,最大队列保护内存溢出,完美!
但是线程池使用不当也会导致死锁。这种死锁,要是不知道原理,死都不知道咋死的,并且非常难定位。大家知道,死锁一般都是由于资源征用引起的。而线程池引起的死锁,可能连个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 @Before20 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 @Test32 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 CallableinnerCallable = new Callable () {44 @Override45 public String call() throws Exception {46 Thread.sleep(100);47 return "inner callable";48 }49 };50 51 Callable outerCallable = new Callable () {52 @Override53 public String call() throws Exception {54 Thread.sleep(10);55 Future 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 > futures = new ArrayList<>();63 for(int i = 0; i< 10; i++){64 System.out.println("submit : " + i);65 Future 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 = 0submit : 0submit : 1submit : 2submit : 3submit : 4submit : 5submit : 6submit : 7submit : 8submit : 9