Java 线程池

相信在实际工作中,大家对于线程池的使用并不陌生,例如以下几个应用场景:

  • 支付成功之后,异步发送短信通知用户;

  • 公司的OA系统中,提交某些申请之后,异步发送给各个部门负责人进行审批;

  • 请求某个接口时,需要做些日志上报之类的记录。

线程池的使用

下边是一个非常简单的线程池使用案例:

public class ThreadExecutorDemo {

    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                1,
                1,
                1,
                TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000));
        threadPoolExecutor.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("this is submitTask");
            }
        });
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println("this is executeTask");
            }
        });
    }
}

通过这段简单的案例代码我们可以知道,线程池常用接口有两类,一种叫 submit,一种叫 execute。这两种方法在执行的时候都不会阻塞到调用线程自身,都能达到异步的效果,但是它们在实现效果方面却有所不同。下边让我们对这两种不同接口做了一些比对,大致如下表所示:

方法名字 返回内容 异常处理情况 是否可以实现异步效果
execute 如果出现异常,并且没有加 try-catch 块,线程池会将异常给吞掉。 可以
submit future对象 如果出现异常,会将异常自动抛出 可以

线程池在实际工作中,可以运用的业务场景非常多,下边我列举了几个非常实用的应用场景和大家分享。

用户注册场景

用户注册之后,当用户信息写入到数据库之后,通过线程池异步发送一条消息通知给到用户身上。

public class UserRegistryThreadExecutorDemo {

    public static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            1,
            1,
            1,
            TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000));


    /**
* 用户注册
*/
public static void userRegistry() {
        //插入用户信息
        addUserInfo();
        //异步发送消息通知
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                sendNotifyMsg();
            }
        });
        System.out.println("用户注册操作流程结束");
    }

    /**
* 模拟信息插入动作
*/
private static void addUserInfo() {
        System.out.println("插入用户信息");
    }

    /**
* 发送通知信息
*/
private static void sendNotifyMsg() {
        System.out.println("发送消息通知开始");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("发送消息通知结束");
    }


    public static void main(String[] args) {
        userRegistry();
    }
}

由于发送消息通知这个操作的过程耗时会比较高,并且其发送的结果不需要百分百返回给到用户那边,那么这个发送的操作就可以交给线程池去处理。

审核场景

在审核场景中,用户点击提交按钮之后,会将审核记录插入到对应的表中,然后通过异步线程发送给到各个部门的负责人。

public class AuditExecutorDemo {

    public static final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
            1,
            1,
            1,
            TimeUnit.SECONDS, new ArrayBlockingQueue<>(1000));


    /**
* 执行审核操作
*/
public static boolean doAudit() {
        createAuditRecord();
        threadPoolExecutor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    Thread.sleep(1000);
                    System.out.println("发送审核记录给到A部门负责人");
                    Thread.sleep(1000);
                    System.out.println("发送审核记录给到B部门负责人");
                    Thread.sleep(1000);
                    System.out.println("发送审核记录给到C部门负责人");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
        return true;
    }

    /**
* 模拟将审核记录写入到数据库中
*/
private static void createAuditRecord() {
        System.out.println("插入审核记录");
    }

    public static void main(String[] args) {
        boolean auditStatus = doAudit();
        System.out.println("提交审核记录结果:" + auditStatus);
    }
}

和用户注册用例中比较相似,因为消息发送给到各个负责人的过程是比较耗时的一个操作,所以这一块可以通过线程池异步去完成。

通过这几个实际案例我们可以发现,我们在使用线程池的时候,基本都是将任务往线程池里丢。那么当我们将线程任务丢入到线程池之后,又会发生什么事情呢?下边就需要我们深入了解下线程池的原理了。

线程池原理

这里我用一张动态图来带大家形象地理解线程池的内部执行流程,如下:

img

其实线程池的核心原理就和生产者消费者差不多,比较相似的点在于,一端不断地提交任务,一端不断地消费任务,而生产者就是我们的调用方,消费者则是线程池内部的worker线程。

整体的执行逻辑大致如下:

  • 当生产者提交任务之后,线程会优先分配给空闲的核心线程。

  • 如果核心线程都处于忙碌状态,那么新提交的任务就会被放入到一个队列中去。

  • 若队列满了之后,再有新的任务提交,则会将新提交的任务放入到非核心线程中处理。

  • 假若连非核心线程也都处理不过来的话,那么接下来线程池就会执行拒绝策略。

为了方便大家理解,我整理了一张流程图供大家查看:

img

在实际使用线程池的时候,我们通常又会使用哪些参数呢?下边我整理了一些供大家参考:

  • corePoolSize 核心线程数;

  • maximumPoolSize 最大线程数;

  • keepAliveTime 线程活跃时间;

  • zaResizableCapacityLinkedBlockingQueue 基于 JDK 改造的可伸缩队列;

  • allowCoreThreadTimeOut 允许核心线程数超时后被回收;

  • preStartCoreThread 是否要在一开始就启动 corePoolSize 数量的线程数

  • preStartAllCoreThreads 是否要在一开始就启动 maximumPoolSize 数量的线程数

在结合不同的应用场景下,我们通常会给线程池配置不同的参数,对于线程池参数的配置其实也是一门学问,常见的任务类型可以划分为 IO密集型CPU密集型。 通常 IO 密集型任务主要是一些涉及到读写磁盘,网络通信之类的任务,这类任务在互联网业务场景中会比较多,例如远程服务调用。而 CPU 密集型任务的代表如:数据加密解密计算、图片识别等。

关于线程池参数方面的设置,其实网上有许多种说法,有的会根据一套公式来给出定义,但其实线程池的参数应该可以动态调整才是合理的

不同的应用中间件是如何使用线程池的?

Spring 内部的异步注解

在Spring框架中,有一个叫做 @Async 的注解 ,在 Spring 应用上下文中加入一个 @EnableAsync 的注解,接着在使用的方法头部加入一个 @Async 注解,则被标示的方法在执行的时候,就会被自动提交到一个异步线程池中使用,从而产生异步的效果。例如下边这段代码:

@Service
public class AsyncService {

    //加入注解之后,该方法自动就带有了异步的效果
    @Async
    public void testAsync(){
        try {
            Thread.sleep(1000*2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+" async test");
    }
}

控制器部分代码如下:

@RestController
@RequestMapping(value = "/test")
public class TestController {

    @Resource
    private AsyncService asyncService;

    @GetMapping(value = "/do-test")
    public String doTest(){
        //每次调用都会将该方法扔入一个线程池中执行    
        asyncService.testAsync();
        return "success";
    }
}

在生产环境中,我们通常不会直接使用 Spring 自带的线程池,而是会采用一个自定义的线程池去替代它。这是因为 Spring 对于带有 @Async 注解的方法,会将它们丢入到一个叫做 applicationTaskExecutor 的线程池中。

img

对于这个线程池的属性,我们可以看出,它的内部对于最大线程数设置是一个巨大的数值,因此使用@Async注解默认的线程池的话,容易产生无止境地工作,从而导致oom异常。

为了解决这一存在的风险,通常我们可以尝试将Async对应的线程池给更换为自定义的一个线程池,从而避免 oom 情况的发生,具体操作如下所示:

@Configuration
public class AsyncExecuteConfig extends AsyncConfigurerSupport {

    @Bean
    public ThreadPoolTaskExecutor asyncExecutor() {
        ThreadPoolTaskExecutor threadPool = new ThreadPoolTaskExecutor();
        threadPool.setCorePoolSize(3);
        threadPool.setMaxPoolSize(3);
        threadPool.setWaitForTasksToCompleteOnShutdown(true);
        threadPool.setAwaitTerminationSeconds(60 * 15);
        return threadPool;
    }

    @Override
    public Executor getAsyncExecutor() {
        return asyncExecutor();
    }
}

Dubbo 底层的线程池使用

在 Dubbo 线程池的底层中,其实也存在很多使用线程池场景。在 Dubbo 的内部源代码中,专门对不同类型的线程池做了各种扩展,具体可以通过阅读 Dubbo 的源代码去查看,如下图所示:

img

Dubbo 底层针对线程池的种类做了一系列的封装,并且定义了一个专门的 SPI 接口去进行扩展。

RocketMQ 的消费线程池

在 RocketMQ 的内部也存在着使用线程池的场景,在 Consumer 节点中,会有一个叫做 consumeExecutor 的线程池负责将消息从 broker 中拉取到本地内存进行消费。

img

初此之外,在构建对象 ConsumeMessageConcurrentlyService 的时候,就默认构建了一批线程池。

img

通过阅读不同的中间件我们可以发现,线程池在实际应用中是一款实用性非常高的技术,合理地应用线程池技术,可以很好地帮助我们解决一些复杂的应用场景。

总结:

  • Dubbo 底层使用了业务线程池和 IO 线程池进行分工合作,IO 线程池一般负责接收请求并且转发,业务线程池则是用于接收请求并且进行业务处理。

  • Spring 底层采用了线程池来异步化操作,但是要注意使用 @Async 注解的时候,其底层是会使用一个无界队列的线程池,所以要注意 oom 异常的发生。

  • 在 RocketMQ 中,消费者端定义了一个线程池,用于从 broker 节点上拉取消息数据,具体源代码可以在 ConsumeMessageConcurrentlyService 类中查看到。

在使用线程池的时候有没有遇到过什么问题呢?

  • 线程池没有做优雅关闭,导致部分运行到一半的任务被杀掉;

  • 线程池执行submit的时候,内部出了异常,但是没有加入try-catch代码块,导致异常被吞;

  • 线程池的队列长度配置过大,导致应用服务出现 oom;

  • 线程池的内部希望获取到外部的线程变量,可以使用 TransmittableThreadLocal 来获取;

  • 高并发场景下不适合使用线程池接收任务,适合使用 MQ 来替代。

posted @ 2023-03-25 14:51  Dazzling!  阅读(44)  评论(0编辑  收藏  举报