线程池核心原理浅析

· 浏览次数 : 5

小编点评

**线程池原理** * **FixedThreadPool:**使用无界任务队列,线程数根据系统可用线程数动态调整。 * 存在内存泄露的风险,因为线程池会一直保持一些空闲线程。 * **SingleThreadExecutor:**使用单线程执行任务,线程池只有一个工作线程。 * 当多于 1 个任务提交时,会存到任务队列中,等待任务执行。 * 存在内存泄露的风险。 * **CachedThreadPool:**根据实际情况调整线程数的线程池,线程数量不确定,但不超过 60 个。 * 使用异步队列,可以避免内存泄露。 * **ScheduledThreadPoolExecutor:**支持定时任务执行。 * 可以指定多个后台线程数,并根据时间对线程进行调度。 * 避免内存泄露,因为线程池只创建执行任务的线程。 **其他方法** * **submit():**用于提交需要返回值的任务。 * **get():**用于获取任务的返回值,阻塞当前线程直到任务完成。 * **shutdown():**用于关闭线程池,并释放所有资源。 * **shutdownNow():**用于关闭线程池,并释放所有资源,但不保证任务执行完成。

正文

前言

由于系统资源是有限的,为了降低资源消耗,提高系统的性能和稳定性,引入了线程池对线程进行统一的管理和监控,本文将详细讲解线程池的使用、原理。


为什么使用线程池

池化思想

线程池主要用到了池化思想,池化思想在计算机领域十分常见,主要用于减少资源浪费、提高性能等。

池化思想主要包含以下几个方面:

fuxing

一些常见的资源池包括线程池、数据库连接池、对象池、缓存池、连接池等。

池化思想可以提高系统的性能,因为它减少了资源的创建和销毁次数,避免了不必要的开销。通过池化,系统可以更好地应对高并发情况,降低资源竞争,提高响应速度。

什么是线程池

根据池化思想,在一个系统中,为了避免线程频繁的创建和销毁,让线程可以复用,引入了线程池的概念。线程池中,总有那么几个活跃线程。

当你需要使用线程时,可以从池子中随便拿一个空闲线程,当完成工作时,并不急着关闭线程,而是将这个线程退回到池子,方便其他人使用。

简单说就是,在使用线程池后,创建线程变成了从线程池中获得空闲线程,关闭线程编程了向池子里归还线程。

大致流程如下:

fuxing
## 为什么使用线程池 Java 中的线程池是运用场景最多的并发框架,几乎所有需要异步或并发执行任务的程序都可以使用线程池。

在开发过程中,合理地使用线程池能够带来3个好处。

  1. 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  2. 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
  3. 提高线程的可管理性。线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一分配、调优和监控。

要做到合理利用线程池,必须对其实现原理了如指掌。

线程池的使用

fuxing
## ThreadPoolExecutor ThreadPoolExecutor 的创建方法总体来说可分为 2 种:
  • 通过 ThreadPoolExecutor 构造函数
  • 通过 Executors 类创建

通过构造函数

1.1. 入参含义

这个也是推荐使用的方法,因为通过 Executors 类创建可能会导致 OOM,如下图阿里开发规范中的描述。

fuxing

构造函数入参:

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

构造函数入参含义:

fuxing

1.2. 阻塞队列

workQueue 可选的 BlockingQueue:

fuxing

1.3. 拒绝策略

fuxing

如下图,上述拒绝策略均实现 RejectedExecutionHandler 接口,且为 ThreadPoolExecutor 的内部类。

fuxing

若以上策略仍无法满足实际应用需要,完全可以自已扩展 RejectedExecutionHandler 接口。

public interface RejectedExecutionHandler {

    /**
     * @param r 当前请求执行的任务
     * @param executor 当前的线程池
     */
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

示例:

public class RejectedExecutionDemo {
    public static class MyTask implements Runnable{

        @Override
        public void run() {
            System.out.println(new Date() + ":Thread ID is" + Thread.currentThread().getId());

            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        MyTask myTask = new MyTask();
        ExecutorService executorService = new ThreadPoolExecutor(5, 5,
                0L, TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(10),
                Executors.defaultThreadFactory(),
                (r, executor) -> System.out.println(r.hashCode() + "is discard")
        );

        for (int i = 0; i < 100; i++) {
            executorService.submit(myTask);
            Thread.sleep(10);
        }
    }
}

上述示例中,mytask 执行需要花费100毫秒,因此,必然会导致一些任务被直接丢弃。在实际应用中,我们可以将更详细的信息记录到日志中,来分析任务丢失情况和系统负载。

fuxing

通过 Executors

Executors 类扮演着线程池工厂的角色,通过该类可以取得一个拥有定功能的线程池。

该类可以创建三种类型的 ThreadPoolExecutor:

  • FixedThreadPool
  • SingleThreadExecutor
  • CachedThreadPool

2.1. FixedThreadPool

固定线程数的线程池,该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行。若没有,则新的任务会被暂时存在任务队列中,待有线程空闲时,在处理队列中的任务。

FixedThreadPool 使用的无界任务队列 LinkedBlockingQueue,可能造成内存泄露。

public static ExecutorService newFixedThreadPool(int nThreads) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>());
}

public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}

2.2. SingleThreadExecutor

只有一个工作线程的线程池,当多于 1 个任务被提交时,会存到任务队列中。该线程池使用的无界任务队列 LinkedBlockingQueue,可能造成内存泄露。

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

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

2.3. CachedThreadPool

根据实际情况调整线程数的线程池,线程池的线程数量不确定,若有空闲线程可复用,则会优先使用。若所有线程均在工作,此时新的任务则会创建新的线程优先处理。所有线程在任务执行完毕后,将返回线程池进行复用。

corePoolSize 被设置为0,maximumPoolSize 被设置为无界,存活时间设置为 60s,空闲线程超过60秒后将会被
终止。极端情况线程创建过多,会导致内存泄露。

public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}


public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>(),
                                  threadFactory);
}

ScheduledThreadPoolExecutor

简介

如下图, ScheduledThreadPoolExecutor 继承自ThreadPoolExecutor,它主要用来定期执行任务,功能与 Timer 类似且更加强大,可以在构造函数中指定多个对应的后台线程数。

fuxing

使用

可通过 Executors 创建,源码如下:

public static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) {
    return new DelegatedScheduledExecutorService
        (new ScheduledThreadPoolExecutor(1, threadFactory));
}

public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}

public static ScheduledExecutorService newScheduledThreadPool(
        int corePoolSize, ThreadFactory threadFactory) {
    return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
}

这里的返回值是 ScheduledExecutorService,根据时间对线程进行调度。有三个主要方法:

public interface ScheduledExecutorService extends ExecutorService {

    /**
     * 给定时间对任务进行调度
     */
    public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);

    /**
     * 周期性对任务进行调度
     * 以第一个任务的开始时间 initialDelay + period 
     * 第一个任务在 initialDelay + period 执行
     * 第二个任务在 initialDelay + period * 2 执行
     */
    public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);

    /**
     * 周期性对任务进行调度
     * 上一个任务结束后,再经过 period 时间开始执行
     */
    public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);
}

如果任务遇到异常,那么后续的所有子任务都会停止调度,因此,必须保证异常被及时处理,为周期性任务的稳定调度提供条件。

ForkJoinPool

fork 是开启子进程,join 是等待,意思是分支子进程结束后才能得到结果,实际开发中,若频繁的 fork 开启线程可能严重影响系统性能,所以引入了 ForkJoinPool。

大致流程是,向 ForkJoinPool 线程池中提交一个 ForkJoinTask 任务,就是将任务分解成多个小任务,等任务全部完成后进行处理,这里采用了分治的思想,具体我将在后续单独展开,这里不多做赘述。

ForkJoin 可能出现两个问题:

  1. 子线程积累过多,可能导致系统性能严重下降;
  2. 调用层次过深,可能导致栈溢出。

线程池的任务提交

execute()

该方法用于提交不需要返回值的任务,且无法判断任务是否被线程池执行成功。

源码见下面的线程池原理章节。

submit()

该方法用于提交需要返回值的任务。线程池会返回 Future 对象,可以判断任务是否执行成功,还可以通过 Future 的get()方法来获取返回值。

get()方法会阻塞当前线程直到任务完成,还可以设置超时时间,到时立即返回,不过这时有可能任务没有执行完。

public Future<?> submit(Runnable task) {
    if (task == null) throw new NullPointerException();
    RunnableFuture<Void> ftask = newTaskFor(task, null);
    execute(ftask);
    return ftask;
}

线程池的关闭

可以通过调用线程池的 shutdown 或 shutdownNow 方法来关闭线程池。

它们的原理是遍历线程池中的工作线程,然后逐个调用线程的 interrupt() 来中断线程,所以无法响应中断的任务可能永远无法终止。

两种方法存在一定的区别,shutdownNow首先将线程池的状态设置成 STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表。而 shutdown 只是将线程池的状态设置成 SHUTDOWN 状态,然后中断所有没有正在执行任务的线程。

只要调用了这两个关闭方法中的任意一个,isShutdown方法就会返回true。当所有的任务都已关闭后,表示线程池关闭成功,这时调用isTerminaed方法会返回true。

至于应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用 shutdown 方法来关闭线程池,如果任务不一定要执行完,则可以调用 shutdownNow 方法。

线程池执行原理

执行源码

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();

    int c = ctl.get();
    
    // 如果当前工作线程数是否小于核心线程数
    if (workerCountOf(c) < corePoolSize) {
        // 添加核心线程去执行任务,成功则return
        if (addWorker(command, true))
            return;
        // 添加失败,ctl有变化,需重新获取
        c = ctl.get();
    }


    // 判断是否为RUNNING,此时核心线程数已满,需加入任务队列
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 检查若不是RUNNING则将任务从队列移除
        if (! isRunning(recheck) && remove(command))
            // 执行拒绝策略
            reject(command);
            
        // 正常则添加一个非核心空线程,执行队列中的任务
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }

    // 表示核心线程满了,队列也满了,创建非核心线程,执行任务
    else if (!addWorker(command, false))
        // 最大线程数也满了,走拒绝策略
        reject(command);
}

流程图

fuxing

参考:
[1] 魏鹏. Java并发编程的艺术.
[2] 葛一鸣/郭超. 实战Java高并发程序设计.

与线程池核心原理浅析相似的内容:

线程池核心原理浅析

由于系统资源是有限的,为了降低资源消耗,提高系统的性能和稳定性,引入了线程池对线程进行统一的管理和监控,本文将详细讲解线程池的使用、原理。

10分钟带你徒手做个Java线程池

摘要:花10分钟开发一个极简版的Java线程池,让小伙伴们更好的理解线程池的核心原理。 本文分享自华为云社区《放大招了,冰河带你10分钟手撸Java线程池,yyds,赶快收藏吧》,作者:冰 河。 Java线程池核心原理 看过Java线程池源码的小伙伴都知道,在Java线程池中最核心的类就是Threa

面试官:核心线程数为0时,线程池如何执行?

线程池是 Java 中用于提升程序执行效率的主要手段,也是并发编程中的核心实现技术,并且它也被广泛的应用在日常项目的开发之中。那问题来了,如果把线程池中的核心线程数设置为 0 时,线程池是如何执行的? 要回答这个问题,我们首先要了解在正常情况下,线程池的执行流程,也就是说当有一个任务来了之后,线程池

Java线程池maximumPoolSize和workQueue哪个先饱和?

先说结论,真正的饱和顺序是corePoolSize -> workQueue -> maximumPoolSize。 我们都知道,线程池有以下参数 ThreadPoolExecutor(int corePoolSize, //核心线程数 int maximumPoolSize, // long ke

别再纠结线程池池大小、线程数量了,哪有什么固定公式

可能很多人都看到过一个线程数设置的理论:CPU 密集型的程序 - 核心数 + 1,I/O 密集型的程序 - 核心数 * 2,不会吧,不会吧,真的有人按照这个理论规划线程数?

小米面试:如何实现优先级线程池?

我们知道,线程池中的所有线程都是由统一的线程工厂来创建的,当我们指定线程工厂时,线程池中的所有线程会使用我们指定的线程工厂来创建线程;但如果没有指定线程工厂,则会使用默认的线程工厂 DefaultThreadFactory 来创建线程,核心源码如下: DefaultThreadFactory() {

如何创建一个线程池,为什么不推荐使用Executors去创建呢?

我们在学线程的时候了解了几种创建线程的方式,比如继承Thread类,实现Runnable接口、Callable接口等,那对于线程池的使用,也需要去创建它,在这里我们提供2种构造线程池的方法: 方法一: 通过ThreadPoolExecutor构造函数来创建(首选) 这是JDK中最核心的线程池工具类,

Jmeter-线程组下篇

线程组 线程组作为JMeter测试计划的核心组件之一,对于模拟并发用户的行为至关重要。线程组元件是整个测试计划的入口,所有的取样器和控制器必须放置在线程组下。 可以将线程组视为一个虚拟用户池,其中每个线程可被理解为一个虚拟用户,多个虚拟用户同时执行相同的一批任务。 在这个虚拟用户池中,每个线程之间是

[转帖]一起来体验96核心、192线程CPU——第四代AMD EPYC处理器独家测试

http://k.sina.com.cn/article_1882475282_70344b12027010s1x.html 与第三代EPYC 7003系列处理器相比,新一代EPYC 9004系列处理器有大量的技术进步,主要包括核心数量、计算线程数大幅提升到最高96核心、192线程;5nm“Zen

[转帖]AMD第四代宵龙 9174F 亮眼

https://www.amd.com/zh-hans/processors/epyc-9004-series#%E8%A7%84%E6%A0%BC 型号规格 型号CPU 核心数量线程数量最大加速时钟频率全核心加速频率基准时钟频率三级缓存默认热设计功耗/TDP AMD EPYC™ 9654P 96