线程池实现原理

1. 为什么要使用线程池

  1. 线程的创建和销毁都需要消耗资源(cpu,内存)
  2. 使用线程池可以预先创建好线程,开始使用时速度更快

2. 线程池的设计思想

2.1. 如何做到线程在执行完任务后不回收

线程池就是要缓存线程,当一个任务执行完成之后,执行任务的线程可以不用回收,当有新的任务来到时复用当前线程来执行,平时线程执行的代码如下

java 复制代码
public class Thread {
    public void run() {
    
    }
}

线程的创建和销毁实际是底层的操作系统来实现的,当启动一个线程执行任务时,run() 方法就会执行,当 run() 执行完成之后当前的线程会自动销毁 那么如何做到线程不被销毁呢?其实只要让 run() 方法一直在执行就好了,比如加个 while 循环

java 复制代码
public class Thread {
    public void run() {
        while(true) {

        }
    }
}

2.2. 线程空闲时也消耗资源怎么处理

  1. 为了做到将线程不回收,一直执行,所以使用将任务代码放在 while(true) 里面,但是这样做会出现即使没有任务时,这个线程也会一直消耗 cpu 资源,所以需要进一步优化
  2. 优化方式也很简单,就是没有任务时就阻塞当前线程就好了,当有任务来到时再唤起当前线程进行执行
  3. 所以线程池可以认为是一个 生产者--消费者 模型,提交任务的线程就是生产者,线程池自身就是消费者,线程池从阻塞队列中不断获取任务,有任务就执行,没有任务就阻塞自己

3. 线程池创建方式

3.1. Executors 类

java 复制代码
public class Executors {
    // 创建固定数量线程的线程池
    public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      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));
    }
    ......
}

3.2. ThreadPoolExecutor

通过上面 Executors 类创建的线程池可以看到最终还是 new ThreadPoolExecutor, 但是直接使用 Executors 创建的线程池存在的问题是,要么线程数量没有限制,要么是队列的大小没有限制,所以一般情况下都不推荐使用 Executors 来直接创建线程池,而是手动通过 ThreadPoolExecutor 来创建, 比如下面这种方式

java 复制代码
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
        5,  // 线程池核心线程数
        10,  // 线程池最大线程数
        1000,  // 线程池中超过corePoolSize数目的空闲线程最大存活时间
        TimeUnit.MILLISECONDS,  // 时间单位,毫秒
        new LinkedBlockingQueue<>(50),  // 工作线程等待队列
        Executors.defaultThreadFactory(),  // 自定义线程工厂
        new ThreadPoolExecutor.AbortPolicy());  // 线程池满时的拒绝策略

线程池的生命周期

flowchart LR A[running]-- "执行shutdown()方法"--> B[Shutdown] A-- "执行shutdonwNow() 方法"--> C[Stop] B--"任务列表为空,所有任务都执行完成"-->D[TIDYING] B--"shutdownNow()"-->C C--所有任务都执行完成-->D D--"执行 terminated() 方法"-->E[TERMINATED]
  • RUNNING:能接受新任务,并处理阻塞队列中的任务
  • SHUTDOWN:不接受新任务,但是可以处理阻塞队列中的任务
  • STOP:所有任务都不处理(正在运行的线程准备中断,阻塞队列中的任务不再处理,新的任务不会再接收)
  • TIDYING:所有任务都终止,并且工作线程也为0,处于关闭之前的状态
  • TERMINATED:已关闭。

线程池的处理流程

flowchart LR A[提交任务] --> B{核心线程池是否已满} B --否--> C[创建线程执行任务] B --是--> D{队列是否已满} D --否--> E[将任务存储在队列中] D --是--> F{是否达到最大线程池} F --否--> G[创建线程执行] F --是--> J[执行拒绝策略]

线程池相关核心变量

java 复制代码
public class ThreadPoolExecutor extends AbstractExecutorService {
    // ctl 变量保存了两个信息,线程池的状态(高3位)和线程数量(低29位)
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));
    // 统计线程数量的位数,这里表示使用32-3 = 29 位来表示线程的数量
    private static final int COUNT_BITS = Integer.SIZE - 3;
    private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;

    // 线程池的几种状态
    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;
}

下面重点看一下 ctl 变量是如何通过一个变量存储两种信息的

线程数量

java 复制代码
private static final int COUNT_BITS = Integer.SIZE - 3;
  1. 其中 Integer.SIZE = 32, 所以 COUNT_BITS 就是29,也就是使用 29 位来表示线程的数量
java 复制代码
private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;

     1 << COUNT_BITS
      ​
      1的32位2进制是
      0000 0000 0000 0000 0000 0000 0000 0001
      ​
      左移29位
      0010 0000 0000 0000 0000 0000 0000 0000
      ​
      再进行减一
      000 11111 1111 1111 1111 1111 1111 1111
      ​
      所以线程池最大数目就是
      000 11111 11111111 11111111 11111111
  1. 那怎么通过这 29 位来获取线程的数量呢,通过 workderCountOf(int c) 方法可以看到实际是通过位运算中的 操作来去除最高位来进行获取的
java 复制代码
private static int workerCountOf(int c)  { 
    return c & COUNT_MASK; 
}

线程池的状态

获取线程池状态的方法是 runStateOf()

java 复制代码
private static int runStateOf(int c)     {
     return c & ~COUNT_MASK; 
}

可以看到分成了两步

  1. COUNT_MASK 取反,前面已经知道 COUNT_MASK 前面三位是0,最后29位是1, 取反则是前面三位是1,最后29位是0
  2. 操作和计算线程数量时是一样的,是为了去除后面29位数,只需要计算前面三位的数即可

线程池核心问题

线程池核心线程什么时候创建

  1. 默认情况下不会预先创建线程,所以核心线程也是在有任务来的时候才会创建
  2. ThreadPoolExecutor 类中提供了两个方法
java 复制代码
public class ThreadPoolExecutor extends AbstractExecutorService {
    // 预先启动一个核心线程
    public boolean prestartCoreThread() {
        return workerCountOf(ctl.get()) < corePoolSize &&
            addWorker(null, true);
    }

    // 预先启动所有核心线程
    public int prestartAllCoreThreads() {
        int n = 0;
        while (addWorker(null, true))
            ++n;
        return n;
    }
}

通过上面两个方法可知,在创建线程池之后(提交任务之前,也就是调用 execute 之前)是可以调用这两个方法来预先创建核心线程的

线程池的核心线程是否会被回收

java 复制代码
public class ThreadPoolExecutor extends AbstractExecutorService {
    public void allowCoreThreadTimeOut(boolean value) {
        if (value && keepAliveTime <= 0)
            throw new IllegalArgumentException("Core threads must have nonzero keep alive times");
        if (value != allowCoreThreadTimeOut) {
            allowCoreThreadTimeOut = value;
            if (value)
                interruptIdleWorkers();
        }
    }
}

ThreadPoolExecutor 类中有提供 allowCoreThreadTimeOut(boolean) 方法来设置当达到超时时间时,核心线程也可以被回收掉

核心线程数量和最大线程数量动态修改

java 复制代码
// 动态修改核心线程数量
public void setCorePoolSize(int corePoolSize) {
    ......
}

// 动态修改最大线程数量
public void setMaximumPoolSize(int maximumPoolSize) {
    ......
}

上面有写到可以动态修改线程池的核心线程和最大线程数量,那么这么设计的原因是什么呢?说到这个问题就牵扯到另外一个问题,如何合理设置线程池的线程数量大小?对于线程池数量大小的设置问题有多种建议,比如

  1. CPU密集型,线程池大小设置为N+1, IO密集型,线程池大小设置为2N+1(N指的是cpu核心数)
  2. 线程数=CPU核数 *(1+线程等待时间 / 线程时间运行时间)

对于方式2是结合了实际的业务,但是线程等待时间,线程运行时间都是需要 监控 线程环境才可以得知的,既然涉及到 监控 就说明这个值是需要动态去修改的,所以才会需要提供这样两个修改线程数量的方法

相同的任务如何舍弃

  1. 在某些场景下会有任务不断的往线程池中添加,但是对于添加的相同的任务如果之前还没有执行完成,后续添加的任务可以直接舍弃,这里涉及到的知识点是 队列对象相等的判断
  2. 如果每次添加新任务之前旧的任务已经执行完成,就不存在相同的任务舍弃这个问题,所以这里的场景是提交的任务太多,线程没有执行完成的任务保留在队列中
java 复制代码
public class MyTask implements Runnable{
    private String account;

    @Override
    public void run() {
        doSomething();
    }

    // 重写 equals 方法,这个就是判断任务相同的关键
    @Override
    public boolean equals(Object o) {
        if (this == o) {
            return true;
        }
        if (o == null || getClass() != o.getClass()) {
            return false;
        }
        MyTask that = (MyTask) o;
        // 这里是用账号来判断任务是否相同
        return account.equals(that.account);
    }
}
  1. 提交任务之前先获取队列中的任务,看是否有正在排队的相同账号的任务
java 复制代码
// 获取队列中的任务
BlockingQueue<Runnable> queue = threadPoolExecutor.getQueue();
// 构建当前任务
MyTask task = new MyTask("currentTaskAccount");
if (queue.size() > 0 && queue.contains(authSymbolTask)) {
    logger.info("队列中含有:{}的任务,当前任务被丢弃", task.getAccount());
} else {
    // 队列中不包含当前账号的任务就提交当前任务
    threadPoolExecutor.execute(task);
}

源码分析

线程池提交任务流程

下面代码是 ThreadPoolExecutor

java 复制代码
public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    int c = ctl.get();
    // 线程数量小于核心线程数
    if (workerCountOf(c) < corePoolSize) {
        // 1. 创建线程的核心方法
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 线程数量大于核心数量,则将任务添加到队列中
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 再次检查如果线程池不是运行状态,则将添加到队列的任务移除掉,并且执行拒绝策略
        if (! isRunning(recheck) && remove(command))
            reject(command);
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
        reject(command);
}

可以看到添加线程进行执行主要是在 addWorker() 方法中,所以继续看 addWorkder()

java 复制代码
private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (int c = ctl.get();;) {
        // 线程池正在关闭或已经关闭时就不运行添加新任务
        if (runStateAtLeast(c, SHUTDOWN)
            && (runStateAtLeast(c, STOP)
                || firstTask != null
                || workQueue.isEmpty()))
            return false;

        for (;;) {
            if (workerCountOf(c)
                >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                return false;
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateAtLeast(c, SHUTDOWN))
                continue retry;
            // else CAS failed due to workerCount change; retry inner loop
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    Worker w = null;
    try {
        // worker类包含Thread 和 Runnable Task
        w = new Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                int c = ctl.get();
                // 线程池还在运行状态
                if (isRunning(c) ||
                    (runStateLessThan(c, STOP) && firstTask == null)) {
                    if (t.getState() != Thread.State.NEW)
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    workerAdded = true;
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                // 添加成功就启动线程开始执行
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

Worker 类代码如下

java 复制代码
Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

/** Delegates main run loop to outer runWorker. */
public void run() {
    runWorker(this);
}

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        // 如何保证线程不会被回收呢?就是通过一个 while 循环,不让线程退出 run 方法
        // 如何保证线程在空闲时不占用cpu呢,实现逻辑就是在 getTask() 方法里面,如果没有获取到任务就阻塞当前线程
        // 如何实现线程的回收呢?这个关键点就是 while 条件的判断,当条件不满足时就会退出 while 循环,当前线程执行完了,线程就会被jvm回收掉
        while (task != null || (task = getTask()) != null) {
            w.lock();
            if ((runStateAtLeast(ctl.get(), STOP) ||
                    (Thread.interrupted() &&
                    runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                try {
                    task.run();
                    afterExecute(task, null);
                } catch (Throwable ex) {
                    afterExecute(task, ex);
                    throw ex;
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        // 线程池中线程被回收的逻辑
        processWorkerExit(w, completedAbruptly);
    }
}

线程池中的线程是如何被回收的

sequenceDiagram participant A as ThreadPoolExecutor[A] participant B as ThreadPoolExecutor[B] participant C as Worker[C] participant D as Worker[D] participant E as Worker[E] A->>B: execute() B->>C: addWorker(Runnable firstTask, boolean core) Note over B,C: Worker类包含Thread和Task C->>D: runWorker(Worker) Note over C,D: runWorker()中有while,保证线程的复用 D->>E: getTask() Note over D,E: 获取不到任务会阻塞

在上面时序图中可以看到,在 Worker.runWorker() 方法中会不断的循环,保证线程可以复用,同时在 getTask() 中做是否要退出循环的条件的判断

  1. 阻塞队列中没有任务了
  2. 已经超过了设置的空闲时间

这样就可以退出让 Thread 的run 方法流程走完,退出 run 方法之后相当于这个线程就被回收了

相关推荐
齐 飞7 分钟前
MongoDB笔记01-概念与安装
前端·数据库·笔记·后端·mongodb
九圣残炎16 分钟前
【从零开始的LeetCode-算法】1456. 定长子串中元音的最大数目
java·算法·leetcode
wclass-zhengge18 分钟前
Netty篇(入门编程)
java·linux·服务器
LunarCod24 分钟前
WorkFlow源码剖析——Communicator之TCPServer(中)
后端·workflow·c/c++·网络框架·源码剖析·高性能高并发
Re.不晚1 小时前
Java入门15——抽象类
java·开发语言·学习·算法·intellij-idea
雷神乐乐1 小时前
Maven学习——创建Maven的Java和Web工程,并运行在Tomcat上
java·maven
码农派大星。1 小时前
Spring Boot 配置文件
java·spring boot·后端
顾北川_野1 小时前
Android 手机设备的OEM-unlock解锁 和 adb push文件
android·java
江深竹静,一苇以航1 小时前
springboot3项目整合Mybatis-plus启动项目报错:Invalid bean definition with name ‘xxxMapper‘
java·spring boot
confiself1 小时前
大模型系列——LLAMA-O1 复刻代码解读
java·开发语言