Java线程池的创建及部分源码分析

在Java中,可以使用java.util.concurrent.Executors类来创建线程池,Executors类提供了多种工厂方法来创建不同类型的线程池,当所有任务执行完成后,调用shutdown()方法关闭线程池即可。

关于线程池的其他文章请看:

https://blog.csdn.net/galaxyou/article/details/136708966

一、FixedThreadPool(LinkedBlockingQueue)

创建一个固定大小的线程池。线程池中的线程数量固定,当线程发生故障时,线程池会自动替换线程。

适用于处理大量短时间运行的任务。这种线程池的大小固定,可以避免线程频繁创建和销毁带来的性能开销。同时,它可以确保系统的资源使用在可控范围内,避免长时间运行的线程消耗过多资源。

int nThreads = 5;
ExecutorService executorService = Executors.newFixedThreadPool(nThreads);

FixedThreadPool 部分源码:

可以看到 Executors 新建一个 FixedThreadPool 实际上还是返回了一个 new ThreadPoolExecutor,其中最关键的是传递了一个 LinkedBlockingQueue<Runnable>,这是一个无界队列,无界队列的最大长度为 Integer.MAX_VALUE,这就可能会导致线上 OOM 问题

/**
创建一个线程池,该线程池重用在共享无界队列中运行的固定数量的线程。在任何时候,线程最多 nThreads 都是活动的处理任务。
如果在所有线程都处于活动状态时提交了其他任务,则这些任务将在队列中等待,直到线程可用。
如果任何线程在关闭前的执行过程中因失败而终止,如果需要执行后续任务,则将有一个新线程代替它。
池中的线程将一直存在,直到它被显式 shutdown 地关闭。
形参:
nThreads – 池中的线程数
返回值:
新创建的线程池
抛出:
IllegalArgumentException –如果 nThreads <= 0  
*/
 public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
 }

ThreadPoolExecutor 部分源码:

在这里就可以看到线程池的7个核心参数了,具体参数含义请看注释

/**
使用给定的初始参数、默认线程工厂和默认拒绝的执行处理程序创建一个新ThreadPoolExecutor参数。
使用其中一种 Executors 工厂方法而不是此通用构造函数可能更方便。
形参:
corePoolSize – 要保留在池中的线程数,即使它们处于空闲状态,除非 allowCoreThreadTimeOut 已设置 
maximumPoolSize – 池中允许的最大线程数 
keepAliveTime – 当线程数大于内核时,这是多余的空闲线程在终止之前等待新任务的最长时间。 
unit – 参数的时间 keepAliveTime 单位 
workQueue – 在执行任务之前用于保存任务的队列。此队列将仅 Runnable 保存该 execute 方法提交的任务。
抛出:
IllegalArgumentException – 如果以下情况之一成立: corePoolSize < 0 keepAliveTime < 0 maximumPoolSize <= 0 maximumPoolSize < corePoolSize
NullPointerException – 如果 workQueue 为 null
*/

public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue) {
       this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
            Executors.defaultThreadFactory(), defaultHandler);
}

ThreadPoolExecutor 的状态值:

RUNNING:接受新任务并处理排队任务 
SHUTDOWN:不接受新任务,但处理排队任务 
STOP:不接受新任务,不处理排队任务,中断正在进行的任务 
整理:所有任务都已终止,workerCount 为零,转换到状态的线程 整理将运行 terminated() 钩子方法 
TERMINATED: terminated() 已完成这些值之间的数字顺序很重要,以便进行有序比较。runState 随时间单调增加,但不必命中每个状态。

/**
主池控制状态 ctl 是一个原子整数,打包了两个概念字段 workerCount,表示有效线程数 runState,指示是否正在运行、关闭等为了将它们打包成一个 int,
我们将 workerCount 限制为 (2^29)-1(大约 5 亿个)线程,而不是 (2^31)-1(20 亿个)线程。
如果将来出现问题,可以将变量更改为 AtomicLong,并调整下面的移位/掩码常量。
但在需要出现之前,使用 int 的这段代码会更快、更简单一些。workerCount 是允许启动和不允许停止的工人数。
该值可能暂时与实际活动线程数不同,例如,当 ThreadFactory 在请求时无法创建线程时,以及当退出线程在终止之前仍在执行簿记时。用户可见的池大小报告为工作线程集的当前大小。

runState 提供主生命周期控制,采用以下值: 
RUNNING:接受新任务并处理排队任务 
SHUTDOWN:不接受新任务,但处理排队任务 
STOP:不接受新任务,不处理排队任务,中断正在进行的任务 
整理:所有任务都已终止,workerCount 为零,转换到状态的线程 整理将运行 terminated() 钩子方法 
TERMINATED: terminated() 已完成这些值之间的数字顺序很重要,以便进行有序比较。runState 随时间单调增加,但不必命中每个状态。
过渡是: 
RUNNING -> SHUTDOWN 调用 shutdown() 时(RUNNING 或 SHUTDOWN) -> STOP 调用 shutdownNow() 时 SHUTDOWN -> 整理 当队列和池都为空时 STOP -> 整理 当池为空时 整理 -> TERMINATED 当 terminated() 钩子方法完成时 等待 awaitTermination() 中的线程将在状态达到 TERMINATED 时返回。检测从 SHUTDOWN 到 TIDYING 的转换并不像您想要的那么简单,因为在 SHUTDOWN 状态期间,队列可能会在非空之后变为空,反之亦然,但我们只有在看到它为空后,我们看到 workerCount 为 0(这有时需要重新检查 - 见下文)时才能终止。

*/
 private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
 private static final int COUNT_BITS = Integer.SIZE - 3;
 private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;

 // runState is stored in the high-order bits
 private static final int RUNNING    = -1 << COUNT_BITS;
 private static final int SHUTDOWN   =  0 << COUNT_BITS;
 private static final int STOP       =  1 << COUNT_BITS;
 private static final int TIDYING    =  2 << COUNT_BITS;
 private static final int TERMINATED =  3 << COUNT_BITS;

 

二、CachedThreadPool(SynchronousQueue)

创建一个可缓存的线程池。线程池中的线程数量可根据需要自动伸缩。当线程闲置超过60秒时,线程池会自动回收线程。

这样的线程池适用于处理大量短期、突发性任务的场景,它能快速响应任务提交请求,同时通过线程回收机制避免资源浪费。

 CachedThreadPool部分源码:

可以看到该方式创建的线程池其核心线程池数为0,说明该线程池不会预设核心线程,而是根据任务提交情况动态创建新线程来执行任务。

而最大线程池数为 Integer.MAX_VALUE,使用可能会出现 OOM 问题,好处就是在 60s内未使用的线程会被删除不会浪费CPU资源。

SynchronousQueue<Runnable> 是不储存任何元素的阻塞队列,意味着使用 CachedThreadPool 创建的线程接收到任务会直接创建线程去执行任务。

    /**
     创建一个线程池,该线程池根据需要创建新线程,但在以前构建的线程可用时将重用这些线程。这些池通常会提高执行许多短期异步任务的程序的性能。
     如果可用,对的 execute 调用将重用以前构建的线程。如果没有可用的现有线程,则将创建一个新线程并将其添加到池中。
     60 秒内未使用的线程将被终止并从缓存中删除。因此,保持空闲足够长时间的池不会消耗任何资源。
     请注意,可以使用构造函数创建 ThreadPoolExecutor 具有相似属性但不同详细信息(例如,超时参数)的池。
    返回值:
    新创建的线程池
     */
    public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

三、SingleThreadExecutor(LinkedBlockingQueue)

创建一个单线程的线程池,线程池中只有一个线程,所有任务将按照提交顺序执行。

拥有固定线程数的线程池,如果没有任务执行,那么线程会一直等待,适用执行长期的任务

ExecutorService executorService = Executors.newSingleThreadExecutor();

SingleThreadExecutor部分源码:

创建一个单线程的线程池,由于是单线程,且使用了遵循 FIFO 原则,即先进先出的无界队列,长度最大为 Integer.MAX_VALUE,可能出现 OOM 

/**
创建一个 Executor,该执行器使用在无界队列中操作的单个工作线程。
(但请注意,如果此单个线程由于在关闭前的执行过程中失败而终止,则如果需要执行后续任务,则将有一个新线程代替它。)
任务保证按顺序执行,并且在任何给定时间都不会有超过一个任务处于活动状态。
与其他等效 newFixedThreadPool(1) 项不同,返回的执行程序保证不会重新配置以使用其他线程。
返回值:
新创建的单线程执行程序
*/

 public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }

四、ScheduledThreadPool(DelayedWorkQueue)

创建一个固定大小的线程池,支持定时任务执行。

适用于处理定时任务或需要周期执行的任务。这种线程池可以确保定时任务的执行,同时可以控制线程池的大小,避免资源耗尽。

int nThreads = 5;
ScheduledExecutorService executorService = Executors.newScheduledThreadPool(nThreads);

ScheduledThreadPool部分源码:

可以看到最大线程池大小为 Integer.MAX_VALUE,也就意味着可能会出现 OOM 的问题

DelayedWorkQueue():使用 DelayedWorkQueue 类型的队列作为工作队列。DelayedWorkQueue 是 ScheduledThreadPoolExecutor 内部实现的一个特殊队列,支持按照任务的延迟时间排序和执行,这也是为什么 ScheduledThreadPoo 支持定时周期任务的主要原因


/**
创建一个线程池,该线程池可以计划命令在给定延迟后运行或定期执行。
形参:
corePoolSize – 要保留在池中的线程数,即使它们处于空闲状态
返回值:
新创建的计划线程池
抛出:
IllegalArgumentException –如果 corePoolSize < 0
*/
/public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }


/**
使用给定的核心池大小创建一个新的 ScheduledThreadPoolExecutor 。
形参:
corePoolSize – 要保留在池中的线程数,即使它们处于空闲状态,除非 allowCoreThreadTimeOut 已设置
抛出:
IllegalArgumentException –如果 corePoolSize < 0
*/

 public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE,
              DEFAULT_KEEPALIVE_MILLIS, MILLISECONDS,
              new DelayedWorkQueue());
    }

五、WorkStealingPool

创建一个工作窃取线程池。线程池会创建一个内部的ForkJoinPool,用于执行ForkJoinTask任务。

ExecutorService executorService = Executors.newWorkStealingPool(5);

WorkStealingPool部分源码:

 WorkStealingPool(即 ForkJoinPool)在内部通过一个 WorkQueue 数组来维护各个工作线程的双端队列。这些双端队列是任务调度的关键数据结构,它们不仅用于存储待执行的任务,还支持工作窃取算法所要求的并发访问和任务迁移操作。具体的实现细节隐藏在 ForkJoinPool 和相关内部类(WorkQueue)的源代码中。

    /**
     创建一个线程池,该线程池维护足够的线程以支持给定的并行级别,并且可以使用多个队列来减少争用。
     并行度级别对应于主动参与或可用于任务处理的最大线程数。线程的实际数量可能会动态增长和收缩。
     工作窃取池无法保证已提交任务的执行顺序。
     形参: parallelism – 目标并行度级别
     返回值: 新创建的线程池     
    */
    public static ExecutorService newWorkStealingPool(int parallelism) {
        return new ForkJoinPool
            (parallelism,
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

    /**
     使用 可用处理器 数作为其目标并行度级别创建工作窃取线程池。
     返回值: 新创建的线程池     
    */
    public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

窃取的主要流程:

当一个工作线程完成自己的队列中的所有任务后,它会遍历 workQueues 数组,尝试从其他工作线程的队列尾部窃取任务。这里所谓的“窃取”,实质上是通过原子操作从目标队列的尾部摘取任务节点,并将其添加到自己的队列中。

工作窃取算法:

工作窃取算法是一种高效的多线程任务调度策略,尤其适用于处理大量细粒度任务的场景。其基本思想是:每个线程都有自己的工作队列,负责从队列中取出任务执行。当某个线程完成自己队列中的任务,而其他线程仍有任务待处理时,该线程会从其他线程的队列中“窃取”任务来执行,而不是等待自己的队列重新填充。这种策略可以有效利用多核处理器的空闲资源,减少线程间的竞争和阻塞,提高整体系统的并行效率。

六、手动创建线程池

1、创建线程池

注意事项:

(1)最大线程数 >= 核心线程数   ==  maximumPoolSize >= corePoolSize

(2)使用 ArryBlockingQueue 的有界阻塞队列,可以避免 OOM

(3)选择合适的拒绝策略,JDK内置了四种拒绝策略

         1、AbortPolicy:直接抛出异常,阻止系统正常运行。可以根据业务逻辑选择重试或者放弃提交等策略。

         2、CallerRunsPolicy :只要线程池未关闭,该策略直接在调用者线程中运行当前被丢弃的任务。不会造成任务丢失,同时减缓提交任务的速度给执行任务缓冲时间。

         3、DiscardOldestPolicy :丢弃最老的一个请求,也就是即将被执行的任务,并尝试再次提交当前任务。

         4、DiscardPolicy :该策略默默地丢弃无法处理的任务,不予任何处理。如果允许任务丢失,这是最好的一种方案。

  @Test
    void ThreadPoolExecutorTest(){
        //核心线程数
        int corePoolSize = 5;
        //最大线程数
        int maximumPoolSize = 10;
        //存活时间
        long keepAliveTime = 60;
        //时间单位秒
        TimeUnit timeUnit = TimeUnit.SECONDS;
        //阻塞任务队列100,这里一定要使用ArrayBlockingQueue,也就是基于数组实现的有界阻塞队列,可以避免OOM
        BlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
        //拒绝策略,可以根据自己需要选择
        RejectedExecutionHandler handler = new ThreadPoolExecutor.AbortPolicy();

        ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                timeUnit,
                workQueue,
                handler
        );
    }

2、使用线程池

使用 execute(Runnable task) 方法提交任务到线程池:

threadPool.execute(() -> {
    // 任务逻辑
});

或者使用 submit(Callable<T> task) 方法提交有返回值的任务,并通过 Future 接口获取结果:

Future<String> future = threadPool.submit(() -> {
    // 任务逻辑,返回一个字符串结果
    return "Task result";
});

try {
    String result = future.get();
    System.out.println("Task result: " + result);
} catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
}

3、关闭线程池

在应用程序结束时,记得优雅地关闭线程池,以确保所有任务完成并释放资源

threadPool.shutdown();
// 或者,如果需要立即停止所有正在执行的任务和未开始的任务:
// threadPool.shutdownNow();

使用 shutdown() 方法会等待所有已提交的任务执行完毕,然后关闭线程池。如果需要立即停止所有任务,可以使用 shutdownNow() 方法,但它可能造成任务中断,且返回一个包含尚未开始或未完成任务的列表。

  • 25
    点赞
  • 23
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值