异步线程异常逃逸

背景

某业务场景由异步 + 消息链路保证一定执行成功。

预发消息链路执行次数增加,可以推测出异步链路执行肯定失败了,但是经过排查,并没有看到相关的报错日志。只能直接看部署分支的代码,排查下来发现异步代码中存在NPE隐患。修复隐患后消息链路执行次数恢复正常,问题暂时解决,但为什么报错了没有日志?

UncaughtExceptionHandler

直接上结论,由于异步线程池没有配置 UncaughtExceptionHandler,当异步线程执行过程中发生异常时将不会被捕获,造成异常逃逸现象。

    /**
     * Set the handler invoked when this thread abruptly terminates
     * due to an uncaught exception.
     * <p>A thread can take full control of how it responds to uncaught
     * exceptions by having its uncaught exception handler explicitly set.
     * If no such handler is set then the thread's <tt>ThreadGroup</tt>
     * object acts as its handler.
     * @param eh the object to use as this thread's uncaught exception
     * handler. If <tt>null</tt> then this thread has no explicit handler.
     * @throws  SecurityException  if the current thread is not allowed to
     *          modify this thread.
     * @see #setDefaultUncaughtExceptionHandler
     * @see ThreadGroup#uncaughtException
     * @since 1.5
     */
    public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {
        checkAccess();
        uncaughtExceptionHandler = eh;
    }

异步线程池初始化

在初始化异步线程池的时候指定 UncaughtExceptionHandler,做好异常处理和日志埋点。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;

public class CustomThreadPoolUtil {

    /**
     * 默认存活时间
     */
    private static final long DEFAULT_KEEP_ALIVE_TIME = 5000L;

    /**
     * 创建线程池,捕获了异步线程中发生的异常,使用方可配置monitor告警及时发现异常
     *
     * @param coreSize   核心线程数
     * @param queueSize  阻塞队列大小
     * @param threadName 线程池名
     * @param handler    拒绝策略
     * @return ThreadPoolExecutor
     */
    public static ThreadPoolExecutor newFixedThreadPoolWithThreadName(int coreSize, int queueSize, String threadName, RejectedExecutionHandler handler) {
        ThreadFactory threadFactory = createThreadFactor(threadName);
        ArrayBlockingQueue<Runnable> blockingQueue = new ArrayBlockingQueue<>(queueSize, true);
        if (handler != null) {
            return new ThreadPoolExecutor(coreSize, coreSize, DEFAULT_KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS, blockingQueue, threadFactory, handler);
        } else {
            return new ThreadPoolExecutor(coreSize, coreSize, DEFAULT_KEEP_ALIVE_TIME, TimeUnit.MILLISECONDS, blockingQueue, threadFactory);
        }
    }

    public static ThreadFactory createThreadFactor(String threadName) {
        if (threadName == null || threadName.length() == 0) {
            throw new IllegalArgumentException("thread name is blank");
        }

        return new ThreadFactory() {

            // 线程计数器
            private final AtomicLong threadNumber = new AtomicLong(0);

            @Override
            public Thread newThread(Runnable runnable) {
                Thread thread = new Thread(runnable, threadName + "_" + threadNumber.getAndIncrement());
                if (thread.isDaemon()) {
                    thread.setDaemon(false);
                }
                if (thread.getPriority() != Thread.NORM_PRIORITY) {
                    thread.setPriority(Thread.NORM_PRIORITY);
                }
                thread.setUncaughtExceptionHandler((t, e) -> {
                    // TODO log and alarm
                    System.out.println("threadName:" + Thread.currentThread().getName() + " This is an alarm!!! custom thread pool execute exception, exp:" + e.getMessage());
                });
                return thread;
            }
        };
    }
}

使用对比

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CustomThreadPoolTest {

    private static final ThreadPoolExecutor DEFAULT_THREAD_POOL = new ThreadPoolExecutor(
            4,
            4,
            1,
            TimeUnit.SECONDS,
            new ArrayBlockingQueue<>(100, true),
            Executors.defaultThreadFactory(),
            new ThreadPoolExecutor.AbortPolicy());

    private static final ThreadPoolExecutor CUSTOM_THREAD_POOL = CustomThreadPoolUtil.newFixedThreadPoolWithThreadName(
            4,
            100,
            "senbo_thread",
            null);

    public static void main(String[] args) {
        asyncExecuteByDefaultThreadPool();
        asyncExecuteByCustomThreadPool();
        System.out.println("main finished.");
    }

    private static void asyncExecuteByDefaultThreadPool() {
        try {
            DEFAULT_THREAD_POOL.execute(() -> {
                // 未设置 UncaughtExceptionHandler,将打印ThreadGroup
                System.out.println("threadName:" + Thread.currentThread().getName() + " UncaughtExceptionHandler:" + Thread.currentThread().getUncaughtExceptionHandler().getClass().getName());;
                String str = "1x";
                System.out.println(Integer.valueOf(str));
                System.out.println("default thread pool execute success");
            });
        } catch (Exception e) {
            System.out.println("default thread pool execute exception:" + e.getMessage());
        }
    }

    private static void asyncExecuteByCustomThreadPool() {
        try {
            CUSTOM_THREAD_POOL.execute(() -> {
                System.out.println("threadName:" + Thread.currentThread().getName() + " UncaughtExceptionHandler:" + Thread.currentThread().getUncaughtExceptionHandler().getClass().getName());;
                String str = "1x";
                System.out.println(Integer.valueOf(str));
                System.out.println("custom thread pool execute success");
            });
        } catch (Exception e) {
            System.out.println("custom thread pool execute exception:" + e.getMessage() + ", "+ e.getLocalizedMessage());
        }
    }
}

打印日志如下,可以看到 default异步线程池没有捕获到线程内部发生的异常,而 custom异步线程池捕获到了异常,并打印了相印的告警日志。

threadName:pool-1-thread-1 UncaughtExceptionHandler:java.lang.ThreadGroup
main finished.
threadName:senbo_thread_0 UncaughtExceptionHandler:thread.CustomThreadPoolUtil$1$$Lambda$3/1831932724
threadName:senbo_thread_0 This is an alarm!!! custom thread pool execute exception, exp:For input string: "1x"
Exception in thread "pool-1-thread-1" java.lang.NumberFormatException: For input string: "1x"
	at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)
	at java.lang.Integer.parseInt(Integer.java:580)
	at java.lang.Integer.valueOf(Integer.java:766)
	at thread.CustomThreadPoolTest.lambda$asyncExecuteByDefaultThreadPool$0(CustomThreadPoolTest.java:43)
	at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1149)
	at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:624)
	at java.lang.Thread.run(Thread.java:750)
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值