Java并发系列终结篇:彻底搞懂Java线程池的工作原理

    // ...省略校验相关代码

    

    this.corePoolSize = corePoolSize;

    this.maximumPoolSize = maximumPoolSize;

    this.workQueue = workQueue;

    this.keepAliveTime = unit.toNanos(keepAliveTime);

    this.threadFactory = threadFactory;

    this.handler = handler;

}



// ...    

}




这个构造方法中有7个参数之多,我们逐个来看每个参数所代表的含义:



*   **corePoolSize** 表示线程池的核心线程数。当有任务提交到线程池时,如果线程池中的线程数小于corePoolSize,那么则直接创建新的线程来执行任务。

*   **workQueue** 任务队列,它是一个阻塞队列,用于存储来不及执行的任务的队列。当有任务提交到线程池的时候,如果线程池中的线程数大于等于corePoolSize,那么这个任务则会先被放到这个队列中,等待执行。

*   **maximumPoolSize** 表示线程池支持的最大线程数量。当一个任务提交到线程池时,线程池中的线程数大于corePoolSize,并且workQueue已满,那么则会创建新的线程执行任务,但是线程数要小于等于maximumPoolSize。

*   **keepAliveTime** 非核心线程空闲时保持存活的时间。非核心线程即workQueue满了之后,再提交任务时创建的线程,因为这些线程不是核心线程,所以它空闲时间超过keepAliveTime后则会被回收。

*   **unit** 非核心线程空闲时保持存活的时间的单位

*   **threadFactory** 创建线程的工厂,可以在这里统一处理创建线程的属性

*   **handler** 拒绝策略,当线程池中的线程达到maximumPoolSize线程数后且workQueue已满的情况下,再向线程池提交任务则执行对应的拒绝策略



2.线程池工作流程

=========



线程池提交任务是从execute方法开始的,我们可以从execute方法来分析线程池的工作流程。



(1)当execute方法提交一个任务时,如果线程池中线程数小于corePoolSize,那么不管线程池中是否有空闲的线程,都会创建一个新的线程来执行任务。



![](https://img-blog.csdnimg.cn/20210712145222264.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dlaXhpbl81NzkwNzAyOA==,size_16,color_FFFFFF,t_70) 



(2)当execute方法提交一个任务时,线程池中的线程数已经达到了corePoolSize,且此时没有空闲的线程,那么则会将任务存储到workQueue中。



![](https://img-blog.csdnimg.cn/img_convert/d9779d2b3a8e41a3b76e82169e6ce947.png)



(3)如果execute提交任务时线程池中的线程数已经到达了corePoolSize,并且workQueue已满,那么则会创建新的线程来执行任务,但总线程数应该小于maximumPoolSize。 



(4)如果线程池中的线程执行完了当前的任务,则会尝试从workQueue中取出第一个任务来执行。如果workQueue为空则会阻塞线程。



![](https://img-blog.csdnimg.cn/img_convert/1c2cddba4b922b4a301dac3873937d94.png)



(5)如果execute提交任务时,线程池中的线程数达到了maximumPoolSize,且workQueue已满,此时会执行拒绝策略来拒绝接受任务。



![](https://img-blog.csdnimg.cn/img_convert/ccf4135834efe64536ecd31ff82e7b31.png)



(6)如果线程池中的线程数超过了corePoolSize,那么空闲时间超过keepAliveTime的线程会被销毁,但程池中线程个数会保持为corePoolSize。



![](https://img-blog.csdnimg.cn/img_convert/8ea28c544aa0ae9fa5ff3688f1fbc475.png)



(7)如果线程池存在空闲的线程,并且设置了allowCoreThreadTimeOut为true。那么空闲时间超过keepAliveTime的线程都会被销毁。



![](https://img-blog.csdnimg.cn/img_convert/238d66a64ef2536c0c2d56061ab88748.png)



  

3.线程池的拒绝策略



如果线程池中的线程数达到了maximumPoolSize,并且workQueue队列存储满的情况下,线程池会执行对应的拒绝策略。在JDK中提供了RejectedExecutionHandler接口来执行拒绝操作。实现RejectedExecutionHandler的类有四个,对应了四种拒绝策略。分别如下:



*   **DiscardPolicy** 当提交任务到线程池中被拒绝时,线程池会丢弃这个被拒绝的任务

*   **DiscardOldestPolicy** 当提交任务到线程池中被拒绝时,线程池会丢弃等待队列中最老的任务。

*   **CallerRunsPolicy** 当提交任务到线程池中被拒绝时,会在线程池当前正在运行的Thread线程中处理被拒绝额任务。即哪个线程提交的任务哪个线程去执行。

*   **AbortPolicy** 当提交任务到线程池中被拒绝时,直接抛出RejectedExecutionException异常。



三、线程池源码分析

=========



从上一章对线程池的工作流程解读来看,线程池的原理似乎并没有很难。但是开篇时我说过想要读懂线程池的源码并不难,主要原因是线程池内部运用到了大量并发相关知识,另外还与线程池中用到的位运算有关。



1.线程池中的位运算(了解内容)

================



在向线程池提交任务时有两个比较重要的参数会决定任务的去向,这两个参数分别是线程池的状态和线程池中的线程数。在ThreadPoolExecutor内部使用了一个AtomicInteger类型的整数ctl来表示这两个参数,代码如下:



public class ThreadPoolExecutor extends AbstractExecutorService {

// Integer.SIZE = 32.所以 COUNT_BITS= 29

private static final int COUNT_BITS = Integer.SIZE - 3;

// 00001111 11111111 11111111 11111111 这个值可以表示线程池的最大线程容量

private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;

// 将-1左移29位得到RUNNING状态的值

private static final int RUNNING    = -1 << COUNT_BITS;    

// 线程池运行状态和线程数

private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));



private static int ctlOf(int rs, int wc) { return rs | wc; }



// ...

}




因为涉及多线程的操作,这里为了保证原子性,ctl参数使用了AtomicInteger类型,并且通过ctlOf方法来计算出了ctl的初始值。如果你不了解位运算大概很难理解上述代码的用意。



我们知道,int类型在Java中占用4byte的内存,一个byte占用8bit,所以Java中的int类型共占用32bit。对于这个32bit,我们可以进行高低位的拆分。做Android开发的同学应该都了解View测量流程中的MeasureSpec参数,这个参数将32bit的int拆分成了高2位和低30位,分别表示View的测量模式和测量值。而这里的ctl与MeasureSpec类似,ctl将32位的int拆分成了高3位和低29位,分别表示线程池的运行状态和线程池中的线程个数。



下面我们通过位运算来验证一下ctl是如何工作的,当然,如果你不理解这个位运算的过程对理解线程池的源码影响并不大,所以对以下验证内容不感兴趣的同学可以直接略过。



可以看到上述代码中RUNNING的值为-1左移29位,我们知道在计算机中\*\*负数是以其绝对值的补码来表示的,而补码是由反码加1得到。\*\*因此-1在计算机中存储形式为1的反码+1



1的原码:00000000 00000000 00000000 00000001

                                        +

1的反码:11111111 11111111 11111111 11111110

   ---------------------------------------

-1存储: 11111111 11111111 11111111 11111111




接下来对-1左移29位可以得到RUNNING的值为:



// 高三位表示线程状态,即高三位为111表示RUNNING

11100000 00000000 00000000 00000000




而AtomicInteger初始线程数量是0,因此ctlOf方法中的“|”运算如下:



RUNNING: 11100000 00000000 00000000 00000000

                                           |

线程数为0: 00000000 00000000 00000000 00000000

      ---------------------------------------

得到ctl: 11100000 00000000 00000000 00000000




通过RUNNING|0(线程数)即可得到ctl的初始值。同时还可以通过以下方法将ctl拆解成运行状态和线程数:



// 00001111 11111111 11111111 11111111

private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;

// 获取线程池运行状态

private static int runStateOf(int c)     { return c & ~COUNT_MASK; }

// 获取线程池中的线程数

private static int workerCountOf(int c)  { return c & COUNT_MASK; }



假设此时线程池为RUNNING状态,且线程数为0,验证一下runStateOf是如何得到线程池的运行状态的:



COUNT_MASK: 00001111 11111111 11111111 11111111

~COUNT_MASK: 11110000 00000000 00000000 00000000

                                               &

ctl: 11100000 00000000 00000000 00000000

         ----------------------------------------

RUNNING: 11100000 00000000 00000000 00000000

复制代码




如果不理解上边的验证流程没有关系,只要知道通过runStateOf方法可以得到线程池的运行状态,通过workerCountOf可以得到线程池中的线程数即可。



接下来我们进入线程池的源码的源码分析环节。



2.ThreadPoolExecutor的execute

============================



向线程池提交任务的方法是execute方法,execute方法是ThreadPoolExecutor的核心方法,以此方法为入口来进行剖析,execute方法的代码如下:



public void execute(Runnable command) {

    if (command == null)

        throw new NullPointerException();

    // 获取ctl的值

    int c = ctl.get();

    // 1.线程数小于corePoolSize

    if (workerCountOf(c) < corePoolSize) {

        // 线程池中线程数小于核心线程数,则尝试创建核心线程执行任务

        if (addWorker(command, true))

            return;

        c = ctl.get();

    }

    // 2.到此处说明线程池中线程数大于核心线程数或者创建线程失败

    if (isRunning(c) && workQueue.offer(command)) {

        // 如果线程是运行状态并且可以使用offer将任务加入阻塞队列未满,offer是非阻塞操作。

        int recheck = ctl.get();

        // 重新检查线程池状态,因为上次检测后线程池状态可能发生改变,如果非运行状态就移除任务并执行拒绝策略

        if (! isRunning(recheck) && remove(command))

            reject(command);

        // 如果是运行状态,并且线程数是0,则创建线程

        else if (workerCountOf(recheck) == 0)

            // 线程数是0,则创建非核心线程,且不指定首次执行任务,这里的第二个参数其实没有实际意义

            addWorker(null, false);

    }

    // 3.阻塞队列已满,创建非核心线程执行任务

    else if (!addWorker(command, false))

        // 如果失败,则执行拒绝策略

        reject(command);

}



execute方法中的逻辑可以分为三部分:



*   1.如果线程池中的线程数小于核心线程,则直接调用addWorker方法创建新线程来执行任务。

*   2.如果线程池中的线程数大于核心线程数,则将任务添加到阻塞队列中,接着再次检验线程池的运行状态,因为上次检测过之后线程池状态有可能发生了变化,如果线程池关闭了,那么移除任务,执行拒绝策略。如果线程依然是运行状态,但是线程池中没有线程,那么就调用addWorker方法创建线程,注意此时传入任务参数是null,即不指定执行任务,因为任务已经加入了阻塞队列。创建完线程后从阻塞队列中取出任务执行。

*   3.如果第2步将任务添加到阻塞队列失败了,说明阻塞队列任务已满,那么则会执行第三步,即创建非核心线程来执行任务,如果非核心线程创建失败那么就执行拒绝策略。



可以看到,代码的执行逻辑和我们在第二章中分析的线程池的工作流程是一样的。



接下来看下execute方法中创建线程的方法addWoker,addWoker方法承担了核心线程和非核心线程的创建,通过一个boolean参数core来区分是创建核心线程还是非核心线程。先来看addWorker方法前半部分的代码:



// 返回值表示是否成功创建了线程

private boolean addWorker(Runnable firstTask, boolean core) {

    // 这里做了一个retry标记,相当于goto.

    retry:

    for (int c = ctl.get();;) {

        // Check if queue empty only if necessary.

        if (runStateAtLeast(c, SHUTDOWN)

            && (runStateAtLeast(c, STOP)

                || firstTask != null

                || workQueue.isEmpty()))

            return false;



        for (;;) {

            // 根据core来确定创建最大线程数,超过最大值则创建线程失败,注意这里的最大值可能有s三个corePoolSize、maximumPoolSize和线程池线程的最大容量

            if (workerCountOf(c)

                >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))

                return false;

            // 通过CAS来将线程数+1,如果成功则跳出循环,执行下边逻辑    

            if (compareAndIncrementWorkerCount(c))

                break retry;

            c = ctl.get();  // Re-read ctl

            // 线程池的状态发生了改变,退回retry重新执行

            if (runStateAtLeast(c, SHUTDOWN))

                continue retry;

        }

    }

    

    // ...省略后半部分

   

    return workerStarted;

}



这部分代码会通过是否创建核心线程来确定线程池中线程数的值,如果是创建核心线程,那么最大值不能超过corePoolSize,如果是创建非核心线程那么线程数不能超过maximumPoolSize,另外无论是创建核心线程还是非核心线程,最大线程数都不能超过线程池允许的最大线程数COUNT\_MASK(有可能设置的maximumPoolSize大于COUNT\_MASK)。如果线程数大于最大值就返回false,创建线程失败。



接下来通过CAS将线程数加1,如果成功那么就break retry结束无限循环,如果CAS失败了则就continue retry从新开始for循环,注意这里的retry不是Java的关键字,是一个可以任意命名的字符。



接下来,如果能继续向下执行则开始执行创建线程并执行任务的工作了,看下addWorker方法的后半部分代码:



private boolean addWorker(Runnable firstTask, boolean core) {

    // ...省略前半部分



    boolean workerStarted = false;

    boolean workerAdded = false;

    Worker w = null;

    try {

        // 实例化一个Worker,内部封装了线程

        w = new Worker(firstTask);

        // 取出新建的线程

        final Thread t = w.thread;

        if (t != null) {

            // 这里使用ReentranLock加锁保证线程安全

            final ReentrantLock mainLock = this.mainLock;

            mainLock.lock();

            try {

                int c = ctl.get();

                // 拿到锁湖重新检查线程池状态,只有处于RUNNING状态或者处于SHUTDOWN并且firstTask==null时候才会创建线程

                if (isRunning(c) ||

                    (runStateLessThan(c, STOP) && firstTask == null)) {

                    // 线程不是处于NEW状态,说明线程已经启动,抛出异常

                    if (t.getState() != Thread.State.NEW)

                        throw new IllegalThreadStateException();

                    // 将线程加入线程队列,这里的worker是一个HashSet   

                    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并开启线程执行任务的过程,Worker是对线程的封装,创建的worker会被添加到ThreadPoolExecutor中的HashSet中。也就是线程池中的线程都维护在这个名为workers的HashSet中并被ThreadPoolExecutor所管理,HashSet中的线程可能处于正在工作的状态,也可能处于空闲状态,一旦达到指定的空闲时间,则会根据条件进行回收线程。



我们知道,线程调用start后就会开始执行线程的逻辑代码,执行完后线程的生命周期就结束了,那么线程池是如何保证Worker执行完任务后仍然不结束的呢?当线程空闲超时或者关闭线程池又是怎样进行线程回收的呢?这个实现逻辑其实就在Worker中。看下Worker的代码:



private final class Worker

    extends AbstractQueuedSynchronizer

    implements Runnable

{

    // 执行任务的线程

    final Thread thread;

    // 初始化Worker时传进来的任务,可能为null,如果不空,则创建和立即执行这个task,对应核心线程创建的情况

    Runnable firstTask;



    Worker(Runnable firstTask) {

        // 初始化时设置setate为-1

        setState(-1); // inhibit interrupts until runWorker

        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;

    }

    // ...

}



Worker是位于ThreadPoolExecutor中的一个内部类,它继承了AQS,使用AQS来实现了独占锁的功能,但是并没支持可重入。这里使用不可重入的特性来表示线程的执行状态,即可以通过isHeldExclusively方法来判断,如果是独占状态,说明线程正在执行任务,如果非独占状态,说明线程处于空闲状态。关于AQS我们前边文章中已经详细分析过了,不了解AQS的可以翻看前边ReentranLock的文章。



另外,Worker还实现了Runnable接口,因此它的执行逻辑就是在run方法中,run方法调用的是线程池中的runWorker(this)方法。任务的执行逻辑就在runWorker方法中,它的代码如下:



final void runWorker(Worker w) {

    Thread wt = Thread.currentThread();

    // 取出Worker中的任务,可能为空

    Runnable task = w.firstTask;
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值