Java线程池设计思想及使用demo

设计思想

本文主要介绍线程池的设计思想及简单使用方式,如有疏漏之处,敬请批评指正。

在介绍线程池之前,首先明确一下什么是线程。百度百科的说法:线程是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

笔者按照自己的理解,以服务大厅(进程)与服务窗口(线程)举例:

  • 单线程模式:服务大厅只有一个窗口,所有办理业务的人,排队进行处理。

  • 多线程模式:来一个客户办理业务,便找客服人员多开一个窗口,办理完成之后,再关闭窗口。

  • 线程池模式:提前预设N个窗口,找一个调度人员给客户进行挂号排队,叫到号的客户去对应窗口进行业务办理。如果客户特别多,动态增加服务窗口,为了保证服务大厅的可用性,最大可增加至M个窗口。当窗口空闲一段时间之后,再请客服人员退场休息。

使用线程池有哪些好处呢?

  • 线程池可以减少多线程场景下,不断创建、销毁线程的资源;
  • 线程池可以根据业务量、系统承载能力,有效管理最小线程、最大线程,以防止业务量过大、程序不断创建线程导致OOM。

看到这里的同学,可能对线程池已经有了一定概念,接下来一起看一下线程池的工作原理,笔者画了两张简图。

多线程:

多线程

线程池:

线程池

重点是工作队列。线程池中队列的出现,代表线程池使用的是事件驱动模型,当工作线程资源充足时,系统会将队列中的任务委派至对应线程处理(部分线程池无缓存队列);当没有可用线程时,可以根据配置等待或丢弃。如果当前线程池中的活跃线程数小于最大线程数,当所有线程处于工作状态时,会创建新的线程,直到活跃线程数等于最大线程数。待活跃线程空闲一定时间之后,会被回收。

事件驱动模型可以降低不同模块或系统之间的耦合度。线程池的设计思想,通过工作队列将请求与处理请求的线程解耦。笔者一直信奉一句话:“程序世界中,没有什么问题是加一层中间件解决不了的”。而线程池中的工作队列,也在一定程度上充当中间件的作用。

了解线程池的设计思想之后,我们来看下常见的使用方式。

使用方式

首先,引用一段《阿里巴巴Java开发手册》中的话:

线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2) CachedThreadPool:允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。

——《阿里巴巴Java开发手册》

所以,本文只介绍ThreadPoolExecutor线程池的使用方式。

ThreadPoolExecutor构造函数如下:

public ThreadPoolExecutor(
            // 最小连接数
            int corePoolSize,
            // 最大连接数
            int maximumPoolSize,
            // 线程空闲等待时间,超过时间之后,空闲线程回收
            long keepAliveTime,
            // 线程空闲时间单位
            TimeUnit unit,
            // 工作队列
            BlockingQueue<Runnable> workQueue,
            // 构建线程的工厂函数
            ThreadFactory threadFactory,
            // 拒绝策略
            RejectedExecutionHandler handler) {
        if (corePoolSize < 0 ||
                maximumPoolSize <= 0 ||
                maximumPoolSize < corePoolSize ||
                keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.acc = System.getSecurityManager() == null ?
                null :
                AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }
  • corePoolSize:最小线程数
  • maximumPoolSize:最大线程数
  • keepAliveTime、unit:如果线程池中活跃线程数 > corePoolSize,那么当线程空闲时间到达keepAliveTime & unit之后,空闲线程会被回收。
  • workQueue:工作线程,常用的有SynchronousQueue、ArrayBlockingQueue、LinkedBlockingQueue三种。
    • SynchronousQueue:无缓存的阻塞队列。既,队列长度为0,当收到请求时,若无空闲线程,则触发拒绝策略。(此队列一般要求线程池最大线程数为Integer.MAX_VALUE,防止因阻塞导致请求丢失,但也存在OOM的问题。)
    • ArrayBlockingQueue:基于数组结构的阻塞队列,线程池创建后,无法修改缓存区(队列)的长度。
    • LinkedBlockingQueue:基于联表结构的阻塞队列。
    • 其他队列:PriorityBlockingQueue、DelayQueue
  • threadFactory:线程构造函数,可以定义线程池中线程的名称等信息,方便排查问题;
  • handler:拒绝策略。系统提供了四种拒绝策略,分别是:CallerRunsPolicy、AbortPolicy、DiscardPolicy、DiscardOldestPolicy。
    • CallerRunsPolicy:提交任务的线程自己去执行。比如使用main方法创建线程池,当触发拒绝策略时,主线程(main)去执行此任务;
    • AbortPolicy:默认拒绝策略,系统抛出异常:RejectedExecutionException;
    • DiscardPolicy:直接丢弃(任务丢失,不建议使用);
    • DiscardOldestPolicy:丢弃最早进入队列的任务,新任务放入队列中。
    • 当然,如果你觉得这些策略都不够优雅,你也可以自定义拒绝策略。只需要实现RejectedExecutionHandler接口即可(自定义拒绝策略,在某种程度上,可以实现服务降级。我们可以Mock一个响应,给客户端)。

代码示例

package test;

import cn.hutool.core.thread.ThreadFactoryBuilder;

import java.util.concurrent.*;

public class Test {


    public static void main (String[] args) {


        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                1,	// 初始线程数
                10, // 最大线程数
                1000, // 空闲线程活跃时间
                TimeUnit.MINUTES, // 空闲线程活跃时间单位
                //new LinkedBlockingQueue<Runnable>(3),
                // new SynchronousQueue<Runnable>(),
                new ArrayBlockingQueue<Runnable>(3),
                // new DelayQueue<>(),
                //new PriorityBlockingQueue<>(),
          			// 通过ThreadFactory,自定义线程名称,方便排查问题
                new ThreadFactoryBuilder().setNamePrefix("test-thread-pool-").build(),
                new ThreadPoolExecutor.CallerRunsPolicy()
          			// 自定义拒绝策略
//                new RejectedExecutionHandler() {
//
//                    @Override
//                    public void rejectedExecution (Runnable r, ThreadPoolExecutor executor) {
//                        System.out.println("哈哈哈,不够使了~" + r + executor);
//                    }
//                }
        );

        for (int i = 0; i < 20; i++) {
            int id = i;
            threadPoolExecutor.execute(() -> {
                System.out.println("测试ID: " + id + ",线程:" + Thread.currentThread().getName());
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            });
        }

        threadPoolExecutor.shutdown();
    }

}

文以载道,疏漏之处敬请批评指正,愿与诸君共勉。

下面是一个Java创建线程池使用的示例代码: ``` import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class ThreadPoolDemo { public static void main(String[] args) { // 创建线程池,最多同时执行2个任务 ExecutorService threadPool = Executors.newFixedThreadPool(2); // 添加任务到线程池 for (int i = 1; i <= 5; i++) { final int index = i; threadPool.execute(new Runnable() { public void run() { System.out.println("任务 " + index + " 开始执行,线程名为 " + Thread.currentThread().getName()); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("任务 " + index + " 执行完毕,线程名为 " + Thread.currentThread().getName()); } }); } // 关闭线程池 threadPool.shutdown(); } } ``` 以上代码中,我们使用`Executors.newFixedThreadPool()`方法创建一个固定大小的线程池,最多同时执行2个任务。然后使用`execute()`方法添加5个任务到线程池中,每个任务都会打印出当前线程名,并让线程休眠1秒钟模拟任务执行的过程。最后使用`shutdown()`方法关闭线程池。 输出结果如下: ``` 任务 1 开始执行,线程名为 pool-1-thread-1 任务 2 开始执行,线程名为 pool-1-thread-2 任务 1 执行完毕,线程名为 pool-1-thread-1 任务 3 开始执行,线程名为 pool-1-thread-1 任务 2 执行完毕,线程名为 pool-1-thread-2 任务 4 开始执行,线程名为 pool-1-thread-2 任务 3 执行完毕,线程名为 pool-1-thread-1 任务 5 开始执行,线程名为 pool-1-thread-1 任务 4 执行完毕,线程名为 pool-1-thread-2 任务 5 执行完毕,线程名为 pool-1-thread-1 ``` 可以看到,线程池中最多同时执行2个任务,任务执行的线程名不固定,任务执行完毕后,线程会自动回收。这样可以避免频繁的创建和销毁线程的开销,提高程序的性能。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值