多线程的异常处理

多线程的异常处理

在线程执行过程中,若需对异常进行额外处理,可借助 Java 提供的UncaughtExceptionHandler机制。这是线程的一个子接口,当未捕获的异常导致线程中断时,JVM 会通过thread.getUncaughtExceptionHandler()查询线程的异常处理器,并将线程和异常作为参数传递给uncaughtException方法。

UncaughtExceptionHandler 核心原理

UncaughtExceptionHandler是一个函数式接口,其核心定义如下:

@FunctionalInterface
public interface UncaughtExceptionHandler {
    // 由JVM通过thread.dispatchUncaughtException调用,处理未捕获的异常
    void uncaughtException(Thread t, Throwable e);
}

异常处理器的传递链

当线程抛出未捕获的异常时,JVM 会按以下顺序查找处理器:

  1. 线程自身的处理器:通过thread.setUncaughtExceptionHandler()设置的实例;
  2. 线程组的处理器:若线程未设置处理器,使用其所属 线程组(ThreadGroup) 的处理器;
  3. 全局默认处理器:若线程组也未设置,使用Thread.setDefaultUncaughtExceptionHandler()设置的全局处理器;
  4. 系统默认行为:若以上均未设置,打印异常堆栈到System.err

线程组(ThreadGroup)默认的异常处理逻辑:

public class ThreadGroup implements Thread.UncaughtExceptionHandler {
    public void uncaughtException(Thread t, Throwable e) {
        if (parent != null) {
            parent.uncaughtException(t, e); // 递归到父线程组
        } else {
            Thread.UncaughtExceptionHandler ueh = 
                Thread.getDefaultUncaughtExceptionHandler();
            if (ueh != null) {
                ueh.uncaughtException(t, e); // 调用全局处理器
            } else if (!(e instanceof ThreadDeath)) {
                e.printStackTrace(); // 打印堆栈
            }
        }
    }
}

自定义异常处理器

通过实现UncaughtExceptionHandler接口,可自定义异常处理逻辑(如日志记录、报警、资源释放等)。

步骤 1:实现自定义处理器

class MyExceptionHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        // 可添加日志记录、监控报警等逻辑
        System.err.println("线程 [" + t.getName() + "] 异常退出:" + e.getMessage());
        // 记录完整堆栈到日志文件
        Logger.getLogger(getClass()).error("线程异常", e);
    }
}

步骤 2:为线程绑定处理器

public class TestExceptionHandler {
    public static void main(String[] args) {
        Thread thread = new Thread(() -> {
            throw new RuntimeException("测试异常");
        });
        
        // 绑定自定义处理器
        thread.setUncaughtExceptionHandler(new MyExceptionHandler());
        thread.start();
    }
}

输出示例

线程 [Thread-0] 异常退出:测试异常
2023-10-15 14:30:45 ERROR TestExceptionHandler:23 - 线程异常
java.lang.RuntimeException: 测试异常
    at TestExceptionHandler.lambda$main$0(TestExceptionHandler.java:10)
    ...

线程池中的异常处理

实际开发中,线程池是更常用的线程管理方式。但线程池中的异常处理与普通线程存在差异:

  • execute(Runnable):异常会直接抛出,可被UncaughtExceptionHandler捕获;
  • submit(Runnable/Callable):异常会被封装在Future中,需主动调用get()触发。

submit 方法异常捕获机制解析

submit方法将任务包装为FutureTask,其run()方法内部捕获了所有异常:

// FutureTask源码片段
public void run() {
    try {
        result = callable.call();
        set(result);
    } catch (Throwable ex) {
        setException(ex); // 存储异常到outcome字段
    }
}

// setException方法将异常信息存储在outcome变量中了,而且将state状态修改为了EXCEPTIONAL
protected void setException(Throwable t) {
    outcome = t; // 保存异常
    state = EXCEPTIONAL; // 标记状态为异常
}

只有调用Future.get()时,异常才会被重新抛出:

public V get() throws ExecutionException {
    if (state == EXCEPTIONAL)
        throw new ExecutionException(outcome); // 包装原始异常
}

线程池异常处理的最佳实践

方式一:任务内部主动捕获异常

在任务逻辑中添加try-catch块:

executorService.submit(() -> {
    try {
        // 业务逻辑
        int result = 1 / 0; // 会触发ArithmeticException
    } catch (Exception e) {
        // 记录业务异常,避免影响其他任务
        logger.error("任务执行失败", e);
    }
});
方式二:重写线程池的afterExecute方法

通过钩子方法统一处理异常:

class CustomThreadPoolExecutor extends ThreadPoolExecutor {
    public CustomThreadPoolExecutor(...) {
        super(...);
    }

    @Override
    protected void afterExecute(Runnable r, Throwable t) {
        super.afterExecute(r, t);
        
        // 处理submit提交的任务异常
        if (t == null && r instanceof Future<?>) {
            try {
                Future<?> future = (Future<?>) r;
                if (future.isDone()) {
                    future.get(); // 触发异常
                }
            } catch (CancellationException ce) {
                // 任务被取消
            } catch (ExecutionException ee) {
                t = ee.getCause(); // 获取原始异常
            } catch (InterruptedException ie) {
                Thread.currentThread().interrupt();
            }
        }
        
        // 统一处理所有异常
        if (t != null) {
            logger.error("线程池任务异常", t);
            // 可添加报警逻辑(如发送邮件、短信)
        }
    }
}
方式三:自定义线程工厂

为线程池中的所有线程设置统一的异常处理器:

ThreadFactory factory = new ThreadFactoryBuilder()
    .setNameFormat("custom-pool-%d")
    .setUncaughtExceptionHandler((t, e) -> {
        logger.error("线程 [{}] 异常: {}", t.getName(), e.getMessage(), e);
    })
    .build();

ExecutorService executor = new ThreadPoolExecutor(
    5, 10, 60L, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(),
    factory
);

全局异常处理器设置

为所有线程设置默认异常处理器:

public static void main(String[] args) {
    // 设置全局异常处理器
    Thread.setDefaultUncaughtExceptionHandler((t, e) -> {
        System.err.println("全局处理器捕获异常:线程 [" + t.getName() + "] " + e.getMessage());
        // 可添加统一日志记录或监控上报
    });

    // 创建未显式设置处理器的线程
    Thread t = new Thread(() -> {
        throw new RuntimeException("全局处理器测试");
    });
    t.start();
}

五、异常处理最佳实践总结

  1. 优先使用线程池:通过ThreadPoolExecutorafterExecute方法统一处理异常;
  2. 任务粒度控制:每个任务应足够独立,避免因单个任务异常导致整个线程池崩溃;
  3. 主动检查 Future 结果:对submit提交的任务,及时调用Future.get()获取结果;
  4. 结合日志与监控:异常发生时,记录完整堆栈信息并触发报警;
  5. 避免静默失败:所有异常都应被明确处理,防止资源泄漏或数据不一致。