ReentrantLock

问题1

ReentrantLock 默认是非公平锁。如果在高并发场景下,把 ReentrantLock lock = new ReentrantLock(true) 改成公平锁,系统的吞吐量通常会发生断崖式下跌。

请从 JDK 8 源码的角度告诉我,非公平锁和公平锁在 lock() 以及 tryAcquire() 上的核心差异在哪?为什么非公平锁能带来巨大的性能优势?它又是如何对队列里的老线程进行“插队”的?


源码分析

对比 ReentrantLock 内部两个实现类 NonfairSync(非公平)和 FairSync(公平)的源码

非公平锁的加锁逻辑:

static final class NonfairSync extends Sync {
    final void lock() {
        // 【第一次插队】:不管队列里有没有人,一上来直接 CAS 抢锁!
        if (compareAndSetState(0, 1))
            setExclusiveOwnerThread(Thread.currentThread());
        else
            acquire(1); // 没抢到,才走 AQS 标准流程
    }

    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }
}

// Sync 内部的 nonfairTryAcquire
final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        // 【第二次插队】:如果锁刚好释放了,state 为 0,不管队列排了多长,直接 CAS 抢!
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
               int nextc = c + acquires;
              if (nextc < 0) // overflow
                  throw new Error("Maximum lock count exceeded");
              setState(nextc);
              return true;
          }
    return false;
}

公平锁的加锁逻辑:

static final class FairSync extends Sync {
    final void lock() {
        acquire(1); // 一上来老老实实走 AQS 流程,绝不盲目 CAS
    }

    protected final boolean tryAcquire(int acquires) {
        final Thread current = Thread.currentThread();
        int c = getState();
        if (c == 0) {
            // 【讲文明懂礼貌】:抢锁前先调用 hasQueuedPredecessors()!
            if (!hasQueuedPredecessors() &&
                compareAndSetState(0, acquires)) {
                setExclusiveOwnerThread(current);
                return true;
            }
        }
        else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0)
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
        return false;
    }
}

关键差异点:hasQueuedPredecessors()
公平锁在 state == 0 时,必须调用这个方法。它的核心逻辑是检查:“当前队列里,有没有人在我前面排队?” 如果有,哪怕锁现在是空闲的,也必须去队列尾部排队

为什么非公平锁性能高? 假设线程 A 持有锁,线程 B 没拿到,进入队列挂起了。 1. 当线程 A 释放锁的一瞬间,它会去唤醒队列里的线程 B。 2. 注意: 线程 B 从操作系统的 unpark 中醒来、完成上下文切换、重新投入运行,需要微秒级的时间。 3. 如果此时刚好有一个新来的线程 C 请求锁,非公平锁会直接把锁给线程 C。线程 C 在 CPU 高速缓存里直接运行。 4. 等线程 C 运行完了释放锁,线程 B 刚好醒来接盘。 结论就是:非公平锁充分利用了线程唤醒的“时间空档期”,极大地提高了 CPU 的利用率和系统整体吞吐量

结论:

公平锁与非公平锁的本质区别在于 是否允许“时序插队”。
非公平锁通过在 lock() 和 tryAcquire() 时进行两次暴力的 CAS 尝试,允许新来的线程去压榨“老线程被唤醒时的上下文切换耗时”,从而换取了极高的吞吐量,代价是可能导致队列中的老线程发生线程饥饿

问题2

ReentrantLock 支持同一个线程多次加锁而不死锁。

底层是怎么记录重入次数的?如果是同一个线程,在释放锁(unlock())时,需要调用多少次才能真正把锁释放掉?如果恶意代码疯狂重入,state 变量会发生什么?Doug Lea 考虑过溢出吗


源码分析

重入的逻辑完全由 AQS 的 state 字段承载。我们看 nonfairTryAcquire 中针对同线程的重入处理:

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    // ... state == 0 抢锁逻辑
    else if (current == getExclusiveOwnerThread()) { // 如果当前线程就是锁的持有者
        int nextc = c + acquires; // 计数器累加!
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded"); // 【严谨】:防御性编程,防止溢出
        setState(nextc); // 刷新 state
        return true;
    }
    return false;
}

再看释放锁的 tryRelease 方法:

protected final boolean tryRelease(int releases) {
    int c = getState() - releases; // 计数器递减
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException(); // 不是锁持有者来释放,直接抛异常
    
    boolean free = false;
    if (c == 0) {
        free = true; // 只有当 state 递减到 0 的时候,才说明锁被完全释放了!
        setExclusiveOwnerThread(null); // 清空锁的持有线程标志
    }
    setState(c); // 更新 state
    return free;
}

细节思考:
对称性:重入了几次,就必须 unlock() 几次。因为只有 state == 0 时,tryRelease 才会返回 true,AQS 才会去真正唤醒后续节点。
防御性编程:Doug Lea 考虑到了 int 类型的最高位是符号位。如果重入次数达到了 \(2^{31}-1\),再加 1 就会变成负数(nextc < 0)。这时候源码直接抛出 Error,阻止了因为状态位变负导致锁机制全面崩溃的灾难

结论

ReentrantLock 的可重入性是基于 AQS 的 state 状态位复用 与 独占线程所有权标志(exclusiveOwnerThread) 共同实现的。
它将锁的“占有状态”与“重入计数”合二为一,通过严谨的递增/递减校验以及防止 int 溢出的防御性报错,确保了重入逻辑在极端调用栈下的绝对安全

问题3

当 ReentrantLock 调用 unlock() 释放锁并准备唤醒下一个线程时,会进入 AQS 的 unparkSuccessor(Node node) 方法。 在这个方法里,如果要找下一个待唤醒的节点,如果 node.next 为空或者被取消了,源码会做出一个匪夷所思的动作:它不从前往后找,而是从队列的尾部(tail)开始,逆着链表往前(prev)遍历。为什么要大费周章地“从后往前”找?双向链表从前往后找不是更快吗?


源码分析

释放锁唤醒的核心源码:

private void unparkSuccessor(Node node) {
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    Node s = node.next; // 默认看紧挨着的下一个节点
    if (s == null || s.waitStatus > 0) { // 如果下一个节点不存在,或者被取消了(CANCELLED=1)
        s = null;
        // 【核心考点】:为什么从尾部 tail 开始向前遍历?
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t; // 找到离 node 最近的那个未取消的节点
    }
    if (s != null)
        LockSupport.unpark(s.thread); // 真正唤醒
}

为什么不能从前往后找?原因卡在“入队时的无锁并发”上!
我们来看新节点入队(enq(Node node))时的核心逻辑:

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t; // 【步骤 1】
            if (compareAndSetTail(t, node)) { // 【步骤 2】
                t.next = node; // 【步骤 3】
                return t;
            }
        }
    }
}

请仔细观察高并发下,一个节点入队时的三个指令顺序:

  1. 步骤 1:新节点的 prev 指向老尾节点。
  2. 步骤 2:通过 CAS 把 tail 指向新节点。
  3. 步骤 3:老尾节点的 next 指向新节点。 致命漏洞发生在这里:
    如果一个新节点刚刚完成了步骤 2,但还没来得及执行步骤 3。
    就在这个极其短暂的瞬间,锁持有者调用了 unlock() 进入了 unparkSuccessor()。 如果它尝试从前往后利用 node.next 遍历:
    由于老尾节点的 next 此时还是 null(步骤 3 还没执行),链表断裂了! 唤醒流程会认为后续没有节点了,导致刚刚入队的新节点永远收不到唤醒信号,死在队列里!
    而如果从后往前通过 tail 往前遍历:
    因为步骤 2 已经成功了,tail 已经指向了新节点,且新节点的 prev 在步骤 1 就已经稳稳地指向了前驱。从后往前的 prev 指针在 CAS 成功的那一刻是绝对完整、不会断裂的!

结论:

AQS 唤醒时从后向前(Tail-to-Head)遍历,是为了解决高并发入队时,next 指针赋值滞后导致的链表断裂问题。
Doug Lea 利用 prev 指针建立的先验原子性,确保了在无锁(CAS)构建双向链表的过程中,唤醒信号绝对不会因为时序交错而丢失

问题4

如果一个线程在 tryAcquire 里没拿到锁,接下来就会调用 addWaiter(Node.EXCLUSIVE)。

在 addWaiter 的源码里,Doug Lea 先写了一段 if (pred != null) 的 CAS 尝试,如果失败了,才调用 enq(node)。既然 enq(node) 内部也是一个死循环 CAS 入队,为什么要在外面多此一举地写一段一模一样的 CAS 代码?这种‘快速通道(Fast-Path)’的设计在极高并发下真的能提升性能吗


源码分析:

addWaiter的源码

private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode);
    // Try the fast path of enq; backup to full enq on failure
    Node pred = tail;
    // 【快速通道】:如果队列已经初始化过了(tail 不为空)
    if (pred != null) {
        node.prev = pred;
        // 尝试一次 CAS 把自己设为新的尾节点
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node; // 成功了直接返回,不用进 enq 的死循环
        }
    }
    // 【慢速通道 / 兜底通道】:如果队列未初始化,或者上面那次 CAS 被人抢夺失败了
    enq(node);
    return node;
}

enq(node)的源码

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // 【特权操作】:如果尾巴是空的,说明队列还没初始化,得先搞个傀儡 Head
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            // 这里和快速通道的代码一模一样
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = t; // 注:源码为 t.next = node;
                return t;
            }
        }
    }
}

为什么不直接进 enq?
这就是大师对 CPU 分支预测和指令周期的极致压榨。

高并发但无冲突的场景:当大量线程是有序或者错开交替拿锁失败时,队列已经初始化好。此时进入 addWaiter,直接走快速通道,一次 CAS 成功,直接返回。这就少经历了一次 for(;😉 循环的底层跳转指令和方法栈的开销。

为什么叫兜底(Full Enq):enq 方法不仅要处理 CAS 失败的重试,它还肩负着‘初始化队列’的重任(即无中生有地 new 一个空 Node 作为傀儡 Head)。快速通道把“初始化”这个极少发生的分支剥离出去,让主流程代码极其精简,有利于 JVM 进行 内联优化(Inline)。

结论:

addWaiter 采用了 “Fast-Path(快速路径)+ Slow-Path(兜底路径)” 的分流设计。
它将高频的“正常尾部追加”与低频的“队列初始化、并发冲突重试”解耦,在最大程度上减少了无冲突时的线程调用栈深度,是工业级高性能无锁组件的经典写法。

问题5

线程入队后,不能马上挂起,必须调用 shouldParkAfterFailedAcquire 判断是否需要挂起。

这个方法的名字叫‘失败后是否应该挂起’。为什么在这个方法里,线程需要顺着 prev 指针一路往前清理被取消的节点(CANCELLED)?为什么只有当前驱节点的 waitStatus 是 SIGNAL(-1)时,当前线程才敢安心去挂起


源码分析

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    
    if (ws == Node.SIGNAL)
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true; // 【情况一】:前驱已经是 SIGNAL 了,大功告成,可以直接挂起
        
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
        // 【情况二】:前驱节点的 ws > 0(即 CANCELLED = 1),说明前驱是个死节点
        do {
            node.prev = pred = pred.prev; // 顺着链表一直往前找
        } while (pred.waitStatus > 0);   // 直到找到一个没死的前驱
        pred.next = node; // 把死掉的链表片段直接剪断!
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        // 【情况三】:前驱是 0 或者 PROPAGATE。
        // 当前线程不能挂起,必须用 CAS 把前驱改成 SIGNAL!然后返回 false(不挂起,再自旋一次)
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

为什么一定要把前驱改成 SIGNAL?
SIGNAL 的字面意思是“信号”。当一个节点的 waitStatus 变成了 SIGNAL,意味着:“我后面跟着一个兄弟在睡觉呢,等我释放锁的时候,我老哥们儿有义务叫醒它。”

新入队的节点,它的默认 waitStatus 是 0。

当它第一次进入 shouldParkAfterFailedAcquire 时,发现前驱是 0。

它不敢挂起!因为前驱如果是 0,前驱释放锁时就不会触发 unparkSuccessor 来唤醒它。

于是它强行施加动作:把前驱的状态 CAS 改为 SIGNAL(-1),然后返回 false。

高潮来了:因为返回了 false,外层循环会让这个线程再自旋一次去尝试拿锁(万一前驱就在这零点几微秒内释放锁了呢?)。

如果第二次还是拿不到锁,再次进来,发现前驱已经是 SIGNAL 了,这时它才安全地返回 true,准备去 park

结论:

shouldParkAfterFailedAcquire 是一个 “责任绑定与链表重构” 的过程。
一个线程想要挂起,必须先在前驱节点上绑定一个叫 SIGNAL 的叫醒契约。在这个过程中,它顺便扮演了“环卫工人”的角色,顺着 prev 指针把队列里所有因为超时或异常而取消(CANCELLED)的僵尸节点全部踢出队列,确保链表的绝对纯洁

问题6

经历了入队和状态绑定,线程最终进入 acquireQueued 的 for(;😉 循环。

既然已经进了排队死循环,为什么还要在每次循环里去判断 if (p == head && tryAcquire(arg))?既然线程是在这里被 LockSupport.park() 挂起的,那当它被唤醒(unpark)时,它是从哪一行代码醒来的?醒来之后它做的第一件事是什么?为什么它要吞掉中断信号(interrupted = true),等到拿到锁出队之后才自己补一个中断(selfInterrupt())?


源码分析

独占锁最后的收官之战源码acquireQueued:

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor(); // 拿到当前节点的前驱
            // 【自旋抢锁】:只有当我的前驱是 Head 时,我才有资格去 tryAcquire!
            if (p == head && tryAcquire(arg)) {
                setHead(node); // 抢锁成功,把自己设为新的 Head(原 Head 出队)
                p.next = null; // help GC
                failed = false;
                return interrupted; // 【功成身退】:返回在这期间有没有被打过中断
            }
            
            // 如果前驱不是 Head,或者抢锁失败了,判断应不应该挂起
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()) // 【线程在此处挂起!!!】
                
                // 【醒来点】:当外界调用 unpark() 或者 thread.interrupt() 时,线程从上面这一行醒来!
                // 如果是因为 interrupt 醒来的,parkAndCheckInterrupt 会返回 true,记录下被打过中断
                interrupted = true; 
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

细品 Doug Lea 的“头节点特权”与“中断延迟”:

  1. 为什么只有 p == head 才能抢锁?
    因为 AQS 是一个 FIFO(先进先出)队列。Head 节点是当前持有锁的线程(或者是傀儡节点)。只有紧挨着 Head 的第一个真实排队节点(即 head.next),才配在醒来时或者入队时去和外界新来的线程去争抢这把锁。这就保证了基本的队列公平性。
  2. 致命细节:它是怎么处理中断的?
    ReentrantLock.lock() 是不响应中断的。这意味着,即使你在线程排队时调用了 thread.interrupt(),它也不会像 Thread.sleep() 那样直接抛出 InterruptedException 崩溃退出。
    当它被中断唤醒时:
    parkAndCheckInterrupt() 内部调用了 Thread.interrupted()。这个方法会做两件事:1. 返回当前中断状态(true);2. 顺手把中断状态给抹平(复位成 false)!
    抹平是为了让下一次循环时,park 还能继续生效,否则如果不复位,下一次循环 park 就直接穿透了。
    它默默记录下 interrupted = true,然后继续假装没发生一样去死循环抢锁。
    直到它千辛万苦终于抢到锁了,从 acquireQueued 返回到 ReentrantLock 顶层时,它才会调用 selfInterrupt()
static void selfInterrupt() {
    Thread.currentThread().interrupt(); // 自己给自己补一刀中断,把中断信号还给用户代码
}

结论:

acquireQueued 是 AQS 的核心控制中枢。
它通过一个坚固的死循环实现了 “依靠前驱唤醒、只有次席可抢锁” 的 FIFO 严格秩序。同时,它对中断信号采取了 “先吞没、后复位、出队再补刀” 的延迟响应策略,完美兑现了 ReentrantLock.lock() “任凭风浪起,不拿锁绝不退出”的强硬语义。

ThreadPoolExecutor

问题1:当调用 execute() 提交任务时,如果当前工作线程数已经达到了 corePoolSize,线程池是‘先尝试入队’还是‘先判断最大线程数’?

在高并发下,如果多个线程同时往队列里塞任务,同时又有线程在销毁,execute 是如何做到不加锁(用 CAS)还能保证线程安全和边界正确性的?

execute方法

public void execute(Runnable command) {
    if (command == null) throw new NullPointerException();
    int c = ctl.get();
    
    // 【第一步】:工作线程数小于核心线程数,直接创建核心线程
    if (workerCountOf(c) < corePoolSize) {
        if (addWorker(command, true))
            return;
        c = ctl.get(); // 如果创建失败(比如并发下其他线程也创建了),重新获取 ctl
    }
    
    // 【第二步】:核心线程满了,尝试把任务放入阻塞队列
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
        // 双重检查:如果入队后,线程池突然被 shutdown 了,必须把任务从队列移除并拒绝
        if (! isRunning(recheck) && remove(command))
            reject(command);
        // 双重检查:如果线程池还是运行状态,但此时工作线程死光了(workerCount == 0)
        // 必须赶紧紧急创建一个非核心线程(addWorker(null, false))去消费队列里的任务!
        else if (workerCountOf(recheck) == 0)
            addWorker(null, false);
    }
    // 【第三步】:队列也满了,尝试创建非核心线程(直到 maxPoolSize)
    else if (!addWorker(command, false))
        reject(command); // 连非核心线程也创建失败,直接走拒绝策略
}

addWorker方法,它是真正去底层创建线程(new Thread(worker))的地方。Doug Lea 在这里设计了一个精妙的无锁双层死循环


retry:
for (;;) {
    int c = ctl.get();
    int rs = runStateOf(c);

    // 检查线程池状态,如果已经是 SHUTDOWN 且不需要补线程,直接拒绝
    if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty()))
        return false;

    for (;;) {
        int wc = workerCountOf(c);
        // 校验是否超出了容量限制(或者是核心/最大线程数限制)
        if (wc >= CAPACITY || wc >= (core == true ? corePoolSize : maximumPoolSize))
            return false;
        // 【关键点】:用 CAS 增加工作线程计数。如果成功,跳出外层大循环,开始真正 new Thread
        if (compareAndIncrementWorkerCount(c))
            break retry;
        c = ctl.get();  // CAS 失败,说明被别人抢先了,重新读 ctl
        if (runStateOf(c) != rs) // 如果运行状态变了,回退到最外层循环重来
            continue retry;
    }
}

结论

execute 的核心设计思想是 “乐观锁重试 (CAS) + 延迟初始化 (Lazy Initialization)”。
它没有使用任何重度同步锁(如 ReentrantLock)来保护状态,而是通过 ctl 的 CAS 操作先占坑(增加 workerCount),占坑成功后才允许创建 Worker 线程。这样极大地提高了吞吐量。同时,内部高频率的 “Double Check(双重检查)” 确保了任务入队与线程池关闭之间的并发时序不会错乱

问题2

我们都知道非核心线程在超过 keepAliveTime 没有任务时会被销毁,核心线程默认不销毁。

在源码里,到底是谁、在什么时候、通过什么手段把这个线程给干掉的?核心线程和非核心线程在底层有贴标签区分吗?如果设置了 allowCoreThreadTimeOut(true),底层又是如何统一处理的?


每个 Worker 线程其实就是一个实现了 Runnable 的死循环。它启动后,会在一个 runWorker(Worker w) 的方法里不停地调用 getTask() 去拿任务,拿不到就退出循环,一旦退出循环,线程就自然死亡了。

getTask源码

private Runnable getTask() {
    boolean timedOut = false; // 上一次 poll 是否超时了

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

        // 如果线程池停止了,或者队列空了,直接减少计数并返回 null,让线程去死
        if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
            decrementWorkerCount();
            return null;
        }

        int wc = workerCountOf(c);

        // 【核心设计】:当前线程拿任务时,到底要不要执行超时等待(KeepAliveTime)?
        // 条件:要么开启了 allowCoreThreadTimeOut,要么当前总线程数 > 核心线程数
        boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;

        // 如果线程数超标了,或者上一次拿任务超时了,说明这货该被淘汰了
        if ((wc > maximumPoolSize || (timed && timedOut))
            && (wc > 1 || workQueue.isEmpty())) {
            // 尝试 CAS 把工作线程数减 1
            if (compareAndDecrementWorkerCount(c))
                return null; // 成功减 1 后返回 null,回到 runWorker 触发线程退出逻辑
            continue; // CAS 失败,说明有并发,重试
        }

        try {
            // 【关键手腕】:利用 BlockingQueue 的不同 API 实现存活控制
            // 如果 timed 为 true(需要淘汰多余线程),用 poll() 阻塞等待 keepAliveTime
            // 如果 timed 为 false(核心线程不需要淘汰),用 take() 无限期阻塞,直到有新任务进来
            Runnable r = timed ?
                workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                workQueue.take();
            if (r != null)
                return r;
            timedOut = true; // 走到这里说明 poll 超时了,没拿到任务,下一轮循环就会触发上面的销毁逻辑
        } catch (InterruptedException retry) {
            timedOut = false; // 被中断唤醒,不算超时,重试
        }
    }
}

在底层,核心线程和非核心线程根本没有身份标签!
它们在 Worker 集合里一视同仁。决定一个线程该不该死,完全取决于当前瞬间的 wc > corePoolSize 条件。
如果当前总线程数是 12,核心线程数是 10。那么这 12 个线程在调用 getTask() 时,timed 都是 true。谁运气差,碰上队列没任务,抢先执行了 workQueue.poll() 且超时了,谁就会触发 compareAndDecrementWorkerCount 成功,然后返回 null 宣告死亡。直到线程数跌回 10 个,剩下的 10 个线程看到的 timed 变成了 false,它们就会调用 workQueue.take() 死死阻塞在那里,这就变成了“核心线程”。

结论:

线程池维持核心线程数和销毁非核心线程,不是靠给线程打标签,而是通过阻塞队列的逻辑复用(take() 与 poll(time))和工作线程总数(wc)的动态边界判定来实现的。
这种设计将“线程生命周期管理”与“队列等待机制”完美融合,避免了维护两套线程集合的复杂性。

问题3

在 addWorker 的开头,有一段非常难懂的状态校验表达式:
if (rs >= SHUTDOWN && ! (rs == SHUTDOWN && firstTask == null && ! workQueue.isEmpty())) return false;

为什么要写得这么绕?如果直接写 if (rs >= SHUTDOWN) return false; 会漏掉什么场景?高并发下,当线程池正在执行 shutdown() 时,这个表达式是如何放行特定线程的?

逻辑推导:为什么要用逻辑非 ! 括起来?
我们知道线程池的状态大小顺序是:RUNNING (-1) < SHUTDOWN (0) < STOP (1) < TIDYING (2) < TERMINATED (3)。

如果 rs >= SHUTDOWN 成立,说明线程池已经不是 RUNNING 状态了。此时默认是不允许创建新线程的。但是,有一种极其特殊的场景需要例外!
那就是括号里的条件同时满足时:

rs == SHUTDOWN:线程池仅仅是调用了 shutdown(),还没有彻底垮掉(不是 STOP 或更恶劣的状态)。

firstTask == null:这次触发 addWorker 不是为了提交新任务,而是为了纯粹增加一个工作线程。

! workQueue.isEmpty():此时阻塞队列里还有没执行完的存量任务。

问题1的双重检查
当核心线程满了,任务入了队,此时突然发生并发,某些核心线程因为异常死掉了(或者由于设置了超时被销毁了),导致 workerCount == 0。
这时候,队列里还有任务(! workQueue.isEmpty()),线程池状态是 SHUTDOWN(拒绝接收新任务,但必须把老任务执行完)。
如果不放行这个 addWorker(null, false),那么队列里的存量任务将永远没有任何线程去执行,直接变成死任务!

结论

这个极其复杂的表达式,是 Doug Lea 为 SHUTDOWN 状态留下的唯一温床。它的核心逻辑是:“一旦进入 SHUTDOWN 或更高级别状态,绝对不再接受任何带有 firstTask 的新任务;但如果仅仅是 SHUTDOWN 且队列里还有存量任务,允许通过 addWorker(null, false) 紧急补充空闲线程来消灭存量任务。”

问题4

Worker 内部类本身就继承了 AbstractQueuedSynchronizer(AQS)。它自己就是一把锁!

在 runWorker 执行任务时,为什么要给当前 Worker 加上独占锁(w.lock())?既然是在线程私有的死循环里运行,为什么要自己锁自己?这把锁跟外界调用 shutdown() 或 shutdownNow() 有什么精妙的联动?


// Worker 的构造方法
Worker(Runnable firstTask) {
    setState(-1); // 【关键】:初始化时将 AQS 的 state 设为 -1
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

为什么初始化时 state = -1?
因为在 AQS 逻辑中,只有 state >= 0 才允许被中断和加锁。Doug Lea 这是为了防止线程在真正跑起来之前(还在初始化或者刚加入集合时),被外界的 shutdown() 中断
再看 runWorker 里的死循环:


final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // 【关键】:这里执行 unlock(),将 state 从 -1 刷新成 0,允许后续中断
    boolean completedAbruptly = true;
    try {
        // 死循环不停拿任务
        while (task != null || (task = getTask()) != null) {
            w.lock(); // 【核心考点】:执行任务前,先加锁!
            
            // 如果线程池正在 STOP,或者当前线程被中断了,确保执行线程被正确打上中断标记
            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) { ... }
            } finally {
                afterExecute(task, thrown); // 钩子方法
                task = null;
                w.completedTasks++;
                w.unlock(); // 任务执行完,解锁!
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly); // 线程退出清理
    }
}

为什么执行任务前要 w.lock()?
我们来看看外界调用 shutdown() 时,是怎么尝试中断空闲线程的

void interruptIdleWorkers(boolean onlyOne) {
    final ReentrantLock mainLock = this.mainLock;
    mainLock.lock();
    try {
        for (Worker w : workers) {
            Thread t = w.thread;
            // 【联动点】:如果线程没有被中断,且成功拿到了 w.tryLock() 锁!
            if (!t.isInterrupted() && w.tryLock()) { 
                try {
                    t.interrupt(); // 只有拿到锁的线程,才执行中断!
                } catch (SecurityException ignore) {
                } finally {
                    w.unlock();
                }
            }
            if (onlyOne) break;
        }
    } finally {
        mainLock.unlock();
    }
}


如果一个 Worker 正在 task.run() 跑用户的代码,它已经执行了 w.lock(),那么外界 shutdown() 里的 w.tryLock() 就会失败。这意味着:正在执行任务的线程,shutdown() 是不会粗暴中断它的,必须等它把当前任务跑完解锁!

如果一个 Worker 正阻塞在 getTask() 的 workQueue.take() 上,由于它此时没有加锁,外界的 w.tryLock() 就能成功,从而安全地给它丢一个 interrupt() 中断信号,让它从队列阻塞中醒来,优雅退出。

如果是 shutdownNow() 呢?
shutdownNow() 底层调用的是 interruptWorkers(),它不调用 w.tryLock(),而是直接暴力 t.interrupt(),管你有没有在跑任务,全部强制中断

结论

Worker 继承 AQS 的本质,是为了实现一把 “非重入的、表示线程忙碌状态的互斥锁”。
通过 w.lock() 和 w.tryLock() 的巧妙配合,线程池实现了精准识别空闲线程与忙碌线程的能力。它保证了 shutdown() 宣告关闭时,正在运行的用户任务不会被无故腰斩,只有真正的空闲线程才会被中断召回,实现了工业级的优雅停机(Graceful Shutdown)