【并发】Java并发线程池底层原理详解与源码分析(下)

【并发】Java并发线程池底层原理详解与源码分析(下)

前情回顾

上篇文章地址

遗留问题解析

手动实现线程池代码

运行结果

!!!先说结论!!!

图解 ThreadPoolExecutor  

ThreadPoolExecutor 源码分析

线程池源码结构

【面试题】 execute()方法与submit()方法有什么区别???

ThreadPoolExecutor 变量详解 

ThreadPoolExecutor 之 execute()方法详解 

(1)添加核心线程执行任务

(2)任务入队(队列) 

(3)添加临时线程执行任务,如果失败则执行拒绝策略  

【问】这里为什么需要二次确认???

ThreadPoolExecutor 之 addWorker()方法详解  

(1)goto部分 做“check”

(2)“干事情”

ThreadPoolExecutor 之 runWorker()方法详解 

执行优先级源码实现

线程复用机制源码实现 


【并发】Java并发线程池底层原理详解与源码分析(下)

前情回顾

上篇文章地址

【并发】Java并发线程池底层原理详解与源码分析(上)_面向鸿蒙编程的博客-CSDN博客线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。Executors 返回的线程池对象的弊端:1) FixedThreadPool 和 SingleThreadPool:允许的请求队列长度为 MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。2.CachedThreadPool:允许的创建线程数量为 MAX_VALUE,可能会创建大量建大量的线程,从而导致 OOMhttps://blog.csdn.net/weixin_43715214/article/details/128045130

遗留问题解析

手动实现线程池代码

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 自定义线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(10, 20,
                0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(10));
        for (int i = 1; i <= 100; i++) {
            threadPoolExecutor.execute(new MyTask(i));
        }
    }
}
 
class MyTask implements Runnable {
    int i = 0;
 
    public MyTask(int i) {
        this.i = i;
    }
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "程序员做第" + i + "个项目");
        try {
            Thread.sleep(3000L);//业务逻辑
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

运行结果

我们在上一个章节中提到了手动实现线程池,但是它的结果似乎有点匪夷所思! 

为什么11~20会在最后面?

为什么运行会抛异常?

为什么打印日志只有前30个?

!!!先说结论!!!

在案例代码中,有100个任务,1个任务的处理时间大致需要3秒,10个核心线程,10个临时线程,阻塞队列的容量是10。

这里只会打印会前30个任务(10+10+10=30),由于在3s核心线程临时线程都在忙碌中,队列也,按照ThreadPoolExecutor默认的策略会抛出异常

按照线程池的工作顺序,会先分配10个核心线程(1~10),再装满队列(11~20),最后分配临时线程(21~30);执行逻辑是核心线程临时线程会先把“手头上”的任务处理完,才会去处理队列里的任务,这就是队列里的任务(11~20)最后打印的原因!!!

图解 ThreadPoolExecutor  

我们可以先来研究一下,线程池逻辑结构以及它的执行流程,以便于我们可以更加轻松的看懂源码!

下图是线程池接收任务的顺序

先装满核心线程 -> 再装满队列 -> 最后是临时线程

ThreadPoolExecutor 运行完整的流程

ThreadPoolExecutor 源码分析

接下来,我将会结合线程池ThreadPoolExecutor源码来解析这个问题!!! 

线程池源码结构

 顶层接口是Executor,它只有一个方法execute()ExecutorService接口继承了Executor接口,它主要是提供了submit()

【面试题】 execute()方法与submit()方法有什么区别???

submit底层还是调用execute

(1)关于返回值的问题

  • submit:有返回值,返回值(包括异常)被封装于FutureTask对象。适用于有返回结果的任务
  • execute:void类型的函数,没有返回值,适用于没有返回的任务

(2)关于异常处理的问题

  • submit:submit的时候并不会抛出异常(此时线程可能处于就绪状态)。只有在get操作的时候会抛出。因为get操作会阻塞等待线程的执行完毕。
  • execute在执行的时候会直接抛出。可以通过实现 UncaughtExceptionHandler接口来完成异常的捕获。

ThreadPoolExecutor 变量详解 

// 记录线程池的状态信息,高三位是状态信息,其余位表示工作的worker数量
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

// 表示worker数量
private static final int COUNT_BITS = Integer.SIZE - 3;

// worker容量
private static final int CAPACITY = (1 << COUNT_BITS) - 1;

// 可以接受新的任务,也可以处理队列中的任务
private static final int RUNNING    = -1 << COUNT_BITS;
// 不接受新的任务,但是可以处理队列里的任务
private static final int SHUTDOWN   =  0 << COUNT_BITS;

// 后面3个都不行!!!
private static final int STOP       =  1 << COUNT_BITS;
private static final int TIDYING    =  2 << COUNT_BITS;
private static final int TERMINATED =  3 << COUNT_BITS;

ThreadPoolExecutor 之 execute()方法详解 

由于 ThreadPoolExecutor 类封装得很好,所以exectue()看起来很简单,只有短短的几行代码。

这几行代码归纳起来其实只有4个步骤

public void execute(Runnable command) {
    // 1. 任务非空校验
    if (command == null)
        throw new NullPointerException();

    // 2. 添加核心线程执行任务
    int c = ctl.get();
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }

    // 3. 任务入队(队列)
    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);
    }
    
    // 4. 添加临时线程执行任务,如果失败则执行拒绝策略 
    else if (!addWorker(command, false))
        reject(command);
}

看到这里,相信大家应该可以将其与上文中提到的任务提交优先级联系起来!

先装满核心线程 -> 再装满队列 -> 最后是临时线程

除去第一步,校验是否为空值的步骤外,实际上核心的步骤也就3步! 

注:3个步骤中都有 addWorker(X,X) 的操作!但是它们参数都不同,所以它们代表的含义也各不相同,稍后会详解这个方法!

(1)添加核心线程执行任务

int c = ctl.get();
if (workerCountOf(c) < corePoolSize) {
    if (addWorker(command, true))
        return;

    // 如果addWorker失败,则ctl要重新获取。
    // 因为不管是状态变,还是worker数量变,ctl都已经变了,你需要重新获取最新值
    c = ctl.get();
}
// 初始值 CAPACITY = 536870911
// 00011111 11111111 11111111 11111111
private static final int CAPACITY   = (1 << COUNT_BITS) - 1;

// c的初始值 -536870912
// 转换位二进制就是 11100000000000000000000000000000
// 11100000000000000000000000000001(+1)
// 11100000000000000000000000000010(+1)
private static int workerCountOf(int c)  { return c & CAPACITY; }

int是32位了,这里用高3位表示状态,低29位表示线程数量! 

workerCountOf(c)用于计算当前线程池中正在工作的线程数,如果计算的数量大于等于核心线程数,则会执行后面的逻辑!

(2)任务入队(队列) 

在当前worker数量大于等于corePoolSize 或者 上面的addWorker()失败之后才会走到这里! 

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);
}
private static boolean isRunning(int c) {
    return c < SHUTDOWN;
}

boolean offer(E e);

查看一下当前线程的状态是否是running状态;如果是,则会执行offer()操作,尝试往工作队列里面添加这个任务,但是有可能失败(工作队列可能满了) 

public boolean remove(Runnable task) {
    boolean removed = workQueue.remove(task);
    tryTerminate(); // In case SHUTDOWN and now empty
    return removed;
}

重新获取一次ctl的值,做二次确认;如果此时状态不是running,就对之前的任务执行remove()操作,并抛异常!

else if (workerCountOf(recheck) == 0)
    addWorker(null, false);

当代码走到这里的时候,说明之前的offer()操作肯定是成功了,  这个判断是为了防止没有worker,但是队列里面有任务,没人执行!!!

addWorker(null, false),这个方法执行时只是创建了一个新的线程,但是没有传入任务,这是因为前面已经将任务添加到队列中了,这样可以防止线程池处于 running 状态,但是没有线程去处理这个任务。

(3)添加临时线程执行任务,如果失败则执行拒绝策略  

else if (!addWorker(command, false))
    reject(command);

执行到这里,说明核心线程此时都在工作,并且队列也满了!现在要申请让临时线程开始工作。

【问】这里为什么需要二次确认???

在多线程环境下,线程池的状态时刻在变化,而 ctl.get() 是非原子操作,很有可能刚获取了线程池状态后线程池状态就改变了。判断是否将 command 加入 队列是线程池之前的状态。倘若没有 二次确认,万一线程池处于非 running 状态(在多线程环境下很有可能发生),那么 command 永远不会执行

ThreadPoolExecutor 之 addWorker()方法详解  

private boolean addWorker(Runnable firstTask, boolean core) {
    retry:
    for (;;) {
        // 每次for循环都需要获取最新的ctl值
        int c = ctl.get();
        
        // 获取这个线程的状态
        int rs = runStateOf(c);

        if (rs >= SHUTDOWN &&
                !(rs == SHUTDOWN &&
                    firstTask == null &&
                    !workQueue.isEmpty()))
            return false;

        for (;;) {
            int wc = workerCountOf(c);
            if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            if (compareAndIncrementWorkerCount(c))
                break retry;
            c = ctl.get();  // Re-read ctl
            if (runStateOf(c) != rs)
                continue retry;
        }
    }

    boolean workerStarted = false;
    boolean workerAdded = false;
    ThreadPoolExecutor.Worker w = null;
    try {
        w = new ThreadPoolExecutor.Worker(firstTask);
        final Thread t = w.thread;
        if (t != null) {
            final ReentrantLock mainLock = this.mainLock;
            mainLock.lock();
            try {
                int rs = runStateOf(ctl.get());

                if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                    if (t.isAlive()) 
                        throw new IllegalThreadStateException();
                    workers.add(w);
                    int s = workers.size();
                    if (s > largestPoolSize)
                        largestPoolSize = s;
                    workerAdded = true;
                }
            } finally {
                mainLock.unlock();
            }
            if (workerAdded) {
                t.start();
                workerStarted = true;
            }
        }
    } finally {
        if (! workerStarted)
            addWorkerFailed(w);
    }
    return workerStarted;
}

addWorker(Runnable firstTask, boolean core)

这里面有两个入参 firstTask、core

  • firstTask 即是当前添加的线程需要执行的首个任务.
  • core 用来标记当前执行的线程是否是核心线程

上述代码在exectue()中被调用了3次,但是每次入参都不同!

  • addWorker(command, true)开启核心线程执行任务
  • addWorker(null, false) 创建一个线程,从队列中获取任务
  • addWorker(command, false)开启临时线程来执行任务

代码比较长,我们将代码分成两个部分来看

(1)goto部分 做“check”

retry:
for (;;) {

    int c = ctl.get();

    // 获取当前线程的状态
    int rs = runStateOf(c);

    // 不需要临时线程
    // 相当于rs >= shutdown && (rs != shutdown || firstTask != null || workQueue.isEmpty())
    if (rs >= SHUTDOWN &&
        ! (rs == SHUTDOWN &&
           firstTask == null &&
           ! workQueue.isEmpty()))
        return false;

    for (;;) {
        // 判断容量
        int wc = workerCountOf(c);
        if (wc >= CAPACITY ||
            wc >= (core ? corePoolSize : maximumPoolSize))
            return false;

	    // 尝试通过CAS方式,如果前面没有问题,这里自增+1
        if (compareAndIncrementWorkerCount(c))
            // 自增后退出循环,执行后面逻辑!
            break retry;
        c = ctl.get();  

        // 如果线程池状态发生变化,重新从最外层循环
        if (runStateOf(c) != rs)
            continue retry;
    }
}

判断是否要开临时线程

if (rs >= SHUTDOWN && 
        !(rs == SHUTDOWN && 
          firstTask == null &&
          !workQueue.isEmpty()))
        return false;

这个代码有一点绕!翻译过来其实就是:

rs >= shutdown && (rs != shutdown || firstTask != null || workQueue.isEmpty()) 

  1. rs > shutdown:线程池状态处于STOP,TIDYING,TERMINATED时,添加工作线程失败,不接受新任务
  2. rs >= shutdown && firstTask != null:线程池状态处于 SHUTDOWN,STOP,TIDYING,TERMINATED状态且worker的首个任务不为空时,添加工作线程失败,不接受新任务。
  3. rs >= shutdown && workQueue.isEmppty:线程池状态处于 SHUTDOWN,STOP,TIDYING,TERMINATED状态且阻塞队列为空时,添加工作线程失败,不接受新任务。

当满足这3种情况的时候,就不要 “加 Worker” 了!!!

判断容量

int wc = workerCountOf(c);
if (wc >= CAPACITY ||
    wc >= (core ? corePoolSize : maximumPoolSize))
    return false;
  1. 工作线程数量是否超过可表示的最大容量(CAPACITY)
  2. 如果添加核心线程,是否超过最大核心线程容量(corePoolSize)
  3. 如果添加临时线程,是否超过线程池最大线程容量(maximumPoolSize) 

如果容量符合“标准”,就会执行后面的CAS操作;如果CAS执行成功,退出循环,进入第二阶段!

private boolean compareAndIncrementWorkerCount(int expect) {
    return ctl.compareAndSet(expect, expect + 1);
}

public final boolean compareAndSet(int expect, int update) {
    return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}

(2)“干事情”

// 注意 Worker代表线程,Task表示任务
boolean workerStarted = false;
boolean workerAdded = false;
Worker w = null;

try {
    //创建Worker对象实例
    w = new Worker(firstTask);

    //获取Worker对象里的线程
    final Thread t = w.thread;

    if (t != null) {

        //开启可重入锁,独占
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            //获取线程池运行状态
            int rs = runStateOf(ctl.get());

            //满足 rs < SHUTDOWN 判断线程池是否是RUNNING,或者
            //rs == SHUTDOWN && firstTask == null 线程池如果是SHUTDOWN,
            //且首个任务firstTask为空,
            if (rs < SHUTDOWN ||
                (rs == SHUTDOWN && firstTask == null)) {
                if (t.isAlive()) 
                    throw new IllegalThreadStateException();

                //将Worker实例加入线程池workers
                workers.add(w);
                int s = workers.size();
                if (s > largestPoolSize)
                    largestPoolSize = s;

                //线程添加成功标志位 -> true
                workerAdded = true;
            }
        } finally {
            //释放锁
            mainLock.unlock();
        }

        //如果worker实例加入线程池成功,则启动线程,同时修改线程启动成功标志位 -> true
        if (workerAdded) {
            // 执行 start 会调 run方法!!!
            t.start();
            workerStarted = true;
        }
    }
} finally {
    if (! workerStarted)
        //添加线程失败 回滚
        addWorkerFailed(w);
}
return workerStarted;

最核心的部分如下所示 

try {
    int rs = runStateOf(ctl.get());
    if (rs < SHUTDOWN || (rs == SHUTDOWN && firstTask == null)) {
        // 当前线程还没有启动,但是它却是存活状态,就会抛异常!!!
        if (t.isAlive()) 
            throw new IllegalThreadStateException();
        workers.add(w);
        int s = workers.size();
        if (s > largestPoolSize)
            largestPoolSize = s;
            workerAdded = true;
    }
} finally {
    mainLock.unlock();
}
  1. 首先检查线程池的状态,当线程处于 RUNNING 状态 或者 线程处于 SHUTDOWN 状态当前线程的 firstTask 为空,满足以上条件时才能将 worker 实例添加进线程池,即workers.add(w);
  2. 同时修改 largestPoolSize,largestPoolSize变量用于记录目前最大线程数
  3. 将标志位 workerAdded 设置为 true,表示添加线程成功
  4. 无论成功与否,在 finally 中都必须执行 mainLock.unlock()来释放锁
if (workerAdded) {
    t.start();
    workerStarted = true;
}

当上面的条件都满足的时候,会执行 start() 方法,会调 run方法!!! 

ThreadPoolExecutor 之 runWorker()方法详解 

在线程池中,其实还有两个概念——提交优先级执行优先级 

上面的源码分析,其中 exectue() addWorker() 提交部分;而这里的 runWorker() 执行部分

在上面的addWorker()方法中调了start()方法,JVM会通过 start()来调用run()方法!!!自然最后会调到runWorker()

if (workerAdded) {
    // 执行 start 会调 run方法!!!
    t.start();
    workerStarted = true;
}

public void run() {
    runWorker(this);
}
final void runWorker(ThreadPoolExecutor.Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); 
    boolean completedAbruptly = true;
    try {
        // 执行优先级!!!
        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);
                Throwable thrown = null;
                try {
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}

执行优先级源码实现

while (task != null || (task = getTask()) != null) {...}

为什么说这里可以将执行优先级体现得“淋漓尽致”呢???

这个task表示任务,当 task 为null的时候,才会执行 getTask() 即从队列中获取任务,并交给线程处理!

这里又一次的说明了 addWorker(null,false) 的作用!!! 

所以,总的来说,这里的逻辑是核心线程和临时线程先处理完自己手头上的任务后,才会去线程池里拿!!! 

beforeExecute(wt, task);
afterExecute(task, thrown);

上面的代码中可以看到有beforeExecuteafterExecute,它们都是钩子函数,可以分别在子类中重写它们用来扩展 ThreadPoolExecutor,例如添加日志、计时、监视或者统计信息收集的功能

线程复用机制源码实现 

processWorkerExit(w, completedAbruptly);

private void processWorkerExit(ThreadPoolExecutor.Worker w, boolean completedAbruptly) {
    if (completedAbruptly) 
        decrementWorkerCount();

    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        completedTaskCount += w.completedTasks;
        workers.remove(w);
    } finally {
        mainLock.unlock();
    }

    tryTerminate();

    int c = ctl.get();
    if (runStateLessThan(c, STOP)) {
        if (!completedAbruptly) {
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
            if (workerCountOf(c) >= min)
                return; 
        }
        // 线程复用!!!
        addWorker(null, false);
    }
}

在 runWorker() 代码的最后的finally里面,会调 processWorkerExit() 方法,这个方法非常的重要!因为线程池的复用机制就是在这里体现的!!!

最后这里调了 addWorker(null, false) 表示会创建一个新的线程(但是没有任务),其实就是表示当前线程将自己之前手头上的活处理完了,现在又可以接活了!!!

posted @ 2022-11-27 20:31  金鳞踏雨  阅读(26)  评论(0编辑  收藏  举报  来源