JUC源码解析——AQS

本文详细解析了Java并发库中的AbstractQueuedSynchronizer(AQS)的设计与实现,包括acquire和release操作,以及内部的同步状态管理、线程阻塞与唤醒、CLH同步队列的工作原理。AQS是J.U.C包中许多同步器如锁和屏障的基础,它通过原子性地管理同步状态,并使用LockSupport进行线程阻塞和唤醒,以及维护FIFO的等待队列来实现高效的并发控制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在java.util.concurrent包(下称j.u.c包)中,大部分的同步器(例如锁,屏障等等)都是基于AbstractQueuedSynchronizer(下称AQS类)这个类来构建的;它是个抽象的FIFO队列式的同步器,AQS定义了一套多线程访问共享资源的同步器框架。


J.U.C包下的同步器的实现都主要依赖于以下几个功能:

  • 内部同步状态的管理
  • 同步状态的更新和检查操作
  • 且至少有一个方法会导致调用线程在同步状态被获取时阻塞,以及在
    其他线程改变这个同步状态时解除线程的阻塞

而AQS就实现了以上功能,供其他同步器使用。
所有同步器都有两个基本方法,acquire,release。acquire操作阻塞调用的线程,直到或除非同步状态允许其继续执行。而release操作则是通过某种方式改变同步状态,使得一或多个被acquire阻塞的线程继续执行。(不用同步器命名不同Lock.lock,Semaphore.acquire,CountDownLatch.await...)

之前提过Synchronized内置锁,JVM对其进行了许多优化,其性能已经比ReentrentLock更好,但是常规的JVM锁优化策略并不适用于严重依赖于J.U.C包的典型多线程服务端应用。
大部分情况下,特别在同步器有竞争的情况下,稳定地保证其效率才是J.U.C包的主要目标。

同步器的acquire与release

acquire

while(同步状态不允许acquire){
        放入队列  if  没有进队;
        依具体需求来决定是否阻塞当前线程;
}
出队 if 已入对;

release

更新同步状态;
if(状态允许被阻塞线程acquire){
      解除一个或多个队列里的阻塞线程;
}

要实现上述功能需要三个基本组建的相互协作:

  • 同步状态的原子性管理
  • 线程的阻塞与解除阻塞
  • 队列的管理

同步状态
AQS用单个32位int值 state 来表示同步状态
阻塞
利用LockSupport来阻塞/唤醒线程

队列:

无法获取执行资格的线程会构建一个节点Node加入队列,AQS中使用的是CLH队列:CLH同步队列是一个FIFO双向队列,AQS依赖它来完成同步状态的管理,当前线程如果获取同步状态失败时,AQS则会将当前线程以及等待状态等信息构造成一个节点(Node)并将其加入到CLH同步队列,同时会阻塞当前线程,当同步状态释放时,会把首节点唤醒,使其再次尝试获取同步状态。对于队列中的某个节点来说,它只需要通过判断其前一个节点的状态信息来

关于队列的节点Node:5条属性,分别是wateStatus 、prev、next、thread、nextWater。

1,其中 prev,next用于构建双向链表,thread 指向节点对应的线程。

2,waitStatus表示当前被封装成Node结点的等待状态,共有4种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE。

CANCELLED:值为1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点,其结点的waitStatus为CANCELLED,进入该状态后的结点将会被删除。

SIGNAL:值为-1,表明该节点之后有节点在阻塞,当该节点被唤醒或删除后会必须唤醒其后继节点

CONDITION:值为-2,与Condition相关,该标识的结点处于条件队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从条件队列转移到同步队列中,等待获取同步锁。

PROPAGATE:值为-3,与共享模式相关。这个标记只在一处用到,即共享锁的doReleaseShared方法,该方法在同步队列头节点状态waitStatus=0 时将其设为PROPAGATE ,本身doReleaseShared是为了释放后继节点的,但是当头节点状态为0,我们不知道有没有后继节点,所以就采用这种方式,将头节点标记为PROPAGATE,意味着将共享锁的释放传递下去,它与setHeadAndPropagate方法有关,可以去看我的CountDownLatch文章里对该方法的分析

    static final class Node {
//表示共享模式,如CountDownLatch
        static final Node SHARED = new Node();
//表示独占模式,如ReentrentLoack
        static final Node EXCLUSIVE = null;
//节点操作因为超时或者中断而被删除。节点不应该留在此状态,
//一旦达到此状态将从同步队列中删除。
        static final int CANCELLED =  1;
//表明该节点之后有节点在阻塞,当该节点被唤醒或删除后会必须唤醒其后继节点
        static final int SIGNAL    = -1;
//表明节点在Condition队列中。之后它会被放回同步队列中,状态设为为0
        static final int CONDITION = -2;
// 这个标记只在一处用到,即共享锁的doReleaseShared方法,该方法在同步队列头节点状态waitStatus=0
//时将其设为PROPAGATE ,本身doReleaseShared是为了释放后继节点的,但是当头节点状态为0,我们不知
//道有没有后继节点,所以就采用这种方式,将头节点标记为PROPAGATE,意味着将共享锁的释放传递下去,它与setHeadAndPropagate方法有关,
//可以去看我的CountDownLatch文章里对该方法的分析
        static final int PROPAGATE = -3;
// 节点没有特别标记的状态就为0,如初始节点。
        volatile int waitStatus;
//此节点的前一个节点。节点的waitStatus依赖于前一个节点的状态。
        volatile Node prev;
//此节点的后一个节点。后一个节点是否被唤醒(uppark())依赖于当前节点是否被释放。
        volatile Node next;
//节点绑定的线程
        volatile Thread thread;
//标记当前节点的模式是共享还是独占
        Node nextWaiter;
        ...
    }

AQS

public abstract class AbstractQueuedSynchronizer extends
    AbstractOwnableSynchronizer implements java.io.Serializable { 
    //同步队列的头节点,该节点并没有持有对任何线程对象的引用
    private transient volatile Node head;
    //等待队列的尾节点
    private transient volatile Node tail;
    //同步状态
    private volatile int state;
    ......

}

 

实现原理

    public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
//在节点加入队列过程中若线程被中断,则会调用该方法,底层调用interrupt()
            selfInterrupt();  
    }

需要子类重写tryAcquire与tryRelease方法利用CAS来修改同步状态status;tryAcquire为false即修改状态失败,说明此时有其它线程获取了锁正在执行,这里的锁指的是执行资格。来看看接下来的两个方法:
多线程下各个线程都会尝试修改状态,如果可以修改则tryAcquire返回true,acquire直接返回;若不能修改,放进队列;

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;
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    enq(node);
    return node;
}

// 该方法在两种情况下调用:1,队列为空,会在该方法中先创建一个空的头节点,之后利用循环CAS将node节点添到队列尾部去。
//2,在addWaiter中,由于并发竞争激烈导致之前获得的队列尾部已经失效,cas初次设置失败,enq被调用,在循环cas中不断尝试直到成功。
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;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

addWaiter方法为了使安全性得到保障,采用循环CAS来将node添加到队列尾部。

acquireQueued


接下来总的逻辑是:放入队列后,会检查前一个节点的状态,前一个节点状态为SIGNAL则挂起当前线程通过LockSupport.park(this);
来看看acquireQueued的实现

    final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) { // 无限循环保证了竞争下的安全性,只有在轮到自己(即一个节点为head)且竞
                                                  //争更改同步状态成功后,根据中断情况返回值
                final Node p = node.predecessor();
                // 如果当前节点的前一个节点为头节点,则代表等待队列中没有等待的线程,多以再次尝
                // 试tryAcquire更改同步状态,
                // 若成功则调用setHead将node设为头节点,其thread设为空
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally { // 确保异常情况下该节点会被从队列中删除
            if (failed)
                cancelAcquire(node); //将节点从等待队列中删除
        }
    }

    private void setHead(Node node) {
        head = node;
        node.thread = null;
        node.prev = null;
    }

当前一个节点为head时说明该轮到它了,再次尝试tryAcquire,同新的(还未进入队列)尝试更改同步状态的线程竞争;
没轮到它的时候,会执行shouldParkAfterFailedAcquire,该方法只有在节点状态为SIGNAL返回true,CANCELLED则删除节点,其它情况就用CAS将状态改为SIGNAL;
来看看shouldParkAfterFailedAcquire方法实现:

shouldParkAfterFailedAcquire会将当前节点node的前一个节点的状态变为SIGNAL,若前一个节点状态为CANCELLED则删除

    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;
        if (ws > 0) {
            /*
             * Predecessor was cancelled. Skip over predecessors and
             * indicate retry.
             */
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
            /*
             * 该情况下waitStatus 一定是0或者PROPAGATE。将其改为SIGNAL,代码回到acquireQueue
             * 中,再次for循环一次,再次tryAcquire尝试更改同步状态,之所以如此是因为任务可能很
             * 快执行完,当前线程只需等待一瞬即可无需放入队列中阻塞等待,所以代码设计如此。
             */
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
    }

shouldParkAfterFailedAcquire返回true,则会调用parkAndCheckInterrupt将线程挂起,被唤醒后根据线程中断标记来返回boolean值;

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted(); //会清除标记位
    }

若一个线程被唤醒后它能得到执行资格吗?这涉及到公平与非公平的问题?非公平指的是当前线程release唤醒head.next节点的线程,它需要与新线程竞争;公平就是在这里加限制,新线程尝试获取锁时会先判断队列里有无等待的节点,有则加入队列等待。举例来说,A之前一直挂起,现在轮到A了,也就是前一个节点是head,与是唤醒A,A在acquireQueued方法for循环中苏醒尝试tryAcquire,但是这时一个新的线程先一步执行acquire方法,先于A 执行tryAcquire,A又得挂起;由此可看出这是非公平的,有线程插队

Release

子类需要实现tryRelease,实现自己的逻辑。此时阻塞队列中的线程未被唤醒,若有一新线程tryAcquire成功(获取锁),意味着head.next节点被插队,线程被唤醒后回到acquireQueued中tryAcquire失败,继续阻塞。

    public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            //判断条件成立说明队列中还有线程等待被唤醒
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }
//唤醒后继节点
    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) {
            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);
    }

总结

举例说明变化情况:现在有一个线程N正在执行,阻塞队列为空,A线程tryAcquire失败,addWaiter创建了一个空的Node作为Head,它的next指向A线程的Node,随后进入acquireQueued,再次尝试tryAcquire因为N线程可能已经执行完了,若失败调用shouldParkAfterFailedAcquire,此时将头Head的同步状态由0变为SIGNAL返回false,回到acquireQueued里再次循环,还是尝试再次tryAcquire,失败调用shouldParkAfterFailedAcquire,由于Head的waitStatus为SIGNAL返回true,进入parkAndCheckInterrupt,将A线程阻塞;若又一线程B也失败,它将会将A 的waitStatus变为SIGNAL,排在A的后面阻塞着;
执行的线程完成了,release释放同步状态,唤醒阻塞队列里的线程;接着上面的逻辑,首先N线程tryRelease成功,取出head节点执行unparkSuccessor,将head节点waitStatus重置为0,取出head.next也就是A,LockSupport.unpark唤醒A线程,逻辑回到了A阻塞的地方也就是acquireQueued的for循环里,再次尝试tryAcquire(在这里可能被插队),成功,将A设为head,将原先为空的head的next指针清除以便GC回收;

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值