背景

419大促值班的时候,遇到一个生产的OSP(VIP的RPC框架)的框架异常,意思大致是某个RPC服务接口出现方法级线程池爆满,而通过定位,发现该接口是定时任务每隔半个小时触发一次。初步判断是这个方法可能是某个地方组塞住了,然后每隔半个小时创建新线程执行该方法的时候,由于之前的线程都没有运行完,所以线程一直在阻塞着,导致该方法的线程池爆满。

定位

log

出问题之后,第一时间就是看log,从异常栈看到是RPC的某个方法,出现这个方法级线程池爆满。而日志里面让我耳目一新的是 **Thread dump by ThreadDumpper for rejected by method level business ** ,然后接下来的是一些异常栈信息,就是把当前存活的线程的线程栈打出来,把每个存活的线程都状态,线程栈信息全打出来,类似

"thread-dump-0" Id=13 WAITING
	at java.lang.Object.wait(Native Method)
	at java.util.concurrent.ForkJoinTask.externalAwaitDone(ForkJoinTask.java:334)
	at java.util.concurrent.ForkJoinTask.doInvoke(ForkJoinTask.java:405)
	at java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:734)
	at java.util.stream.ReduceOps$ReduceOp.evaluateParallel(ReduceOps.java:714)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233)
	at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
	at com.kevin.vjtool.StaticWithLmdUtils.<clinit>(StaticWithLmdUtils.java:20)
	...

也就是在这个ThreadDumpper做的thread dump让我们能快速定位到生产的这个问题。为了说明下这个问题,我写了测试代码还原线上问题。

还原线上问题

这个问题其实很简单,就是建立个线程池,然后开满线程去跑一个会引起线程阻塞的方法,等到线程池用满之后,再来一个线程就会触发reject抛异常,抛异常的时候再将当前存活的线程的线程栈打出来。

会引起阻塞/死锁的操作

是的,这份代码会触发死锁,后面再解释

public class StaticWithLmdUtils {

    static {
        System.out.println("begin init");
        List<Integer> nums = Lists.newArrayList(1, 2, 3);
        List<Object> collect = nums.parallelStream().map(e -> e + 100).collect(Collectors.toList());
        System.out.println("end init");
    }

    public static void test() {
        System.out.println("hello");
    }
}

模拟多个线程耗尽线程池

public class ThreadDumpMain {

    public static void main(String[] args) {
        ThreadFactory push2CDNFactory = new ThreadFactoryBuilder().setNameFormat("thread-dump-%d").build();
        ExecutorService push2CDNPool = new ThreadPoolExecutor(72, 72, 0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(1), push2CDNFactory, new ThreadPoolExecutor.AbortPolicy());
        try {
            for (int i = 0; i < 160; i++) {
                push2CDNPool.submit(new Thread(() -> {
                    StaticWithLmdUtils.test();
                }));
            }
        } catch (Exception e) {
        	 // com.vip.vjtools.vjkit.concurrent. ThreadDumpper  from vjtool
            ThreadDumpper dumpper = new ThreadDumpper(20, 1000 * 60 * 10);
            dumpper.tryThreadDump();
        }
    }
}

解释:采用一个固定大小的线程池,然后创建多个线程扔到线程池跑,到满了之后,触发vjtool的ThreadDumpper进行将当前存活的线程栈打出来,通过观察线程栈信息来定位问题。

运行结果

省略了一部分,把重要的抽取出来,实际上使用需要自己要仔细观察所有的线程栈


"thread-dump-41" Id=98 RUNNABLE
	at com.kevin.vjtool.ThreadDumpMain.lambda$main$0(ThreadDumpMain.java:24)
	at com.kevin.vjtool.ThreadDumpMain$$Lambda$1/75457651.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:745)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
	at java.util.concurrent.FutureTask.run(FutureTask.java)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	...

"ForkJoinPool.commonPool-worker-3" Id=84 WAITING
	at sun.misc.Unsafe.park(Native Method)
	at java.util.concurrent.ForkJoinPool.awaitWork(ForkJoinPool.java:1824)
	at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1693)
	at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:157)
	...
	
"thread-dump-0" Id=13 WAITING
	at java.lang.Object.wait(Native Method)
	at java.util.concurrent.ForkJoinTask.externalAwaitDone(ForkJoinTask.java:334)
	at java.util.concurrent.ForkJoinTask.doInvoke(ForkJoinTask.java:405)
	at java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:734)
	at java.util.stream.ReduceOps$ReduceOp.evaluateParallel(ReduceOps.java:714)
	at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233)
	at java.util.stream.ReferencePipeline.collect(ReferencePipeline.java:499)
	at com.kevin.vjtool.StaticWithLmdUtils.<clinit>(StaticWithLmdUtils.java:20)
	...

"thread-dump-29" Id=71 RUNNABLE
	at com.kevin.vjtool.ThreadDumpMain.lambda$main$0(ThreadDumpMain.java:24)
	at com.kevin.vjtool.ThreadDumpMain$$Lambda$1/75457651.run(Unknown Source)
	at java.lang.Thread.run(Thread.java:745)
	at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:511)
	at java.util.concurrent.FutureTask.run$$$capture(FutureTask.java:266)
	at java.util.concurrent.FutureTask.run(FutureTask.java)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1142)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:617)
	...

分析

这里我们看到一个非常奇怪的地方就是WAITING状态,名字Wiethread-dump-0线程的线程栈,在StaticWithLmdUtils.java:20,类的第20行的代码非常简单

List<Object> collect = nums.parallelStream().map(e -> e + 100).collect(Collectors.toList());

为什么这个会一直在WAITING,一个很普通的操作。反复运行几次还是一样的问题,于是上Stack Overflow查了下,发现是java的一个不算bug的bug,详情见 https://stackoverflow.com/questions/34820066/why-does-parallel-stream-with-lambda-in-static-initializer-cause-a-deadlock ,简单讲就是在静态代码块初始化的时候使用到parallelStream会引起deadlock,然后别的线程调用到该类的方法时就会阻塞住。深层次的原因,我们组几个小伙伴看了大半天官方的说明还是没了解,有兴趣的可以深入看看(个人理解为是parallelStream会发起多个线程并行执行与当前线程有资源竞争导致)。至于解决方案就比较简单了,改为使用strem,不用parallelStream带来的死锁。

ThreadDumper

在这次问题的定位中,ThreadDumper发挥了极大的作用,特别是在线程池爆满这种异常情况发生时,将当时的线程快照记录起来,快速帮助定位问题。
说下ThreadDumper使用
ThreadDumper的源码中有个有参构造,第一个参数是打印出栈的层次,第二个是间隔时间,层次默认是8,为了避免过多的输出,我个人觉得在不是很损耗性能情况下,把层次打得多点,有利于查看源头。至于间隔时间timeIntervalLimiter,为了避免过度频繁打印带来JVM的停顿而作的保护,其实现是通过一个AtomicLong来记录上一次触发的时间。而当前存活的线程的线程栈是通过Map<Thread, StackTraceElement[]> threads = Thread.getAllStackTraces();来获取。