线程池的实现原理分析讲解(上)

线程池原理篇幅太长,所以博主分为上、下两章节讲解,望有兴趣的朋友慢慢阅读~
什么是线程池
在 Java 中,如果每个请求到达就创建一个新线程,创建和销毁线程花费的时间和消耗的系统资源都相当大,甚至可能要比在处理实际的用户请求的时间和资源要多的多。如果在一个 Jvm 里创建太多的线程,可能会使系统由于过度消耗内存或“切换过度”而导致系
统资源不足为了解决这个问题,就有了线程池的概念,线程池的核心逻辑是提前创建好若干个线程放在一个容器中。如果有任务需要处理,则将任务直接分配给线程池中的线程来执行就行,任务处理完以后这个线程不会被销毁,而是等待后续分配任务。同时通过线程池来重复管理线程还可以避免创建大量线程增加开销。
线程池的优势
合理的使用线程池,可以带来一些好处:
1. 降低创建线程和销毁线程的性能开销。
2. 提高响应速度,当有新任务需要执行是不需要等待线程创建就可以立马执行。
3. 合理的设置线程池大小可以避免因为线程数超过硬件资源瓶颈带来的问题。
Java 中提供的线程池 API
我相信有很多朋友或多或少都接触过线程池,也可能自己也研究过线程池的原理。前面部分的内容会相对简单点,但是要想合理的使用线程池,那么势必要对线程池的原理有比较深的理解线程池的使用。要了解一个技术,我们仍然是从使用开始。JDK 为我们提供了几种不同的线程池实现。我们先来通过一个简单的案例来引入线程池的基本使用
在 Java 中怎么创建线程池呢?下面这段代码演示了创建三个固定线程数的线程池:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class Test implements Runnable {
        @Override
        public void run() {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName());
        }
 
        static ExecutorService service = Executors.newFixedThreadPool(3);
 
        public static void main(String[] args) {
            for (int i = 0; i < 100; i++) {
                service.execute(new Test());
            }
            service.shutdown();
        }
    }
Java 中提供的线程池 Api
 
为了方便大家对于线程池的使用,在 Executors 里面提供了几个线程池的工厂方法,这样,很多新手就不需要了解太多关于 ThreadPoolExecutor 的知识了,他们只需要直接使用Executors 的工厂方法,就可以使用线程池:
newFixedThreadPool:该方法返回一个固定数量的线程池,线程数不变,当有一个任务提交时,若线程池中空闲,则立即执行,若没有,则会被暂缓在一个任务队列中,等待有空闲的线程去执行。
newSingleThreadExecutor: 创建一个线程的线程池,若空闲则执行,若没有空闲线程则暂缓在任务队列中。
newCachedThreadPool:返回一个可根据实际情况调整线程个数的线程池,不限制最大线程数量,若用空闲的线程则执行任务,若无任务则不创建线程。并且每一个空闲线程会在 60 秒后自动回收。
newScheduledThreadPool: 创建一个可以指定线程的数量的线程池,但是这个线程池还带有延迟和周期性执行任务的功能,类似定时器。
 
ThreadpoolExecutor
上面提到的四种线程池的构建,都是基于 ThreadpoolExecutor 来构建的,小伙伴们打起精神来了,接下来将一起了解一下面试官最喜欢问到的一道面试题“请简单说下你知道的线程池和ThreadPoolThread 有哪些构造参数:
 
1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
       return new ThreadPoolExecutor(nThreads, nThreads,
               0L, TimeUnit.MILLISECONDS,
               new LinkedBlockingQueue<Runnable>());
 }
ThreadpoolExecutor 有多个重载的构造方法,我们可以基于它最完整的构造方法来分析,先来解释一下每个参数的作用,稍后我们在分析源码的过程中再来详细了解参数的意义。
1
2
3
4
5
6
7
public ThreadPoolExecutor(int corePoolSize, //核心线程数量
                             int maximumPoolSize, //最大线程数
                             long keepAliveTime, //超时时间,超出核心线程数量以外的线程空余存活时间
                             TimeUnit unit, //存活时间单位
                             BlockingQueue<Runnable> workQueue, //保存执行任务的队列
                             ThreadFactory threadFactory,//创建新线程使用的工厂
                             RejectedExecutionHandler handler //当任务无法执行的时候的处理方式)
线程池初始化以后做了什么事情
线程池初始化时是没有创建线程的,线程池里的线程的初始化与其他线程一样,但是在完成任务以后,该线程不会自行销毁,而是以挂起的状态返回到线程池。直到应用程序再次向线程池发出请求时,线程池里挂起的线程就会再度激活执行任务。这样既节省了建立线程所造成的性能损耗,也可以让多个任务反复重用同一线程,从而在应用程序生存期内节约大量开销。
newFixedThreadPool
1
2
3
4
5
public static ExecutorService newFixedThreadPool(int nThreads) {
       return new ThreadPoolExecutor(nThreads, nThreads,
               0L, TimeUnit.MILLISECONDS,
               new LinkedBlockingQueue<Runnable>());
   }
FixedThreadPool 的核心线程数和最大线程数都是指定值,也就是说当线程池中的线程数超过核心线程数后,任务都会被放到阻塞队列中。另外 keepAliveTime 为 0,也就是超出核心线程数量以外的线程空余存活时间而这里选用的阻塞队列是 LinkedBlockingQueue,使用的是默认容量 Integer.MAX_VALUE,相当于没有上限。
这个线程池执行任务的流程如下:
1. 线程数少于核心线程数,也就是设置的线程数时,新建线程执行任务2. 线程数等于核心线程数后,将任务加入阻塞队列。
3. 由于队列容量非常大,可以一直添加。
4. 执行完任务的线程反复去队列中取任务执行用途:FixedThreadPool 用于负载比较大的服务器,为了资源的合理利用,需要限制当前线程数量。
newCachedThreadPool
1
2
3
4
5
public static ExecutorService newCachedThreadPool() {
       return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
               60L, TimeUnit.SECONDS,
               new SynchronousQueue<Runnable>());
   }
CachedThreadPool 创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程; 并且没有核心线程,非核心线程数无上限,但是每个空闲
的时间只有 60 秒,超过后就会被回收。
它的执行流程如下:
1. 没有核心线程,直接向 SynchronousQueue 中提交任务。
2. 如果有空闲线程,就去取出任务执行;如果没有空闲线程,就新建一个。
3. 执行完任务的线程有 60 秒生存时间,如果在这个时间内可以接到新任务,就可以继续活下去,否则就被回收。
newSingleThreadExecutor
创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。
线程池的实现原理分析
线程池的基本使用我们都清楚了,接下来我们来了解一下线程池的实现原理ThreadPoolExecutor 是线程池的核心,提供了线程池的实现。ScheduledThreadPoolExecutor 继承了 ThreadPoolExecutor,并另外提供一些调度方法以支持定时和周期任务。Executers 是工具类,主要用来创建线程池对象我们把一个任务提交给线程池去处理的时候,线程池的处理过程是什么样的呢?首先直接来看看定义:
线程池原理分析(FixedThreadPool)
源码分析
execute
基于源码入口进行分析,先看 execute 方法:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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)) {//2.核心池已满,但任务队列未满,添加到队列中
           int recheck = ctl.get();
           //任务成功添加到队列以后,再次检查是否需要添加新的线程,因为已存在的线程可能被销毁了
           if (!isRunning(recheck) && remove(command))
               reject(command);//如果线程池处于非运行状态,并且把当前的任务从任务队列中移除成功,则拒绝该任务
           else if (workerCountOf(recheck) == 0)//如果之前的线程已被销毁完,新建一个线程
               addWorker(null, false);
       } else if (!addWorker(command, false)) //3.核心池已满,队列已满,试着创建一个新线程
           reject(command); //如果创建新线程失败了,说明线程池被关闭或者线程池完全满了,拒绝任务
   }
ctl 的作用
在线程池中,ctl 贯穿在线程池的整个生命周期中
ctl:private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0)); 
它是一个原子类,主要作用是用来保存线程数量和线程池的状态。我们来分析一下这段代码,其实比较有意思,他用到了位运算一个 int 数值是 32 个 bit 位,这里采用高 3 位来保存运行状态,低 29 位来保存线程数量。
我们来分析默认情况下,也就是 ctlOf(RUNNING)运行状态,调用了 ctlOf(int rs,int wc)方法;其中:
private static int ctlOf(int rs, int wc) { return rs | wc; }
其中 RUNNING =-1 << COUNT_BITS ; -1 左移 29 位. -1 的二进制是 32 个 1(1111 1111 1111 1111 1111 1111 1111 1111)-1 的二进制计算方法,原码是 1000…001 . 高位 1 表示符号位。然后对原码取反,高位不变得到 1111…110,然后对反码进行+1 ,也就是补码操作, 最后得到 1111…1111。
那么-1 <<左移 29 位, 也就是 【111】 表示; rs | wc 。二进制的 111 | 000 。得到的结果仍然是 111那么同理可得其他的状态的 bit 位表示 。
1
2
3
4
5
6
7
8
private static final int COUNT_BITS = Integer.SIZE - 3; //32-3
    private static final int CAPACITY = (1 << COUNT_BITS) - 1; //将 1 的二进制向右位移 29 位,再减 1 表示最大线程容量
    //运行状态保存在 int 值的高 3 位 (所有数值左移 29 位)
    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; //所有的任务都已结束,线程数量为 0,处于该状态的线程池即将调用 terminated()方法
    private static final int TERMINATED = 3 << COUNT_BITS;// terminated()方法执行完成
状态转化

addWorker

如果工作线程数小于核心线程数的话,会调用 addWorker,顾名思义,其实就是要创建一个工作线程。我们来看看源码的实现源码比较长,看起来比较唬人,其实就做了两件事。1)才用循环 CAS 操作来将线程数加 1;2)新建一个线程并启用。
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
private boolean addWorker(Runnable firstTask, boolean core) {
       retry: //goto 语句,避免死循环
       for (;;) {
           int c = ctl.get();
           int rs = runStateOf(c);
           /*如果线程处于非运行状态,并且 rs 不等于 SHUTDOWN 且 firstTask 不等于空且且workQueue 为空,直接返回 false(表示不可添加 work 状态)
           1. 线程池已经 shutdown 后,还要添加新的任务,拒绝
           2. (第二*//*个判断)SHUTDOWN 状态不接受新任务,但仍然会执行已经加入任务队列的任务,所以当进入 SHUTDOWN 状态,而传进来的任务为空,并且任务队列不为空的时候,是允许添加新线程的,如果把这个条件取反,就表示不允许添加 worker*/
           if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()))
               return false;
           for (;;) { //自旋
               int wc = workerCountOf(c);//获得 Worker 工作线程数
               //如果工作线程数大于默认容量大小或者大于核心线程数大小,则直接返回 false 表示不能再添加 worker。
               if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
                   return false;
               if (compareAndIncrementWorkerCount(c))//通过 cas 来增加工作线程数,如果 cas 失败,则直接重试
               break retry;
               c = ctl.get(); // Re-read ctl //再次获取 ctl 的值
               if (runStateOf(c) != rs) //这里如果不想等,说明线程的状态发生了变化,继续重试
               continue retry;
           }
       }
       //上面这段代码主要是对 worker 数量做原子+1 操作,下面的逻辑才是正式构建一个 worker
       boolean workerStarted = false; //工作线程是否启动的标识
       boolean workerAdded = false; //工作线程是否已经添加成功的标识
       Worker w = null;
       try {
           w = new Worker(firstTask); //构建一个 Worker,这个 worker 是什么呢?我们可以看到构造方法里面传入了一个 Runnable 对象
           final Thread t = w.thread; //从 worker 对象中取出线程
           if (t != null) {
               final ReentrantLock mainLock = this.mainLock;
               mainLock.lock(); //这里有个重入锁,避免并发问题
               try {
                   int rs = runStateOf(ctl.get());
                   //只有当前线程池是正在运行状态,[或是 SHUTDOWN 且 firstTask 为空],才能添加到 workers 集合中
                   if (rs < SHUTDOWN ||
                           (rs == SHUTDOWN && firstTask == null)) {
                       //任务刚封装到 work 里面,还没 start,你封装的线程就是 alive,几个意思?肯定是要抛异常出去的
                       if (t.isAlive())
                           throw new IllegalThreadStateException();
                       workers.add(w); //将新创建的 Worker 添加到 workers 集合中
                       int s = workers.size();
                       //如果集合中的工作线程数大于最大线程数,这个最大线程数表示线程池曾经出现过的最大线程数
                       if (s > largestPoolSize)
                           largestPoolSize = s; //更新线程池出现过的最大线程数
                       workerAdded = true;//表示工作线程创建成功了
                   }
               } finally {
                   mainLock.unlock(); //释放锁
               }
               if (workerAdded) {//如果 worker 添加成功
                   t.start();//启动线程
                   workerStarted = true;
               }
           }
       } finally {
           if (! workerStarted)
               addWorkerFailed(w); //如果添加失败,就需要做一件事,就是递减实际工作线程数(还记得我们最开始的时候增加了工作线程数吗)
       }
       return workerStarted;//返回结果
   }

 Worker 类说明

 我们发现 addWorker 方法只是构造了一个 Worker,并且把 firstTask 封装到 worker 中,它是做什么的呢?我们来看看:
1. 每个 worker,都是一条线程,同时里面包含了一个 firstTask,即初始化时要被首先执行的任务。
2. 最终执行任务的,是 runWorker()方法。
Worker 类继承了 AQS,并实现了 Runnable 接口,注意其中的 firstTask 和 thread 属性:firstTask 用它来保存传入的任务;thread 是在调用构造方法时通过 ThreadFactory 来创建的线程,是用来处理任务的线程。在调用构造方法时,需要传入任务,这里通过 getThreadFactory().newThread(this);来新建一个线程,newThread 方法传入的参数是 this,因为 Worker 本身继承了 Runnable 接口,也就是一个线程,所以一个 Worker 对象在启动的时候会调用 Worker 类中的 run 方法。Worker 继承了 AQS,使用 AQS 来实现独占锁的功能。为什么不使用 ReentrantLock 来实现呢?可以看到 tryAcquire 方法,它是不允许重入的,而 ReentrantLock 是允许重入的:
lock 方法一旦获取了独占锁,表示当前线程正在执行任务中;那么它会有以下几个作用:
1. 如果正在执行任务,则不应该中断线程;
2. 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断;
3. 线程池在执行 shutdown 方法或 tryTerminate 方法时会调用 interruptIdleWorkers 方法来中断空闲的线程,interruptIdleWorkers 方法会使用 tryLock 方法来判断线程池中的线程是否是空闲状态;
4. 之所以设置为不可重入,是因为我们不希望任务在调用像 setCorePoolSize 这样的线程池控制方法时重新获取锁,这样会中断正在运行的线程;
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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
private final class Worker extends AbstractQueuedSynchronizer implements Runnable {
        private static final long serialVersionUID = 6138294804551838833L;
        /**
         * Thread this worker is running in. Null if factory fails.
         */
        final Thread thread; //注意了,这才是真正执行 task 的线程,从构造函数可知是由ThreadFactury 创建的
        /**
         * Initial task to run. Possibly null.
         */
        Runnable firstTask; //这就是需要执行的 task
        /**
         * Per-thread task counter
         */
        volatile long completedTasks; //完成的任务数,用于线程池统计
 
        Worker(Runnable firstTask) {
            setState(-1); //初始状态 -1,防止在调用 runWorker(),也就是真正执行 task前中断 thread。
            this.firstTask = firstTask;
            this.thread = getThreadFactory().newThread(this);
        }
 
        public void run() {
            runWorker(this);
        }
 
        protected boolean isHeldExclusively() {
            return getState() != 0;
        }
 
        protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }
 
        protected boolean tryRelease(int unused) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }
 
        public void lock() {
            acquire(1);
        }
 
        public boolean tryLock() {
            return tryAcquire(1);
        }
 
        public void unlock() {
            release(1);
        }
 
        public boolean isLocked() {
            return isHeldExclusively();
        }
 
        void interruptIfStarted() {
            Thread t;
            if (getState() >= 0 && (t = thread) != null
                    && !t.isInterrupted()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }
    }
posted @   47号Gamer丶  阅读(195)  评论(0编辑  收藏  举报
编辑推荐:
· 从 HTTP 原因短语缺失研究 HTTP/2 和 HTTP/3 的设计差异
· AI与.NET技术实操系列:向量存储与相似性搜索在 .NET 中的实现
· 基于Microsoft.Extensions.AI核心库实现RAG应用
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
阅读排行:
· winform 绘制太阳,地球,月球 运作规律
· 超详细:普通电脑也行Windows部署deepseek R1训练数据并当服务器共享给他人
· TypeScript + Deepseek 打造卜卦网站:技术与玄学的结合
· AI 智能体引爆开源社区「GitHub 热点速览」
· 写一个简单的SQL生成工具
点击右上角即可分享
微信分享提示