JUC 底层核心支持 AQS 源码分析

属性

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    // 头节点
    private transient volatile Node head;
    // 尾节点
    private transient volatile Node tail;
    // 资源状态(0:初始状态;1:已经被持有,大于1:多个线程重复持有,也就是重入了)
    private volatile int state;
}

内部类

static final class Node {

    // 占用资源方式:共享
    static final Node SHARED = new Node();
	// 占用资源方式:独占
    static final Node EXCLUSIVE = null;
	// 当前节点的线程被取消了,这个节点的线程不会再拿到锁
    static final int CANCELLED =  1;
    // 线程已经准备好了,就等资源释放
    static final int SIGNAL    = -1;
	// 表示节点在等待队列中,等待被唤醒
    static final int CONDITION = -2;
	// 共享锁模式下才会使用,独占锁不会用
    static final int PROPAGATE = -3;
	// 当前节点状态,取值就是上面的 4 个[1,-1,-2,-3]
    volatile int waitStatus;
	// 上一个节点
    volatile Node prev;
	// 下一个节点
    volatile Node next;
	// 节点持有的线程(每个节点都会包装成一个节点放入队列)
    volatile Thread thread;
	// 也是下一个节点,指向下一个处于 CONDITION 状态的节点
    Node nextWaiter;
}

核心方法

  • tryAcquire(arg):获取锁,如果获取锁失败,继续判断别的条件。是一个模板方法,具体怎么获取由子类实现(公平、非公平、读写锁)
  • addWaiter(Node.EXCLUSIVE):当前线程包装成 node,并进入队列
  • acquireQueued(addWaiter(Node.EXCLUSIVE), arg)):修改上一个节点状态,并把当前节点的线程挂起
public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
    }
}

获取锁

tryAcquire 这个方法 AQS 只提供了一个模板,具体要由子类实现

线程入队

分为两个步骤,一个是把线程封装成 node,并维护好链表关系,一个是进入队列

// 参数是模式,模式为:Node.EXCLUSIVE(独占), Node.SHARED(共享)
private Node addWaiter(Node mode) {
    Node node = new Node(Thread.currentThread(), mode); // 封装线程为 Node
    Node pred = tail; // 尾节点
    // 如果尾节点不为空,说明队列已经有节点了,当前队列需要维护链表关系,维护好就完了
    if (pred != null) { 
        node.prev = pred; // 原来尾节点设置为当前节点的上一个节点(维护链表,这里只是单向)
        if (compareAndSetTail(pred, node)) { // 设置当前节点为新的尾节点
            pred.next = node; // 原来尾节点的下一个节点设置为当前节点(这里维护后就是双向链表了)
            return node; // 返回当前封装后的 node
        }
    }
    enq(node); // 入队,其实是初始化队列,此时队列还是空的(进入方法前 node 还没维护链表)
    return node; // 返回封装后的 node(进入方法后,返回的 node 就已经维护好链表了)
}

// 入队,参数 node 是将要入队的节点,当队列不为空才会进这个队列
private Node enq(final Node node) {
    for (;;) {
        Node t = tail; // 尾节点
        if (t == null) { // 此时尾节点是空
            if (compareAndSetHead(new Node())) // 设置头节点,注意都节点是 new 出来的,并不是传进来的节点
                tail = head; // 上面的 if 设置了头节点,head 就有值了,再把[头节点]赋值给[尾节点]
        } else { // 因为是死循环,第一次循环设置了头尾节点,第二次循环尾节点不为空,就会走到这里了
            node.prev = t; // 当前节点的上一个节点设置为原来的尾节点(这里只维护了单向链表)
            if (compareAndSetTail(t, node)) { // 再设置当前节点为尾节点
                t.next = node; // 原来尾节点的下一个节点设置为当前节点(这里维护了就成为了双向链表)
                return t; // 返回原来的尾节点。跳出循环
            }
        }
    }
}

节点入队后

  • 入队后的节点主要处理两件事:更改上一节点的状态、挂起当前节点的线程
  • 入队的节点可能是第二个节点,也可能不是第二个节点(第一个节点是虚节点)
    • 如果是第二个节点,下一个就应该是它获取到锁,入队前尝试一下获取锁,能获取到就不用挂起了,但是有可能是非公平锁,所以不一定能获取到锁
    • 如果不是第二个节点,没什么好说的直接挂起
/**
 * 参数 node 是刚入队的节点
 */
final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor(); // node.predecessor() 就是拿当前节点的上一个节点
            if (p == head && tryAcquire(arg)) { // 如果 p == head(当前节点是第二个节点),执行 tryAcquire(arg) 方法尝试获取锁
                setHead(node); // 如果抢到锁了,把当前节点(也就是第二个节点)设置为头节点
                p.next = null; // 当前节点是第二个节点,说明当前节点就是队尾了,所以当前节点的尾节点置为空,help GC
                failed = false;
                return interrupted; // 返回 false(返回到 acquire(),继续执行 selfInterrupt())
            }
            // 如果当前节点不是第二个节点,或者没抢到锁,就要处理刚进来的节点,当前节点的线程要挂起
            if (shouldParkAfterFailedAcquire(p, node) && // 因为这里是个死循环,这个方法一定会返回 true
                // 底层用了 LockSupport.part(this) 挂起当前线程,唤醒后继续执行
                parkAndCheckInterrupt()) 
                interrupted = true;
        }
    } finally { // 如果异常
        if (failed) // 异常的话 failed 就是 true
            cancelAcquire(node); // 取消当前节点
    }
}

// p 是上一个节点,node 是当前节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    // 上一个节点可能是虚节点也可能不是虚节点,如果是虚节点,waitStatus 肯定是 0,如果不是虚节点就会是[1,-2,-3,-1]中的一个
    int ws = pred.waitStatus; // 是上一个节点的状态
    if (ws == Node.SIGNAL) // -1
        return true;
    if (ws > 0) { // 1
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else { // -2,-3,0
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 把上一个节点的状态改为 -1
    }
    return false; // 返回 false,外层的循环继续下一次循环,下一次循环的时候上一个节点的值是1,就会返回 true 了
}

释放锁

AQS 维护了链表和唤醒下一个节点,具体怎么释放锁由子类实现

// 释放锁,把 state 的值设置为 state - arg
public final boolean release(int arg) {
    if (tryRelease(arg)) { // tryRelease() 是个模板方法,具体由子类实现,目的是把 state 的值 -1
        Node h = head; // 头节点赋值给 h
        if (h != null && h.waitStatus != 0) // 第二个节点入队把头节点的 waitStatus 改成了 -1,所以这个条件会是真,继续往下走
            unparkSuccessor(h); // 唤醒队列中下一个线程
        return true;
    }
    return false;
}

唤醒下一个节点

// 唤醒线程,node 是头节点
private void unparkSuccessor(Node node) {

    int ws = node.waitStatus; // 头节点的 waitStatus 是 -1
    if (ws < 0) // -1 < 0,进入
        compareAndSetWaitStatus(node, ws, 0); // 把头节点的 waitStatue 设置为 0

    Node s = node.next; // 头节点的下一个节点
    if (s == null || s.waitStatus > 0) { // 除非队列没对象了,不然不会走到这里
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null) // 下一个节点不为空的话
        LockSupport.unpark(s.thread); // 唤醒下一个节点的线程
}

唤醒节点后的操作

节点挂起在这个方法中,唤醒后继续执行

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            // 唤醒后再次走这个循环,因为唤醒的是头节点的下一个节点,所以当前节点的上一个节点也就是头节点,就是维护链表,把当前被唤醒的节点维护成头节点
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                setHead(node);
                p.next = null;
                failed = false;
                return interrupted;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt()) // 挂在这里的
                interrupted = true; // 继续执行,因为是死循环,所以再次走一遍循环
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

判断当前队列是否有节点

这个方法主要体现锁的公平和非公平,如果是非公平锁,不会调用这个方法,直接尝试获取锁,如果是公平锁,调用这个方法,如果当前队列有节点就入队

public final boolean hasQueuedPredecessors() {
    Node t = tail; 
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}
posted @   CyrusHuang  阅读(17)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 阿里最新开源QwQ-32B,效果媲美deepseek-r1满血版,部署成本又又又降低了!
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 开源Multi-agent AI智能体框架aevatar.ai,欢迎大家贡献代码
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· AI技术革命,工作效率10个最佳AI工具
点击右上角即可分享
微信分享提示