【Java 线程池】【四】ThreadPoolExector中的Worker工作者原理

发布时间 2023-04-11 21:24:37作者: 酷酷-

1  前言

上一节我们看了ThreadPoolExecutor线程池的execute内部方法流程,addWorker方法流程,看到Worker是线程池内部的工作者,每个Worker内部持有一个线程,addWorker方法创建了一个Worker工作者,并且放入HashSet的容器中,那么这节我们就来看看Worker是如何工作的。

2  内部属性

我们先来看下Worker的属性:

private final class Worker
    extends AbstractQueuedSynchronizer
    implements Runnable
{
    // Worker内部持有的工作线程,就是依靠这个线程来不断运行线程池中的一个个task的
    final Thread thread;
    // 提交给这个Worker的首个任务
    Runnable firstTask;
    // 一个统计变量,记录着这个worker总共完成了多少个任务
    volatile long completedTasks;

    // 构造方法,创建Worker的时候给这个worker传入第一个运行的任务
    Worker(Runnable firstTask) {
        // 初始化AQS内部的state资源变量为-1
        setState(-1);
        // 保存一下首个任务
        this.firstTask = firstTask;
        // 使用线程工厂创建出来一个线程,这个线程负责运行任务
        this.thread = getThreadFactory().newThread(this);
    }

    // 内部的run方法,这个方法执行一个个任务
    public void run() {
        // runWorker方法,去运行一个个task
        runWorker(this);
    }

    // 实现AQS的互斥锁,这里是否持有互斥锁,不等于0就是持有
    protected boolean isHeldExclusively() {
        return getState() != 0;
    }

    // 实现AQS加互斥锁逻辑,就是CAS将state从0设置为1,成功就获取锁
    protected boolean tryAcquire(int unused) {
        if (compareAndSetState(0, 1)) {
            setExclusiveOwnerThread(Thread.currentThread());
            return true;
        }
        return false;
    }
    // 是新AQS释放互斥锁逻辑,就是将state变量从1设置为0,成功就释放锁成功
    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(); }
}

可以发现:
(1)Worker工作者继承了AQS,是一个同步器
(2)Worker实现了Runnable接口
为什么要这么做呢?我们往下看。

3  runWorker方法

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    // 获取worker的第一个任务firstTask,赋值给task(要运行的任务)
    Runnable task = w.firstTask;
    // firstTask传给task之后,设置firstTask为null (方便firstTask完成之后垃圾回收)
    w.firstTask = null;
    // 初始化一下,将w的同步器设置为解锁状态
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
        // 这里是重点
        // 假如task != null ,是因为上边将firstTask设置给了task,所以优先运行第一个任务firstTask
        // 假如task == null,那么调用getTask方法,从线程池的阻塞队列里面取任务出来
        while (task != null || (task = getTask()) != null) {
            // 运行任务之前需要进行加锁
            w.lock();
            // 这里就是校验一下线程池的状态
            // 如果是STOP、TIDYING、TERMINATED 需要中断一下当前线程wt
            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方法执行task任务
                    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之后,需要设置task为null,否则就会死循环不断运行
                task = null;
                // 将worker完成的任务数+1,
                w.completedTasks++;
                // 解锁
                w.unlock();
            }
        }
        
        // 如果走到这里,说明跳出了上面的while循环,当前worker需要进行销毁了
        completedAbruptly = false;
    } finally {
       // 销毁当前worker
        processWorkerExit(w, completedAbruptly);
    }
}

我们画个图理解一下:

上面的核心流程主要是:
(1)在一个while循环里面,不断的运行任务task,task 的来源可能有两种
(2)task可能是创建worker的时候传入的firstTask,或者是调用getTask方法从阻塞队列取出的task
(3)每次调用task.run方法执行任务之前,需要先加锁,然后运行task任务,然后释放锁锁
(4)每次循环前,如果获取运行的task任务为null,则需要跳出while循环,准备销毁这个worker了

4  getTask方法

private Runnable getTask() {
    boolean timedOut = false; // Did the last poll() time out?
    // 在一个循环里面,进行重试
    for (;;) {
        // 获取当前线程池的控制变量(包含线程池状态、线程数量)
        int c = ctl.get();
        // 获取当前线程池的状态
        int rs = runStateOf(c);

        // 如果当前线程池状态是STOP、TIDYING、TERMINATED,则说明线程池关闭了,直接返回null
        // 如果当前线程池状态为SHUTDOWN、并且阻塞队列是空,说明线程池即将关闭,并且没有多余要执行的任务了,直接返回ull
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }
        // 计算当前线程池中线程数量wc
        int wc = workerCountOf(c);

        // 注意这里的timed控制变量很重要,表示从阻塞队列中获取任务的时候,是一直阻塞还是具有超时时间的阻塞
        // (1)当前线程数量 wc > corePoolSize 的时候为true
        // 假如corePoolSize = 5, maximumPoolSize = 0, wc = 8
        // 当前线程数8 > corePoolSize,那么多出的这3个线程,在keepAliveTime时间内空闲就干掉
        // (2) 当allowCoreThreadTimeout 为true,则timed为true
        // 这里的意思是,线程池内的线程,只要超过keepAliveTime空闲的全部干掉
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        // wc > maximumPoolSize 正常情况不可能发生的
        // 这里的意思是如果超时了,超时了还取不到任务,就返回null
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            if (compareAndDecrementWorkerCount(c))
                return null;
            continue;
        }

        try {
            // 这的意思就是
            // 如果timed == true, 从阻塞队列取任务最多阻塞keepAliveTime时间,如果娶不到返回null
            // 如果timed == false。则调用take方法从阻塞队列取任务,一直阻塞,知道取到任务位置
            // 这里涉及的一些阻塞队列的知识,我们在上一篇并发容器的时候已经非常深入的分析过了
            Runnable r = timed ?
                // 调用poll方法,最多阻塞keepAliveTime时间
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                // 调用take方法,在取到任务之前会一直阻塞
                workQueue.take();
            // 如果从队列取到任务,直接返回
            if (r != null)
                return r;
            // 否则就是超时了
            timedOut = true;
        } catch (InterruptedException retry) {
            timedOut = false;
        }
    }
}

我们画个图理解一下:

上面的核心流程主要是:
(1)判断一下当前线程池的状态,如果是STOP、TIDYING、TERMINATED状态中的一种,那么直接返回null,别执行任务了,线程池就要销毁了,赶紧销毁掉所有的worker
(2)如果是SHUTDOWN,并且workerQueue阻塞队列是空,说明线程池即将关闭,并且没有多余的任务了,所以worker也可以被销毁了
(3)如果当前线程数量 wc > corePoolSize,也就是线程数量大于核心线程数,此时多出的这部分线程在keepAliveTimeout时间内没能从阻塞队列取出任务,则返回null,也要销毁这些多出的worker
(4)如果allowCoreThreadTimeout == true,这个表示允许销毁所有线程包括核心线程,就是任意一个线程超过keepAliveTimeout时间内没取到任务,就会被干掉。

5  空闲线程被销毁

最后看下Worker是怎么被销毁的:

private void processWorkerExit(Worker w, boolean completedAbruptly) {
    // completeAbruptly表示当前线程是否因为被中断而被销毁的
    // 如果是正常情况,因为keepAliveTimeout空闲而被销毁,则为false
    if (completedAbruptly)
        decrementWorkerCount();

    final ReentrantLock mainLock = this.mainLock;
    // 加锁
    mainLock.lock();
    try {
        // 计算一下线程池完成总的任务数量
        completedTaskCount += w.completedTasks;
        // 从HashSet容器中移除当前的worker
        workers.remove(w);
    } finally {
        // 释放锁
        mainLock.unlock();
    }
    
    // 尝试中止线程池,这里我们后面的章节再分析
    tryTerminate();

    int c = ctl.get();
    // 如果当前线程状态为RUNNING、SHUTDOWN
    if (runStateLessThan(c, STOP)) {
        if (!completedAbruptly) {
            // 这就是计算一下当前线程池允许的最小线程数
            // 正常情况是min=corePoolSize,但是当allowCoreThreadTimeout为true时候,允许销毁所有线程,则min=0
            int min = allowCoreThreadTimeOut ? 0 : corePoolSize;
            // 如果min = 0 并且 阻塞队列非空,说明还有任务没执行
            // 此时最少要保留1个线程去运行这些任务,不能销毁所有
            if (min == 0 && ! workQueue.isEmpty())
                min = 1;
             // 如果当前线程数量 >= min值,可以了,销毁动作结束了
            if (workerCountOf(c) >= min)
                return; // replacement not needed
        }
        // 说明completedAbruptly == true,说明可能是因为线程被中断(interrupted方法)而被销毁
        // 此时可能还有很多任务还没执行,需要加会容器里面
        addWorker(null, false);
    }
}

上面主要就是做了:
(1)如果是正常情况keepAliveTimeout空闲时间被销毁,则从HashSet容器里面移除即可
(2)如果当前线程池状态是RUNNING、SHUTDOWN,说明还有一些任务没执行。
从HashSet容器移除当前worker之后,需要判断一下如果worker是因为异常情况被中断,需要新创建worker来继续执行任务
我们这里看到worker继承了AQS,每次worker执行任务之前都需要lock加锁,这是为什么呢?那就需要看下interruptIdleWorker方法了,interruptIdleWorker方法是销毁空闲的线程的意思。

private void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        // 遍历容器中的所有worker
        for (Worker w : workers) {
            Thread t = w.thread;
            // 这里核心点就是要调用w.tryLock方法尝试获取锁
            if (!t.isInterrupted() && w.tryLock()) {
                try {
                    // 只有获取w工作者的互斥锁之后才能中断它
                    t.interrupt();
                } catch (SecurityException ignore) {
                } finally {
                    // 释放锁
                    w.unlock();
                }
            }
            if (onlyOne)
                break;
        }
    } finally {
        mainLock.unlock();
    }
}

我们可以发现:
(1)因为Worker执行每个task的之前,都要执行w.lock,对worker进行加锁,然后才能执行task任务。
(2)此时如果有别的线程要中断Worker,也需要获取w的互斥锁,执行w.tryLock()方法。
(3)如果执行tryLock加锁失败,说明当前Worker方法正在执行task,不允许中断,否则可能task执行一半,线程就被中断,导致一些数据异常问题。
所以说加锁就是为了防止Worker正在执行任务的时候,被人直接中断,导致异常。

其实ThreadPoolExecutor方法提供了直接暴力中断所有线程的方法,也就是不管当前Worker是否正在执行task任务,都会直接被中止掉的。而interruptIdleWorkers()这个方法是比较优雅的进行中断,中断worker之前,会先获取锁,如果失败则不允许中断。关于暴力中止线程池、以及其它中止线程池的方式,我们后面会说。

6  小结

到这里Worker的执行原理就看的差不多了,有理解不对的地方欢迎指正哈。