tulip notes
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)

Star-Lord

希望一天成为大师的学徒
首页
  • 学习笔记

    • 《Vue》
  • 踩坑日记

    • JavaScript
  • MQ
  • Nginx
  • IdentityServer
  • Redis
  • Linux
  • Java
  • SpringBoot
  • SpringCloud
  • MySql
  • docker
  • 算法与设计模式
  • 踩坑与提升
  • Git
  • GitHub技巧
  • Mac
  • 网络
  • 项目构建合集
  • 一些技巧
  • 面试
  • 一些杂货
  • 友情链接
  • 项目发布
收藏
  • 分类
  • 标签
  • 归档
GitHub (opens new window)
  • Java基础与面向对象

  • 高级进阶

  • 并发合集

    • Java中创建线程的几种方式
    • 并发相关概念与体系图
    • 线程状态与操作系统的用户态、内核态
    • 线程中的声明与守护线程_基础
    • 程序中的幽灵错误_基础
    • JDK并发包
    • 线程池相关
      • 概念
        • 诞生背景
        • 优势
      • 线程池
        • Executors工具类
        • 例子
        • 计划任务Schedule
        • 核心-ThreadPoolExecutor
        • 任务队列
        • 拒绝策略
        • 线程工厂
        • 扩展-警示
        • 相关问题
        • 线程池中的线程使用流程
        • 扩展-线程池
        • 问题一
        • 问题二
        • 问题三
        • 问题四
    • 并发中的安全集合
    • 生产者和消费者
    • 玩转单例模式
    • 一些工具类的原理
    • 并发包中的AQS
    • ThreadLocal与JMM
    • 锁的探究
  • JVM合集

  • 实战与细节

  • 代码之丑与提升

  • 《Java》学习笔记
  • 并发合集
EffectTang
2024-04-27
目录

线程池相关

# 线程池相关

# 概念

虽然与进程相比,线程是一种轻量级的工具,但其创建和关闭依然需要花费时间,如果为每一个小的任务都创建一个线程,很有可能出现创建和销毁线程所占用的时间大于该线程真实工作所消耗的时间的情况,反而会得不偿失。

其次,线程本身也是要占用内存空间的,大量的线程会抢占宝贵的内存资源,如果处理不当,可能会导致Out of Memory异常。即便没有,大量的线程回收也会给GC带来很大的压力,延长GC的停顿时间.....

# 诞生背景

总的来说,如果应用程序为每一个新任务都创建一个新的线程,会面临以下几个问题:

  1. 资源消耗:线程的创建和销毁需要消耗系统资源,包括CPU时间、内存空间等。频繁地创建和销毁线程会显著增加系统开销,影响性能。
  2. 响应时间:线程的创建通常需要花费一定的时间,尤其是在资源紧张的情况下。这会导致新任务的启动延迟,影响程序的响应速度。
  3. 系统稳定性:无限制地创建线程可能会迅速耗尽系统资源,如内存和文件描述符,导致系统崩溃或变得不稳定。
  4. 管理复杂性:对于大量独立线程,管理它们的状态、优先级、同步等问题变得非常复杂,难以监控和控制。

为了解决上述问题,为了避免系统频繁地创建和销毁线程,我们可以让创建的线程进行复用。于是,线程池的概念应运而生。线程池类似数据库连接池的概念。线程池中,总有那么几个活跃线程。当你需要使用线程时,可以从池子中随便拿一个空闲线程,当完成工作时,并不急着关闭线程,而是将这个线程退回到池子,方便其他人使用。

# 优势

使用线程池后,我们解决了上述的问题并有了以下优势:

  • 复用线程:预先创建一定数量的线程并维护它们,使线程可以在执行完一个任务后快速地被重新分配去执行下一个任务,减少了创建和销毁线程的开销。
  • 控制并发度:允许开发者根据系统资源和负载情况动态调整活动线程的数量,防止资源耗尽,保持系统稳定性。
  • 提高效率和响应性:线程池中的线程准备好立即执行任务,无需等待线程创建,从而降低了任务的等待时间,提高了系统的响应速度。
  • 集中管理:提供了一个统一的接口来管理线程的生命周期、任务排队、线程优先级等,简化了并发编程的复杂度,便于监控和调试。

# 线程池

在Java并发编程中,线程池的核心实现类主要是ThreadPoolExecutor,它是大多数线程池应用场景的基础。以下是关于线程池相关类和接口的一个简化的关系图概述:

java.util.concurrent
│
├── Executor             // 最基础的执行任务的接口
│   ├── ExecutorService   // 扩展Executor,添加了管理方法如shutdown()
│       ├── AbstractExecutorService // 抽象类,实现了ExecutorService的部分方法
│           ├── ThreadPoolExecutor    // 实现了线程池的主要功能,自定义线程池的基础
│           └── ScheduledThreadPoolExecutor // 支持定时及周期性任务的线程池
│
├── Executors            // 工具类,提供了创建不同类型的线程池的静态方法
│
└── Future               // 代表异步计算的结果
    ├── FutureTask        // 实现Future接口和Runnable接口,用于包装Callable或Runnable
    └── ScheduledFuture   // 继承自Future,增加了延期执行和定期执行的功能
1
2
3
4
5
6
7
8
9
10
11
12
13

其中ThreadPoolExecutor表示一个线程池。Executors类则扮演着线程池工厂的角色,通过Executors可以取得一个拥有特定功能的线程池。

  • Future 接口代表异步计算的结果,你可以用它来检查计算是否完成,或者等待结果并获取计算结果。
  • FutureTask 是 Future 的一个实现类,同时它也是 Runnable 的实现,因此可以被线程执行。它通常用来包装 Callable 或 Runnable 对象。
  • ScheduledFuture 是 Future 的子接口,扩展了对延期执行和定期执行的支持。

# Executors工具类

Executors 类是 Java 并发包 (java.util.concurrent) 中的一个非常实用的工具类,它主要用于简化线程池的创建过程,提供了多个静态方法来生成不同类型的 ExecutorService 实例(线程池)。下面是一些 Executors 类中常用的方法

public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newCachedThreadPool()
public static ScheduledExecutorService newSingleThreadScheduledExecutor()
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)”
1
2
3
4
5
  • newFixedThreadPool():该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂存在一个任务队列中,待有线程空闲时,便处理在任务队列中的任务。此方法适用于负载较重但已知并发量的场景。
  • newSingleThreadExecutor():该方法返回一个只有一个线程的线程池。若多余一个任务被提交到该线程池,任务会被保存在一个任务队列中,待线程空闲,按先入先出的顺序执行队列中的任务。适合用于需要顺序处理任务的场景,或者作为线程同步的工具。
  • newCachedThreadPool():该方法返回一个可根据实际情况调整线程数量的线程池。线程池的线程数量不确定,但若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,将返回线程池进行复用。
  • newSingleThreadScheduledExecutor():该方法返回一个ScheduledExecutorService对象,线程池大小为1。ScheduledExecutorService接口在ExecutorService接口之上扩展了在给定时间执行某任务的功能,如在某个固定的延时之后执行,或者周期性执行某个任务。
  • newScheduledThreadPool():该方法也返回一个ScheduledExecutorService对象,但该线程池可以指定线程数量。但额外提供了定时执行和周期性执行任务的能力。

# 例子

public class TaskExe implements Runnable{

    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+" is doing");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        TaskExe taskExe = new TaskExe();
        ExecutorService es = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            es.submit(taskExe);
        }
        es.shutdown();
        //es.shutdownNow();
    }
}
// 输出
/*
pool-1-thread-5 is doing
pool-1-thread-4 is doing
pool-1-thread-2 is doing
pool-1-thread-1 is doing
pool-1-thread-3 is doing
pool-1-thread-1 is doing
pool-1-thread-4 is doing
pool-1-thread-5 is doing
pool-1-thread-2 is doing
* */
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34

该程序会有10个输出,且是以5个为一组的方式,且第一组跟第二组相差1秒。这是因为创建的线程池是固定的只有5个线程,且线程内容需要休息1秒。且你仔细观察就会发现第一组的编号跟第二组的相同,原因同样是因为线程池中只有5个线程可用。

上述例子也展示了,线程池提供的关闭线程池的方法,主要通过以下两个方法来实现:

  1. shutdown():

    这个方法启动一个有序关闭的过程,它不会接受任何新的任务,但是会等待所有已提交的任务执行完成(包括那些已经在工作队列中排队的任务)。调用shutdown()后,线程池会拒绝新任务的提交,并在所有正在执行的任务完成后关闭。此方法不会中断正在执行的任务。

  2. shutdownNow():

    相比于shutdown(),shutdownNow()试图更快速地关闭线程池。它首先尝试停止所有正在执行的任务,并立即停止处理工作队列中的任务,然后返回一个包含未开始执行的任务的列表。这个方法会尽力中断正在执行的任务,并且清空任务队列。不过,已开始执行的任务可能并不会被立即中断,这取决于任务本身的中断响应性。

最后,关于线程池提交线程还有一点要说下,当您调用submit()方法向线程池提交一个任务时,这通常意味着您将任务放入了线程池的任务队列中,线程池随后会从队列中取出任务并分配给一个空闲的线程去执行。

即使,你两次调用submit()方法提交同一个Runnable对象(比如a)到线程池时,这实际上表示你提交了两个独立的任务到线程池,每个任务都是基于相同的Runnable实例。这意味着线程池将会安排这两个任务分别由线程池中的线程执行(会消耗2个线程)。当然,也可能同一个线程执行的。比如一个线程先执行完第一次提交的任务,然后再执行第二次提交的任务,具体取决于线程池当前的工作状况和任务调度策略。

尽管使用 Executors 创建线程池很方便,但现代并发编程实践中推荐直接使用 ThreadPoolExecutor 的构造函数来显式配置线程池参数,以提高应用程序的可控性和性能。这是因为 Executors 提供的简单工厂方法可能无法满足复杂或特定性能要求的应用场景。

# 计划任务Schedule

通过 Executors 创建的线程池有两类,一类是ExecutorService,刚刚的例子就是这一类型,还有一类则是ScheduledExecutorService,它在前者的基础上增加了调度任务的能力。这意味着它可以安排任务在未来的某个时间点执行一次,或者按照固定的延迟或固定周期重复执行。这使得 ScheduledExecutorService 非常适合执行定时任务,如定时备份、定时报告生成、定期检查等。

总的来说,

  • 使用 ExecutorService 的场景通常是对任务并发执行的需求,关注点在于高效地利用线程资源来处理大量短时任务。
  • 而选择 ScheduledExecutorService 的场景则侧重于任务的定时或周期性执行,比如定时维护操作、定时数据同步等。

一个简单是例子:

public class ScheduledExSeDemo implements Runnable{
    SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"---"+ sdf.format(System.currentTimeMillis()));
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    public static void main(String[] args) {
        ScheduledExecutorService ses = Executors.newScheduledThreadPool(10);
        ScheduledExSeDemo demo = new ScheduledExSeDemo();
      // 每隔2秒执行一次,首次执行延迟0秒
        ses.scheduleAtFixedRate(demo,0,2, TimeUnit.SECONDS);
        // 在上一次任务结束后2秒再执行下一次,首次执行延迟0秒
      //ses.scheduleWithFixedDelay(demo,0,2,TimeUnit.SECONDS);

    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

用上述的样子运行,你能发现输出的时间间隔是3s,虽然我们使用scheduleAtFixedRate指定了间隔时间,但是因为程序执行时间(3秒)超过了间隔时间(2秒),所以间隔时间变成了3秒。如果执行时间小于2秒,则间隔时间会是2秒。

但如果你使用scheduleWithFixedDelay,则间隔时间将会变为5秒。

这里你可能有点疑惑,线程池中有10个线程,为什么输出好像是一个一个出现的?这里给出几个可能原因。

  1. 输出缓冲与刷新:在某些情况下,标准输出(System.out)的缓冲机制可能会造成错觉,即虽然多个线程可能已经执行,但输出到控制台却是按顺序进行的。这是因为缓冲区的内容并不是立即刷新到屏幕,而可能在一个线程输出完毕后统一刷新,给人以串行执行的印象。
  2. 线程执行速度:如果线程执行得非常快,以至于看起来像是按顺序执行,实际上是因为线程切换的速度远远超过了人眼能识别的范围,造成了看似有序的假象。
  3. 线程同步或阻塞:如果在代码中无意中引入了线程间的同步点,比如共享资源的锁定,这可能导致线程被迫等待,从而表现出类似于串行的执行顺序。
  4. 单核CPU或CPU调度:在单核CPU的环境下,虽然逻辑上是多线程并发执行,但实际上CPU会在多个线程间快速切换,由于切换速度很快,可能看上去像是串行执行。即便是多核CPU,如果任务调度策略倾向于连续执行同一线程,也可能出现类似现象。

当然,它除了定时执行,还有延迟执行的功能

 executor.schedule(() -> System.out.println("一次性任务执行"), 5, TimeUnit.SECONDS);
1

最后,另外一个值得注意的问题是,调度程序实际上并不保证任务会无限期的持续调用。如果任务本身抛出了异常,那么后续的所有执行都会被中断,因此,如果你想让你的任务持续稳定的执行,那么做好异常处理就非常重要,否则,你很有可能观察到你的调度器无疾而终。

# 核心-ThreadPoolExecutor

上文中通过工具类创建的线程池其实他们都是ThreadPoolExecutor类的封装,为何ThreadPoolExecutor有如此强大的功能呢?来看一下ThreadPoolExecutor最重要的构造函数:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)
1
2
3
4
5
6
7

该类的构造函数有多个,以上是参数最多的一个,其中第六ThreadFactory、第七RejectedExecutionHandler参数可以不传,或者二选一。

函数中各个参数的含义如下,一共有7个参数,但因为重载,在创建时,不必添加7个:

  • corePoolSize:指定了线程池中的线程数量。
  • maximumPoolSize:指定了线程池中的最大线程数量。
  • keepAliveTime:当线程池线程数量超过corePoolSize时,多余的空闲线程的存活时间。即,超过corePoolSize的空闲线程,在多长时间内,会被销毁。
  • unit:keepAliveTime的单位。
  • workQueue:任务队列,被提交但尚未被执行的任务。
  • threadFactory:线程工厂,用于创建线程,一般用默认的即可。
  • handler:拒绝策略。当任务太多来不及处理,如何拒绝任务。

# 任务队列

构造方法中的参数workQueue,指被提交但未执行的任务队列,它是一个BlockingQueue接口的对象,仅用于存放Runnable对象。根据队列功能分类,在ThreadPoolExecutor的构造函数中可使用以下几种BlockingQueue。

  • 直接提交的队列:该功能由SynchronousQueue对象提供。SynchronousQueue是一个特殊的BlockingQueue。SynchronousQueue没有容量,每一个插入操作都要等待一个相应的删除操作,反之,每一个删除操作都要等待对应的插入操作。如果使用SynchronousQueue,提交的任务不会被真实的保存,而总是将新任务提交给线程执行,如果没有空闲的进程,则尝试创建新的进程,如果进程数量已经达到最大值,则执行拒绝策略。因此,使用SynchronousQueue队列,通常要设置很大的maximumPoolSize值,否则很容易执行拒绝策略。
  • 有界的任务队列:有界的任务队列可以使用ArrayBlockingQueue实现。ArrayBlockingQueue的构造函数必须带一个容量参数,表示该队列的最大容量,如下所示。

当使用有界的任务队列时,若有新的任务需要执行,如果线程池的实际线程数小于corePoolSize,则会优先创建新的线程,若大于corePoolSize,则会将新任务加入等待队列。若等待队列已满,无法加入,则在总线程数不大于maximumPoolSize的前提下,创建新的进程执行任务。若大于maximumPoolSize,则执行拒绝策略。可见,有界队列仅当在任务队列装满时,才可能将线程数提升到corePoolSize以上,换言之,除非系统非常繁忙,否则确保核心线程数维持在在corePoolSize。

  • 无界的任务队列:无界任务队列可以通过LinkedBlockingQueue类实现。与有界队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新的任务到来,系统的线程数小于corePoolSize时,线程池会生成新的线程执行任务,但当系统的线程数达到corePoolSize后,就不会继续增加。若后续仍有新的任务加入,而又没有空闲的线程资源,则任务直接进入队列等待。若任务创建和处理的速度差异很大,无界队列会保持快速增长,直到耗尽系统内存。

  • 优先任务队列:优先任务队列是带有执行优先级的队列。它通过PriorityBlockingQueue实现,可以控制任务的执行先后顺序。它是一个特殊的无界队列。无论是有界队列ArrayBlockingQueue,还是未指定大小的无界队列LinkedBlockingQueue都是按照先进先出算法处理任务的。而PriorityBlockingQueue则可以根据任务自身的优先级顺序先后执行,在确保系统性能的同时,也能有很好的质量保证(总是确保高优先级的任务先执行)。

回顾newFixedThreadPool()方法的实现。它返回了一个corePoolSize和maximumPoolSize大小一样的,并且使用了LinkedBlockingQueue任务队列的线程池。因为对于固定大小的线程池而言,不存在线程数量的动态变化,因此corePoolSize和maximumPoolSize可以相等。同时,它使用无界队列存放无法立即执行的任务,当任务提交非常频繁的时候,该队列可能迅速膨胀,从而耗尽系统资源。

newSingleThreadExecutor()返回的单线程线程池,是newFixedThreadPool()方法的一种退化,只是简单的将线程池线程数量设置为1。

newCachedThreadPool()方法返回corePoolSize为0,maximumPoolSize无穷大的线程池,这意味着在没有任务时,该线程池内无线程,而当任务被提交时,该线程池会使用空闲的线程执行任务,若无空闲线程,则将任务加入SynchronousQueue队列,而SynchronousQueue队列是一种直接提交的队列,它总会迫使线程池增加新的线程执行任务。当任务执行完毕后,由于corePoolSize为0,因此空闲线程又会在指定时间内(60秒)被回收。

对于newCachedThreadPool(),如果同时有大量任务被提交,而任务的执行又不那么快时,那么系统便会开启等量的线程处理,这样做法可能会很快耗尽系统的资源。

使用自定义线程池时,要根据应用的具体情况,选择合适的并发队列作为任务的缓冲。当线程资源紧张时,不同的并发队列对系统行为和性能的影响均不同。

# 拒绝策略

ThreadPoolExecutor的最后一个参数指定了拒绝策略。也就是当任务数量超过系统实际承载能力时,该如何处理呢?这时就要用到拒绝策略了。当线程池中的线程已经用完了,无法继续为新任务服务,同时,等待队列中也已经排满了,再也塞不下新任务了。这时,我们就需要有一套机制——拒绝策略。

JDK内置了四种拒绝策略 (通过调用ThreadPoolExecutor 的静态方法可使用):

  1. AbortPolicy(默认策略):抛出RejectedExecutionException异常,阻止新任务的提交。
  2. CallerRunsPolicy:调用者运行策略,即提交任务的线程自己去执行该任务,而不是将任务交给线程池执行。这可以避免任务被拒绝,但可能会影响提交任务的线程性能。
  3. DiscardPolicy:静默丢弃无法处理的任务,不抛出任何异常。这种方式可能导致任务丢失。
  4. DiscardOldestPolicy:丢弃队列中最旧的任务(即最早提交而未被执行的任务),然后尝试重新提交被拒绝的任务。这确保了新任务有机会被执行,但可能会牺牲旧任务。

以上内置的策略均实现了RejectedExecutionHandler接口。

但,如果预定义的策略不满足需求,开发者可以实现自己的RejectedExecutionHandler接口,定制化处理无法接纳的任务的逻辑。

public class CustomRejectionHandler implements RejectedExecutionHandler {
    @Override
    public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
      //“其中r为请求执行的任务,executor为当前的线程池”
        System.out.println(r.toString() + " is rejected");
        // 自定义处理逻辑
    }
}

// 创建线程池实例
ThreadPoolExecutor executor = new ThreadPoolExecutor(
        10, // 核心线程数
        20, // 最大线程数
        60, // 空闲线程存活时间
        TimeUnit.SECONDS, // 时间单位
        new ArrayBlockingQueue<>(100), // 工作队列
        new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
        // 或者使用自定义的拒绝策略
        // new CustomRejectionHandler()
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

在实际应用中,当发生线程拒绝时,我们可以将尽可能详细的信息记录到日志中,来分析系统的负载和任务丢失的情况。

# 线程工厂

ThreadPoolExecutor中还有一个重要参数,ThreadFactory线程工厂。它的作用其实很容易猜,用于线程池中线程的创建。但该参数——线程工厂是可以不用传的,也就是说它有一个默认实现。

ThreadPoolExecutor的默认线程工厂是通过Executors.defaultThreadFactory()方法提供的,这个方法返回一个实现了ThreadFactory接口的实例。这个默认工厂创建的线程具有以下特点:

  1. 线程组: 所有由默认线程工厂创建的线程都属于同一个线程组。线程组在Java中用于管理一组线程,比如中断线程组内所有线程。
  2. 命名规则: 线程名称遵循一个特定的格式,如“pool-N-thread-M”,其中N是线程池的序列号,M是线程在该线程池中的序列号。这种命名有助于调试和监控。
  3. 优先级: 线程的优先级被设置为Thread.NORM_PRIORITY,即默认的普通优先级。
  4. 守护线程: 默认创建的线程不是守护线程(非守护线程)。这意味着,当只剩下守护线程时,JVM会正常退出;而只要有非守护线程(如由默认线程工厂创建的线程)在运行,JVM就不会退出。

当然如果你想自定义一个线程工厂也是ok的,ThreadFactory是一个接口,它只有一个方法,用来创建线程:

Thread newThread(Runnable r);
1

因此,要自定义线程工厂,你需要实现java.util.concurrent.ThreadFactory接口。并实现newThread(Runnable r)方法。这样一来你就可以自定义线程创建的各个方面,比如线程的名字、线程组、优先级、是否为守护线程等。

public class CustomThreadFactory implements ThreadFactory {
    private final String threadNamePrefix;
    private final boolean daemon;
    private final int priority;
    private final ThreadGroup group;
    private final AtomicInteger threadCount = new AtomicInteger(1);

    public CustomThreadFactory(String threadNamePrefix, boolean daemon, int priority) {
        this.threadNamePrefix = threadNamePrefix;
        this.daemon = daemon;
        this.priority = priority;
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() : Thread.currentThread().getThreadGroup();
    }

    @Override
    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r, threadNamePrefix + "-" + threadCount.getAndIncrement());
        t.setDaemon(daemon);
        t.setPriority(priority);
        return t;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23

# 扩展-警示

在阿里的规约,也就是《阿里巴巴开发手册》中有说到,禁止使用Executors去创建线程池,而要通过ThreadPoolExecuter去创建,因为这样可以让开发者更明确线程池的运行规则,规避资源耗尽的风险。

以下是一个通过ThreadPoolExecutor创建线程的例子:

public class PoolT {

    public static void main(String[] args) {
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                2, //核心线程数
                5,	//最大线程数
                30,	//等待时间数
                TimeUnit.SECONDS,	//等待时间单位
                new LinkedBlockingDeque<>(3),  // 等待队列类型 且 容量为3
                new ThreadPoolExecutor.AbortPolicy()	// 拒绝策略
        );

        try {
            for (int i = 0; i < 8; i++) {
                threadPoolExecutor.execute(()->
                    System.out.println(Thread.currentThread().getName()+"执行处理ing"));
                    
            }

        } catch (Exception e) {
            throw new RuntimeException(e);
        } finally {
            //
            threadPoolExecutor.shutdown();
        }

    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29

每次调用 ThreadPoolExecutor 的 execute 方法时,线程池会根据预先设定的规则和当前的状态来决定如何处理任务。具体来说,可能会创建新线程,也可能不会。

上述例子就会触发最大线程数,但不会触发等待策略。以下是一个执行结果,仅供参考

pool-1-thread-5执行处理ing
pool-1-thread-4执行处理ing
pool-1-thread-1执行处理ing
pool-1-thread-5执行处理ing
pool-1-thread-1执行处理ing
pool-1-thread-4执行处理ing
pool-1-thread-3执行处理ing
pool-1-thread-2执行处理ing
1
2
3
4
5
6
7
8

# 相关问题

线程池的最大支持线程数如何计算?

最大线程数+等待队列 = 最大支持线程数

最大线程何时启动?何时不启动?

当需要处理的任务,超过核心线程数+阻塞队列,则会开启最大线程数,也就是说,会继续启动(最大线程数-核心线程数)个的线程。

比如:核心2个,阻塞队列3个,最大线程数5个。

任务书超过5个,则会开启最大线程数。

如果,需要处理的任务,小于--核心线程数+阻塞队列,则线程池中只会运行--核心线程数个线程。多余的线程会进入阻塞队列等待。

拒绝策略何时生效?

当需要处理的任务,超过————最大线程数+等待队列,那多出来的则会使用拒绝策略进行处理。

# 线程池中的线程使用流程

在使用线程池时,线程的创建和复用机制是线程池的核心优势之一。线程池中的线程管理主要分为以下几个步骤:

  1. 创建线程:线程池在启动时会根据配置的核心线程数(corePoolSize)创建一定数量的线程。这些线程会被创建并保持活动状态,等待任务的到来。
  2. 复用线程:当任务提交到线程池时,线程池会优先使用已经创建的空闲线程来执行任务,而不是每次都创建新的线程。这提高了线程的复用率,减少了线程创建和销毁的开销。
  3. 扩展线程:如果当前所有核心线程都在忙碌,并且任务队列已满,线程池会根据配置的最大线程数(maximumPoolSize)创建新的线程来处理任务。这些新创建的线程是非核心线程,会在空闲一段时间后被销毁。

# 扩展-线程池

# 问题一

核心线程耗尽后,为何先启用阻塞队,进行存放任务?直到放不下,才会启用最大线程数。

解答供参考:

  • 线程的创建跟销毁的成本是比较高的,如果某个时刻要处理的任务超过核心线程数,就立即开启最大线程数,比起放到阻塞队里中,成本会高了许多
  • 我们希望用最小的线程数满足最大的吞吐量,于是设计了一个阻塞队列来起到一个缓冲的效果,避免偶尔的大量请求,就频繁的创建跟销毁线程,从而达到一个流量削峰的效果

# 问题二

MQ也可以达到这种流量削峰以及异步化的效果?此时如何选择

解答供参考:

  • 线程池任务持久化能力弱,但使用简单,不需要维护三方组件
  • 但MQ拥有持久化能力,且支持任务堆积,并且可以通过集群的方式进行扩展性能,而不是像线程池一样只能在内存中,且MQ支持重试机制
  • 因此对于可靠性要求高,且持久化有要求建议使用MQ

# 问题三

系统中已经存在了线程池,遇到需求,此时你是用旧的线程池,还是新创建线程池?

解答供参考:

  • 可以判断是IO密集型,还是CPU密集型(io可多配线程数,cpu则可少)
  • 当然,你还可以考虑已有线程池中的拒绝策略,是否跟当前需求适用

# 问题四

最大线程数和核心线程数如何设置?

解答供参考:

  • 同样可以参考它是是IO密集型,还是CPU密集型
  • 但实际上,情况比这复杂得多,更多时候,我们是按照一个经验来设置
  • 因为实际情况复杂得多,因此有了动态线程池
    • 根据实际运行情况调整线程池大小,基于负载、时间和人为调整
#并发#线程池
上次更新: 2025/04/23, 16:23:16
JDK并发包
并发中的安全集合

← JDK并发包 并发中的安全集合→

最近更新
01
面向切面跟自定义注解的结合
05-22
02
时间跟其他数据的序列化
05-19
03
数据加密与安全
05-17
更多文章>
Theme by Vdoing | Copyright © 2023-2025 EffectTang
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式