java线程池详解

线程池

一、概述

1、问题

先看我们遇到的问题:我们创建线程的方式很简单, new Thread(() -> {…}) ,就是因为这么简单粗暴的方式,才带来了致命的问题。首先线程的创建和销毁都是很耗时很浪费性能的操作,你用线程为了什么?为了就是异步,为了就是提升性能。简单的new三五个Thread还好,我需要一千个线程呢?你也for循环new1000个Thread吗?用完在销毁掉。那这一千个线程的创建和销毁的性能是很糟糕的!

2、解决

为了解决上述问题,线程池诞生了,线程池的核心思想就是:线程复用。也就是说线程用完后不销毁,放到池子里等着新任务的到来,反复利用N个线程来执行所有新老任务。这带来的开销只会是那N个线程的创建,而不是每来一个请求都带来一个线程的从生到死的过程。

线程池

1. 概念

比如找工作面试,涉及到两个角色:面试官、求职者。求职者成千上万,每来一个求职者都要为其单独新找一个面试官来面试吗?显然不是,公司都有面试官池子,比如:A、B、C你们三就是这公司的面试官了,有人来面试你们三轮流面就行了。可能不是很恰当,含义就是说我并不需要为每个请求(求职者)都单独分配一个新的线程(面试官),而是我固定好几个线程,由他们几个来处理所有请求。不会反复创建销毁。

2. 参数

public ThreadPoolExecutor(int corePoolSize,
	int maximumPoolSize,
	TimeUnit
	long keepAliveTime,
	unit,
	BlockingQueue<Runnable> workQueue,
	ThreadFactory threadFactory,
	RejectedExecutionHandler handler) {
}

参数解释

  1. corePoolSize:核心线程数:
    线程池在完成初始化之后,默认情况下,线程池中不会有任何线程,线程池会等有任务来的时候再去创建线程。核心线程创建出来后即使超出了线程保持的存活时间配置也不会销毁,核心线程只要创建就永驻了,就等着新任务进来进行处理。

  2. maximumPoolSize:最大线程数
    核心线程忙不过来且任务存储队列满了的情况下,还有新任务进来的话就会继续开辟线程,但是也不是任意的开辟线程数量,线程数(包含核心线程)达到最大线程数后就不会产生新线程了,就会执行拒绝策略。

  3. keepAliveTime:线程保持的存活时间
    如果线程池当前的线程数多于核心线程数,那么如果多余的线程空闲时间超过线程保持的存活时间,那么这些多余的线程(超出核心线程数的那些线程)就会被回收。

  4. unit:线程保持的存活时间单位
    比如:TimeUnit.MILLISECONDS、TimeUnit.SECONDS

  5. workQueue:任务存储队列
    核心线程数满了后还有任务继续提交到线程池的话,就先进入任务存储队列。workQueue通常情况下有如下选择:

    • LinkedBlockingQueue:无界队列,意味着无限制,其实是有限制,大小是int的最大值。也可以自定义大小。
    • ArrayBlockingQueue:有界队列,可以自定义大小,到了阈值就开启新线程(不会超过最大线程数)。
    • SynchronousQueue: Executors.newCachedThreadPool();默认使用的队列。

    一般都采取无界队列,因为他也可以设置大小,可以取代有界队列。

  6. threadFactory:当线程池需要新的线程时,会用threadFactory来生成新的线程
    默认采用的是 DefaultThreadFactory ,主要负责创建线程。newThread() 方法。创建出来的线程都在同一个线程组且优先级也是一样的。这里也可以通过实现ThreadFactory接口使用自定义工厂。

  7. handler:拒绝策略,任务量超出线程池的配置限制或执行shutdown还在继续提交任务的话,会执行handler 的逻辑。
    默认采用的是 AbortPolicy ,遇到上面的情况,线程池将直接采取直接拒绝策略,也就是直接抛出异常。 RejectedExecutionException

    策略说明
    ThreadPoolExecutor.AbortPolicy()丢弃任务并抛出RejectedExecutionException异常。是默认的策略
    ThreadPoolExecutor. DiscardPolicy()丢弃任务,但是不抛出异常,这是不推荐的做法
    ThreadPoolExecutor. DiscardOldestPolicy()抛弃队列中等待最久的任务 然后把当前任务加入队列中
    ThreadPoolExecutor. CallerRunsPolicy()由主线程负责调用任务的run()方法从而绕过线程池直接执行

3. 原理

在这里插入图片描述
线程池刚启动的时候核心线程数为0

丢任务给线程池的时候,线程池会新开启线程来执行这个任务

如果线程数小于 corePoolSize 核心线程数,即使工作线程处于空闲状态,也会创建一个新线程来执行新任务

如果线程数大于或等于 corePoolSize 核心线程数,则会将任务放到 workQueue 任务存储队列

如果任务队列满了,且线程数小于 maximumPoolSize 最大线程数,则会创建一个新线程来运行任务

如果任务队列满了,且线程数大于或等于 maximumPoolSize 最大线程数,则直接采取拒绝策略

  1. 初始化:创建线程池,设定核心线程数、最大线程数、队列容量等参数。
  2. 接收任务:通过execute()或submit()方法提交任务。
  3. 任务分配:
    • 判断核心线程是否都在工作,若有空闲则直接分配任务。
    • 若核心线程都在忙,则判断队列是否满,不满则入队等待。
    • 若队列也满了,则判断当前线程数是否达到最大线程数,未达到则创建新线程执行任务,达到则触发拒绝策略。
  4. 任务执行:线程从队列中取出任务并执行。
  5. 回收资源:当线程空闲时间超过一定阈值或线程池被关闭时,回收线程资源。

4. 线程池的常见类型

Java中常见的线程池类型主要包括以下几种:

  1. FixedThreadPool(固定大小线程池)
    • 线程数量固定,即使有空闲线程,也不会被回收。
    • 适用于负载较重的服务器,能够控制并发线程的数量。
  2. CachedThreadPool(缓存线程池)
    • 线程数量不固定,根据需要动态创建线程,空闲线程会被回收。
    • 适用于执行大量短期异步任务的情况。
  3. ScheduledThreadPool(定时任务线程池)
    • 支持定时及周期性任务执行。
    • 适用于需要按照特定时间间隔执行任务的场景。
  4. SingleThreadExecutor(单线程线程池)
    • 只有一个线程,所有任务按顺序执行。
    • 适用于需要保证任务顺序执行的场景。
  5. ForkJoinPool(工作窃取线程池)
    • 用于执行Fork/Join框架的任务,适用于分割任务并行执行的场景。

5. 线程池的使用案例与分析

5.1 Executors

首先这不是一个线程池,这是线程池的工具类,他能方便的为我们创建线程。但是阿里巴巴开发手册上说明不推荐用Executors创建线程池,推荐自己定义线程池。这是因为Executors创建的任何一种线程池都可能引发血案,具体是什么问题下面会说。

5.2 固定大小线程池

核心线程数和最大线程数是一样的,所以称之为固定线程数。
其他参数配置默认为:永不超时(0ms),无界队列( LinkedBlockingQueue )、默认线程工厂( DefaultThreadFactory )、直接拒绝策略( AbortPolicy )。

api: ExecutorService executorService = Executors.newFixedThreadPool(n)

demo

/**
* Description: 创建2个线程来执行10个任务。
*/
public static void main(String[] args) {
    ExecutorService executorService = Executors.newFixedThreadPool(2);
    for (int i = 0; i < 10; i++) {
        // 从结果中可以发现线程name永远都是两个。不会有第三个。
        executorService.execute(() ->
                System.out.println(Thread.currentThread().getName()));
    }
}
结果如下
pool-1-thread-1
pool-1-thread-2
pool-1-thread-1
pool-1-thread-1
pool-1-thread-2
pool-1-thread-1
pool-1-thread-2
pool-1-thread-1
pool-1-thread-2
pool-1-thread-1

问题

问题就在于它是无界队列,队列里能放int的最大值个任务,并发巨高的情况下极大可能直接OOM(内存溢出)了然后任务还在堆积,毕竟直接用的是jvm内存。所以建议自定义线程池,自己按照需求指定合适的队列大小,自定义拒绝策略将超出队列大小的任务放到对外内存做补偿,比如Redis。别把业务系统压垮就行

5.3 单线程线程池

核心线程数和最大线程数是1,内部默认的,不可更改,所以称之为单线程数的线程池。类似于 Executors.newFixedThreadPool(1);
其他参数配置默认为:永不超时(0ms),无界队列(LinkedBlockingQueue)、默认线程工厂 (DefaultThreadFactory)、直接拒绝策略(AbortPolicy)

api: ExecutorService executorService = Executors.newSingleThreadExecutor();

/**
* Description: 创建1个线程来执行10个任务。
*/
public static void main(String[] args) {
    ExecutorService executorService = Executors.newSingleThreadExecutor();
    for (int i = 0; i < 10; i++) {
        // 从结果中可以发现线程name永远都是pool-1-thread-1。不会有第二个出现。
        executorService.execute(() -> System.out.println(Thread.currentThread().getName()));
    }
}
结果如下
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1

问题

同【固定线程数】的问题,都是无界队列惹的祸。

5.4 缓存线程池

他的功能是来个任务我就开辟个线程去处理,不会进入队列,SynchronousQueue 队列也不带存储元素
的功能。那这意味着来一亿个请求就会开辟一亿个线程去处理,keepAliveTime为60S,意味着线程空闲时间超过60S就会被杀死;这就叫带缓存功能的线程池。

核心线程数是0,最大线程数是int的最大值,内部默认的,不可更改。

其他参数配置默认为:1min超时(60s), SynchronousQueue 队列、默认线程工厂( DefaultThreadFactory )、直接拒绝策略( AbortPolicy )

api: ExecutorService executorService = Executors.newCachedThreadPool();
demo

/**
* Description: 创建个带缓存功能的线程池来执行10个任务。
*/
public static void main(String[] args) {
    ExecutorService executorService = Executors.newCachedThreadPool();
    for (int i = 0; i < 10; i++) {
        // 从结果中可以发现线程name有10个。也就是有几个任务就会开辟几个线程。
        executorService.execute(() -> System.out.println(Thread.currentThread().getName()));
    }
}
结果
pool-1-thread-1
pool-1-thread-4
pool-1-thread-5
pool-1-thread-3
pool-1-thread-2
pool-1-thread-6
pool-1-thread-7
pool-1-thread-8
pool-1-thread-9
pool-1-thread-10

问题

问题就在于他的最大线程数是int的最大值,因为他内部采取的队列是 SynchronousQueue ,这个队列没有容纳元素的能力,这将意味着只要来请求我就开启线程去工作,巅峰期能创建二十几亿个线程出来工作,你自己想想多么可怕!!!

5.5 定时任务线程池

RocketMQ内部大量采用了此种线程池来做心跳等任务。
核心线程数手动传进来,最大线程数是Integer.MAX_VALUE,最大线程数是内部默认的,不可更改。

其他参数配置默认为:永不超时(0ns),带延迟功能的队列( DelayedWorkQueue )、默认线程工厂( DefaultThreadFactory )、直接拒绝策略( AbortPolicy )。

api: ScheduledExecutorService scheduledExecutorService = Executors.newSchedul edThreadPool(n);

demo

public static void main(String[] args) {
    ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(2);
    // 五秒一次 schedule(参数1,参数2,参数3)
    //   参数1:任务
    //   参数2:延迟时间
    //   参数3:时间单位
    scheduledExecutorService.schedule(
            () -> System.out.println(Thread.currentThread().getName()),
            5,
            TimeUnit.SECONDS);
    // 首次五秒后执行,其次每隔1s执行一次
    // scheduleAtFixedRate(参数1. 参数2,参数3,参数4)
    //   参数1:任务
    //   参数2:首次任务延迟时间:首次五秒后执行
    //   参数3:执行间隔时间
    //   参数4:时间单位
    scheduledExecutorService.scheduleAtFixedRate(
            () -> System.out.println(Thread.currentThread().getName()), 5, 1, TimeUnit.SECONDS);
}
运行结果
pool-1-thread-2
pool-1-thread-1
pool-1-thread-2
pool-1-thread-2
pool-1-thread-2
pool-1-thread-2
pool-1-thread-2
pool-1-thread-2
pool-1-thread-2
pool-1-thread-2
pool-1-thread-2
pool-1-thread-1
pool-1-thread-1
pool-1-thread-1
.....

问题

【 带缓存的线程池的问题 】 他的最大线程数是int的最大值,这将意味海量并发期能创建二十几亿个线程出来工作

5.6 疑问

这几种线程池为什么要采取不一样的队列?比如 newFixedThreadPool 为什么采取LinkedBlockingQueue ,而 newCachedThreadPool 又为什么采取SynchronousQueue

因为 newFixedThreadPool 线程数量有限,他又不想丢失任务,只能采取无界队列,而newCachedThreadPool 的话本身自带int最大值个线程数,所以没必要用无界队列,他的宗旨就是我有线程能处理,不需要队列。

5.7 总结几个问题

线程池的状态
  • RUNNING :接受新任务并处理排队任务。
  • SHUTDOWN :不接受新任务,但是会处理排队任务。【见:停止线程的4.6.1、shutdown】
  • STOP :不接受新任务,也不处理排队任务,并中端正在进行的任务。
  • TIDYING :所有任务都已经完事,工作线程为0的时候 ,线程会进入这个状态并执行terminate()钩子方法。
  • TERMINATED :terminate()钩子方法运行完成。
线程池自动创建还是手动?

那肯定是手动了,因为Executors自动创建的那些线程池都存在致命的问题。手动创建线程池我们能自己控制线程数大小以及队列大小,还可以指定组名称等等个性化配置。重点不会出现致命问题,风险都把控在我们手里。

注意:
线程池自动创建是指:上面的4个已经帮我们创建好的,我们直接用即可。
线程池手动创建是指:自己定义线程池的七大参数并执行。

线程数多少合适?
  • CPU密集型(比如加密、各种复杂计算等):建议设置为CPU核数+1。
  • 耗时IO操作(比如读写数据库,压缩解压缩大文件等等):一般会设置CPU核数的2倍。当然也有个很牛X的计算公式:线程数=CPU核数 *(1+平均等待时间/平均工作时间)

注意一般工作是设置CPU核数的2倍,不够再调

5.8 自定义线程池的使用

在Java中,自定义线程池通常意味着你需要直接使用 ThreadPoolExecutor 类,而不是使用Executors 工厂类提供的那些预定义线程池。ThreadPoolExecutor提供了更多的灵活性和控制,允许你详细配置线程池的核心参数,如核心线程数、最大线程数、存活时间、工作队列等。

public static void main(String[] args) {
    // 配置线程池参数
    int corePoolSize = 5; // 核心线程数
    int maximumPoolSize = 10; // 最大线程数
    long keepAliveTime = 1L; // 非核心线程的空闲存活时间
    TimeUnit unit = TimeUnit.SECONDS; // 时间单位
    BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);// 工作队列
    ThreadFactory threadFactory = Executors.defaultThreadFactory();// 线程工厂,这里使用默认的
    RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();// 拒绝策略,这里使用默认的终止策略

    // 创建ThreadPoolExecutor实例
    ThreadPoolExecutor executor = new ThreadPoolExecutor(
            corePoolSize,
            maximumPoolSize,
            keepAliveTime,
            unit,
            workQueue,
            threadFactory,
            handler
    );
    // 提交任务给线程池
    for (int i = 0; i < 20; i++) {
        int taskId = i;
        executor.submit(() -> {
            System.out.println(Thread.currentThread().getName() + " is processing task " + taskId);
        });
    }
    // 关闭线程池(不再接受新任务,但已提交的任务会继续执行)
    executor.shutdown();
    // 等待所有任务完成(可选)
    try {
        if (!executor.awaitTermination(60L, TimeUnit.SECONDS)) {
            // 线程池没有在规定时间内关闭,可以选择取消正在执行的任务
            executor.shutdownNow();
            // 处理未完成的任务等
        }
    } catch (InterruptedException e) {
        // 当前线程在等待过程中被中断
        executor.shutdownNow();
        // 保留中断状态
        Thread.currentThread().interrupt();
    }
}

注意

阻塞任务队列数
线程池的名字,最好跟业务相关
核心线程池大小,看业务实际情况。可以参考【线程数多少合适?】
最大线程池大小,看业务实际情况。可以参考【线程数多少合适?】
拒绝策略,我个人一般都是记录log,如果主要的业务我会根据log做补偿。

ThreadPoolExecutor executor = new ThreadPoolExecutor(
CPU核数 + 1, 
2 * CPU核数 + 1,
5, TimeUnit.SECONDS, new ArrayBlockingQueue<>(2000),
// 线程池名字pay-account
new DefaultThreadFactory("pay-account"), 
(r1, executor) -> {
// 记录log 重新入队列做补偿
});

6. 线程池的使用场景

  • FixedThreadPool:适用于负载较重的服务器,能够控制并发线程的数量,避免过多线程导致资源耗尽。
  • CachedThreadPool:适用于执行大量短期异步任务的情况,能够根据需要动态调整线程数量,减少线程创建和销毁的开销。
  • ScheduledThreadPool:适用于需要按照特定时间间隔执行任务的场景,如定时清理缓存、定时发送邮件等。
  • SingleThreadExecutor:适用于需要保证任务顺序执行的场景,如数据库操作、文件读写等。
### Java 线程池详解 #### 创建线程池的方式 Java 提供了几种创建线程池的方法,最常用的是通过 `Executors` 工厂类来获取预配置好的线程池实例。然而,在实际项目中更推荐使用 `ThreadPoolExecutor` 构造器来自定义参数,以获得更好的灵活性和性能控制[^1]。 ```java // 不推荐的做法:固定大小的缓存线程池可能导致资源浪费或耗尽 ExecutorService cachedPool = Executors.newCachedThreadPool(); // 推荐做法:自定义 ThreadPoolExecutor 参数设置 ThreadFactory namedThreadFactory = new ThreadFactoryBuilder().setNameFormat("demo-pool-%d").build(); ThreadPoolExecutor executor = new ThreadPoolExecutor( corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, workQueue, namedThreadFactory); ``` #### 关键组件说明 - **核心线程数 (`corePoolSize`)** 定义了即使处于空闲状态也会被保留在线程池中的最小线程数量。 - **最大线程数 (`maximumPoolSize`)** 设置允许的最大活动线程数目;当现有任务队列已满而又有新的提交任务时才会触发扩容至该上限。 - **存活时间 (`keepAliveTime`)** 非核心线程闲置后的自动回收等待周期长度。 - **阻塞队列 (`workQueue`)** 存储待处理的任务对象集合,不同类型的队列会影响吞吐量表现及拒绝策略行为。 - **线程工厂 (`threadFactory`)** 负责生产新线程实体,默认实现较为简单,通常建议开发者根据应用场景定制化命名规则以便于调试跟踪。 #### 常见错误与优化技巧 忽视对线程池内部运行状况监控是常见的失误之一。应当定期审查并调整相关参数,确保其适应当前负载需求变化趋势。另外需要注意捕获未预见异常情形下的恢复机制设计,防止因个别失败案例影响整体服务稳定性[^2]。 #### 实践指南 对于Web容器如Tomcat而言,内置有经过优化过的专用线程管理模块用于支撑高并发请求场景下高效运作的要求。尽管如此,了解基础概念仍然有助于更好地理解框架底层运作机理,并能在必要时候做出针对性调优措施。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值