Loading [MathJax]/jax/output/CommonHTML/config.js
前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >线程池异常处理的 5 中方式

线程池异常处理的 5 中方式

作者头像
FunTester
发布于 2025-01-23 08:26:08
发布于 2025-01-23 08:26:08
13600
代码可运行
举报
文章被收录于专栏:FunTesterFunTester
运行总次数:0
代码可运行

在我进行 Java 编程实践当中,特别是高性能编程时,线程池是无法逾越的高山。在最近攀登高山的路途上,我又双叒叕掌握了一些优雅地使用线程池的技巧。

通常我会将异步任务丢给线程池去处理,不怎么会额外处理异步任务执行中报错。一般来说,任务执行报错,会终止当前的线程,这样线程池会创建新的线程执行下一个任务,当然是在需要创建线程和可以创建新线程的前提下。

在我最近一次实践当中,发现一个定长 20 的线程池,已经创建过上万个线程,这让我大呼不可能。仔细一想,最终也在日志当中确认了大量的异步任务报错。所以不得不让我开始研究如何处理线程池中异步任务的异常了。

以下是我的研究报告,诚邀各位共赏。

就我的水平而言,总计发现 5 种常见的异常处理方式。

try-catch

在提交异步任务之前,通常我们会对异步任务检查异常进行处理,但是对于诸如 java.lang.RuntimeException 的非检查异常不会做更多操作。

当我们提交异步任务的时候,可以增加一个 try-catch 处理的话,就可以完全 hold 住异步任务的可能抛出的异常。

在我的框架设计当中,提交异步任务有且仅一个入口,代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/  
 * 异步执行某个代码块  
 * Java调用需要return,Groovy也不需要,语法兼容  
 *  
 * @param f  
 */  
public static void fun(Closure f) {  
    fun(f, null, true);  
}

/  
 * 使用自定义同步器{@link FunPhaser}进行多线程同步  
 *  
 * @param f      代码块  
 * @param phaser 同步器  
 */  
public static void fun(Closure f, FunPhaser phaser) {  
    fun(f, phaser, true);  
}  
  
/  
 * 使用自定义同步器{@link FunPhaser}进行多线程同步  
 *  
 * @param f  
 * @param phaser  
 * @param log  
 */  
public static void fun(Closure f, FunPhaser phaser, boolean log) {  
    if (phaser != null) phaser.register();  
    ThreadPoolUtil.executeSync(() -> {  
        try {  
            ThreadPoolUtil.executePriority();  //处理高优任务,可忽略
            f.call();  
        } finally {  
            if (phaser != null) {  
                phaser.done();  
                if (log) logger.info("async task {}", phaser.queryTaskNum());  
            }  
        }  
    });  
}

所以改造起来比较简单,只需要在最后的方法中,增加 catch 代码块即可。

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
/  
 * 使用自定义同步器{@link FunPhaser}进行多线程同步  
 * @param f  
 * @param phaser  
 * @param log  
 */  
public static void fun(Closure f, FunPhaser phaser, boolean log) {  
    if (phaser != null) phaser.register();  
    ThreadPoolUtil.executeSync(() -> {  
        try {  
            ThreadPoolUtil.executePriority();  
            f.call();  
        } catch (Exception e) {  
            logger.error("fun error", e);  
        } finally {  
            if (phaser != null) {  
                phaser.done();  
                if (log) logger.info("async task {}", phaser.queryTaskNum());  
            }  
        }  
    });  
}

Callable

Java 中,Callable 是一种可以抛出受检异常(Checked Exception)的任务接口。这与 Runnable 的不同之处在于,Callable 能够返回结果,并允许在任务执行过程中抛出异常。异常处理通常在获取任务结果时完成,以下是一些常见的处理方式。

Callable 异常处理的特点:

  1. 异常机制:
    • Callable 方法签名为 V call() throws Exception,允许直接抛出 Exception 或其子类。
    • 提交到线程池的 Callable 任务,如果抛出异常,会被封装到 ExecutionException 中。
  2. 获取异常:
    • 通过 Future.get() 获取结果时,若任务抛出异常,则会引发 ExecutionException
    • 开发者需要在调用 get() 时捕获和处理 ExecutionException

演示代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import java.util.concurrent.*;

public class CallableExceptionExample {
    public static void main(String[] args) {
        ExecutorService executor = Executors.newSingleThreadExecutor();

        Callable<String> task = () -> {
            if (true) {  // 模拟任务中出错
                throw new Exception("Simulated Exception");
            }
            return "Task Completed";
        };
        Future<String> future = executor.submit(task);
        try {
            // 获取任务结果,可能抛出 ExecutionException
            String result = future.get();
            System.out.println(result);
        } catch (ExecutionException e) {
            System.out.println("Caught an ExecutionException: " + e.getCause().getMessage());
        } catch (InterruptedException e) {
            System.out.println("Task was interrupted");
            Thread.currentThread().interrupt();  // 恢复中断状态
        }
    }
}

控制台输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Caught an ExecutionException: Simulated Exception

Callable 抛出异常时,线程池会捕获这个异常,并将其封装在 ExecutionException 中。如果任务在执行过程中被中断,会抛出 InterruptedException。建议在捕获时恢复线程的中断状态,以避免吞掉中断信号。

afterExecute()

在 Java 中,afterExecute()ThreadPoolExecutor 提供的一个钩子方法,允许开发者在每个任务执行完成后执行一些额外的逻辑。它可以用来捕获线程池任务中抛出的运行时异常和其他异常,从而进行集中处理或记录。

afterExecute() 的作用:

  1. 触发时机:
    • 每当线程池中某个任务完成后,无论是正常完成还是抛出异常,都会调用 afterExecute()
    • 默认实现为空,用户可以重写它以添加自定义行为。
  2. 异常捕获:
    • 如果任务在执行过程中抛出异常(例如 RuntimeExceptionError),它会作为参数传递给 afterExecute()Throwable 参数。
    • 需要手动从 Future 中获取异常,或者在异常处理逻辑中记录。

演示案例如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import java.util.concurrent.*;

public class AfterExecuteExample {
    public static void main(String[] args) {
        ThreadPoolExecutor executor = new CustomThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>());

        // 提交一个会抛出异常的任务
        executor.submit(() -> {
            System.out.println("Task started");
            throw new RuntimeException("Task failed with exception");
        });

        executor.shutdown();
    }

    static class CustomThreadPoolExecutor extends ThreadPoolExecutor {
        public CustomThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
            super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
        }

        @Override
        protected void afterExecute(Runnable r, Throwable t) {
            super.afterExecute(r, t); // 调用父类实现以确保正常行为

            // 处理任务异常
            if (t != null) {
                System.err.println("Task threw an exception: " + t.getMessage());
            } else if (r instanceof Future<?>) {
                try {
                    ((Future<?>) r).get(); // 获取任务执行结果或捕获异常
                } catch (CancellationException e) {
                    System.err.println("Task was cancelled");
                } catch (ExecutionException e) {
                    System.err.println("Task threw an exception: " + e.getCause().getMessage());
                }
            }
        }
    }
}

最终控制台输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Task started
Task threw an exception: Task failed with exception

如果需要在任务开始和结束时都执行逻辑,可以同时重写 beforeExecute()afterExecute()。重写此方法时,建议注意线程中断信号的恢复,并确保异常记录逻辑不会引发额外的错误。

自定义 ThreadFactory

在 Java 中,如果需要自定义线程的异常处理行为,可以通过 自定义 ThreadFactory 创建线程并设置异常处理策略。线程的异常处理主要依赖于 Thread.UncaughtExceptionHandler 接口,该接口用于处理线程运行时未捕获的异常。

步骤概览:

  1. 创建自定义 ThreadFactory
    • 实现 ThreadFactory 接口,定制线程的创建逻辑。
    • 在创建线程时,设置自定义的 UncaughtExceptionHandler
  2. 实现异常处理逻辑:
    • 使用 java.lang.Thread#setUncaughtExceptionHandler,定义异常处理行为(如日志记录、发送警报等)。
  3. 将自定义 ThreadFactory 应用于线程池:
    • 在创建线程池时,通过 ExecutorsThreadPoolExecutor 使用自定义的 ThreadFactory
代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
import java.util.concurrent.*;

public class CustomThreadFactoryExample {

    public static void main(String[] args) {
        // 使用自定义线程工厂创建线程池
        ExecutorService executor = Executors.newFixedThreadPool(2, new CustomThreadFactory());

        // 提交任务
        executor.submit(() -> {
            System.out.println("Task 1 started");
            throw new RuntimeException("Task 1 encountered an error");
        });

        executor.submit(() -> System.out.println("Task 2 completed"));

        executor.shutdown();
    }

    // 自定义 ThreadFactory 实现
    static class CustomThreadFactory implements ThreadFactory {
        private int threadId = 0;

        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setName("CustomThread-" + threadId++);
            thread.setUncaughtExceptionHandler(new CustomExceptionHandler());
            return thread;
        }
    }

    // 自定义异常处理器
    static class CustomExceptionHandler implements Thread.UncaughtExceptionHandler {
        @Override
        public void uncaughtException(Thread t, Throwable e) {
            System.err.println("Thread " + t.getName() + " threw an exception: " + e.getMessage());
            // 这里可以添加更多处理逻辑,例如日志记录或警报通知
        }
    }
}

控制台输出:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Task 1 started
Thread CustomThread-0 threw an exception: Task 1 encountered an error
Task 2 completed

使用 Executors.newFixedThreadPool() 并传入自定义的 ThreadFactory,让线程池中的每个线程具备统一的异常处理行为。也可以通过线程标识为每个线程设置了自定义的 UncaughtExceptionHandler

全局异常处理

在 Java 中,**Thread.setDefaultUncaughtExceptionHandler** 是一个全局异常处理机制,用于处理所有未被捕获的线程异常。与每个线程单独设置的 Thread.setUncaughtExceptionHandler 不同,setDefaultUncaughtExceptionHandler 提供了一个全局级别的异常处理器,适用于所有线程(除非线程单独设置了自己的处理器)。

Thread.setDefaultUncaughtExceptionHandler 方法作用:

  • 用于设置一个全局的默认未捕获异常处理器。
  • 如果某个线程未显式设置自己的 UncaughtExceptionHandler,则会使用这个默认处理器。
  • 通常用于记录日志、发送报警等全局异常处理逻辑。

演示代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class DefaultExceptionHandlerExample {

    public static void main(String[] args) {
        // 设置全局默认异常处理器
        Thread.setDefaultUncaughtExceptionHandler((thread, throwable) -> {
            System.err.println("Unhandled exception in thread: " + thread.getName());
            System.err.println("Exception: " + throwable.getMessage());
            throwable.printStackTrace();
        });

        // 创建一个线程,抛出未捕获的异常
        Thread thread1 = new Thread(() -> {
            throw new RuntimeException("Thread 1 failed!");
        });
        thread1.start();

        // 创建另一个线程,也抛出未捕获的异常
        Thread thread2 = new Thread(() -> {
            throw new RuntimeException("Thread 2 encountered an error!");
        });
        thread2.start();
    }
}

控制台如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
Unhandled exception in thread: Thread-0
Exception: Thread 1 failed!
java.lang.RuntimeException: Thread 1 failed!
    at ...

Unhandled exception in thread: Thread-1
Exception: Thread 2 encountered an error!
java.lang.RuntimeException: Thread 2 encountered an error!
    at ...

全局异常处理只针对主线程和未显式设置 UncaughtExceptionHandler 的其他线程生效。

异常处理的优先级:

  • 如果线程显式设置了自己的 UncaughtExceptionHandler(通过 thread.setUncaughtExceptionHandler),那么会优先调用该处理器。
  • 如果线程未设置单独的处理器,则调用全局默认处理器。
  • 如果没有设置全局默认处理器,未捕获的异常将打印到标准错误输出流。

如果主线程抛出异常,Thread.setDefaultUncaughtExceptionHandler 无法捕获它。需要在 main 方法中显式处理。如果使用线程池(如 ExecutorService),未捕获的异常通常会被封装为 ExecutionException,不会触发默认处理器。需要使用 Future.get() 或重写 afterExecute() 来处理线程池的任务异常。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2024-12-16,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 FunTester 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
Redis最基础内容
String类型,也就是字符串类型,是Redis中最简单的存储类型。其value是字符串,不过根据字符串的格式不同,又可以分为3类:
ha_lydms
2023/08/10
2491
Redis最基础内容
深入Redis数据结构和底层原理
其中一个重要的原因,就是Redis中高效的数据结构,因此我们就专门的来研究下Redis的核心数据结构,Go!
闫同学
2022/10/31
3590
深入Redis数据结构和底层原理
Redis 数据类型
string是redis最基本的类型,一个key对应一个value string类型是二进制安全的,即它可以包含任何数据
全栈程序员站长
2022/07/25
3300
常用五大数据类型
命令大小写都可以,如果你只想单纯看 API,不想看例子,请移到最下面的 指令总结。
用户9615083
2022/12/25
8470
常用五大数据类型
redis缓存数据库
redis 介绍 redis是业界主流的key-value nosql 数据库之一。和Memcached类似,它支持存储的value类型相对更多,包括string(字符串)、list(链表)、set(集合)、zset(sorted set --有序集合)和hash(哈希类型)。这些数据类型都支持push/pop、add/remove及取交集并集和差集及更丰富的操作,而且这些操作都是原子性的。在此基础上,redis支持各种不同方式的排序。与memcached一样,为了保证效率,数据都是缓存在内存中。区别的是r
用户1679793
2018/07/05
4.5K0
Redis 数据类型
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
子润先生
2021/07/05
5130
Redis 数据类型
Redis00--Redis的基本命令
Redis hash 是一个键值对集合,Redis hash是一个String类型的field和value的映射表,hash特别适合存储对象,例如SpringSession中的session信息,存储用户信息,用户主页访问量等等。
码农飞哥
2021/08/18
3590
Redis-1.Redis数据结构
自增自减命令 自增自减命令只能作用于整数,如果对不存在的键或者保存了空串的键执行自增/自减操作,那么会将这个键的值当作0处理,如果对无法解释为整数或者浮点数的字符串值性自增/自减操作,把额会返回一个错误。
悠扬前奏
2019/05/30
7110
Redis笔记(二):Redis数据类型
Redis支持五种数据类型:string(字符串),hash(哈希),list(列表),set(集合)及zset(sorted set:有序集合)。
朝雨忆轻尘
2019/06/19
6240
Redis常用命令、5种数据类型的内部编码实现以及实用场景
相信绝大部分人,应该是99%的人都知道Redis的5种的基本类型、它们分别是:字符串、哈希、列表、集合、有序集合,就如同下图这样:
Java学习录
2019/05/06
5120
Redis常用命令、5种数据类型的内部编码实现以及实用场景
简简单单入个Redis的门
Redis是一种key-value的存储系统,它是一种nosql(Not Only [SQL])非关系型的数据库,它支持string(字符串)、list(链表)、set(集合)、hash(哈希类型)和zset(sorted set --有序集合)数据类型,这些数据类型有着丰富的操作,且均具有原子性。
code随笔
2020/11/06
3770
Redis 简介
REmote DIctionary Server(Redis) 是一个由 Salvatore Sanfilippo 开发的 key-value 存储系统。
acc8226
2022/05/17
3380
Redis 简介
Redis常用命令
推荐这个可视化工具,GitHub地址:https://github.com/qishibo/AnotherRedisDesktopManager/
乐心湖
2021/01/18
8510
Redis常用命令
一文读懂Redis数据类型
也就是说redis的存储类型 key(string) -- value(string、hash、list、set、zet)
Devops海洋的渔夫
2022/01/17
4290
一文读懂Redis数据类型
Redis相关命令
什么是Redis   Redis首先是一个存储数据库,数据在缓存在内存中,数据是K-V结构。 Redis的使用 Redis安装使用 Redis的数据类型 类型 描述 备注 string 字符串 K-V 最大值存储512M list 简单字符串列表,可以将元素添加最左边或者右边 最多存储232 - 1 set string类型的无序集合 Hash表实现,查询效率O(1),最多存储232 - 1 zset 有序集合,成员不能重复,但是scope可以重复 image.png hash 键值对的集合 image.p
OPice
2020/01/15
8040
redis基础指令及数据类型
全局指令 redis有5种数据类型,它们是键值对中的值,对于键来说有些通用的命令。这里称之为全局指令。 set 创建一个键值对
小手冰凉
2020/07/14
3650
Redis基础知识(一)
Redis 是一个开源的使用 ANSI C 语言编写、遵守 BSD 协议、支持网络、可基于内存亦可持久化的日志型、Key-Value 数据库,并提供多种语言的 API。 它通常被称为数据结构服务器,因为值(value)可以是 字符串(String), 哈希(Map),列表(list),集合(sets)和 有序集合(sorted sets)等类型。
没有故事的陈师傅
2019/12/11
1.4K0
Redis 常用操作命令,非常详细!
nx:如果key不存在则建立,xx:如果key存在则修改其值,也可以直接使用setnx/setex命令。
Java技术栈
2018/11/30
3K0
Redis(2):常用命令详解
redis命令不区分大小写 通用命令: 1. 获得符合规则的键名列表: keys pattern    其中pattern符合glob风格  ? (一个字符) * (任意个字符) [] (匹配其中的
SecondWorld
2018/03/14
1.1K0
Redis基础(超详解)一 :Redis定义、SQL与NoSQL区别、Redis常用命令、Redi五种数据类型String、List、Set、Hash、ZSet
Redis诞生于2009年,全称是Remote Dictionary Server,远程词典服务器,是一个开源、基于内存的键值型NoSQL数据库。
寻求出路的程序媛
2024/07/03
9610
Redis基础(超详解)一 :Redis定义、SQL与NoSQL区别、Redis常用命令、Redi五种数据类型String、List、Set、Hash、ZSet
相关推荐
Redis最基础内容
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
本文部分代码块支持一键运行,欢迎体验
本文部分代码块支持一键运行,欢迎体验