AQS源码解析


JAVA后端开发知识总结(持续更新…)


AQS源码解析



一、AQS简介

  AQS(AbstractQuenedSynchronizer)是抽象的队列式同步器框架,是除了java自带的synchronized关键字之外的锁机制。其底层采用乐观锁,大量使用了CAS操作,同时采用自旋方式重试,以实现轻量级和高效地获取锁。

  • AQS的核心思想

  如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS是用CLH队列锁实现的,即将暂时获取不到锁的线程加入到队列中。

  • CLH队列

  CLH(Craig,Landin,and Hagersten)队列:一个虚拟的双向队列,虚拟的双向队列即不存在队列实例,仅存在节点之间的关联关系。 AQS是将每一条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node),来实现锁的分配。

  总的来说,AQS维护了一个CLH队列,以及一个用volatile修饰的state(共享资源),其中state由线程通过CAS去改变。其大致框架如下图所示(图片来源于《Java并发之AQS详解》):

在这里插入图片描述

  AQS本身是基于模板方法模式设计的,在使用时无需关注具体的维护和实现(如获取资源失败、入队、出队、唤醒等),只需要重写获取和释放共享资源state的方法即可。目前AQS定义了两种资源共享方式:Exclusive(独占,如:ReentrantLock)和Share(共享,如:Semaphore、CountDownLatch)。

主要的重写方法有

  • tryAcquire(int):独占式尝试获取资源
  • tryRelease(int):独占式尝试释放资源
  • tryAcquireShared(int):共享式获取同步状态
  • tryReleaseShared(int):共享式释放同步状态
  • isHeldExclusively():是否在独占模式下被线程占用,通常只有用到Condition时才需要实现

注意

  这些方法都没有定义成abstract,这保证了独占模式下只用实现tryAcquire和tryRelease,共享模式下只用实现tryAcquireShared和tryReleaseShared。

  目前实现了AQS的组件有:ReentrantLock、ReentrantReadWriteLock、Semaphore、CountDownLatch、CyclicBarrier等。

二、AQS的节点类及state状态

  • Node节点类
static final class Node {
	// waitStatus值
	static final int CANCELLED =  1;	        
	static final int SIGNAL    = -1;	        
	static final int CONDITION = -2;	
	static final int PROPAGATE = -3;
	// 节点的状态
	volatile int waitStatus;
	// 节点的前驱
	volatile Node prev;
	// 节点的后继
	volatile Node next;
	// 节点封装的线程	
	volatile Thread thread;	
}

// 头节点和尾节点
private transient volatile Node head;
private transient volatile Node tail;

注意

head头节点是一个哑结点或者是当前持有锁的线程,真正的等待线程是从第二个节点开始的。

在这里插入图片描述

  • waitStatus表示当前节点的等待状态,总共只会有五种取值
  1. CANCELLED:当前节点已取消,也是唯一大于0的状态。
  2. SIGNAL:表示当前节点的后继节点在等待唤醒,在后继节点找到安全点时,会更新其前驱的状态为此。
  3. CONDITION:节点调用了Condition的await(),正在等待。
  4. PROPAGATE:共享模式下,可以唤醒多个后继节点
  5. 0:初始默认状态,在源码的注释中有提到。
  • 同步状态
private volatile int state;

  该属性表示了锁的状态,state为0表示锁没有被占用,state大于0表示当前已有线程持有该锁,大于1则说明存在重入的情况。

  • state的方法
// getter/setter方法
protected final int getState() {
        return state;
}
protected final void setState(int newState) {
        state = newState;
}
// state 通过 CAS 进行设置
protected final boolean compareAndSetState(int expect, int update) {
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
  • 持有锁的线程信息
// 当前持有锁的线程
private transient Thread exclusiveOwnerThread;

  该属性来源于AQS继承的抽象类AbstractOwnableSynchronizer,用于记录占用锁的线程是哪一个。在监视器锁中,通过ObjectMonitor对象的_owner属性,而在AQS中,则通过exclusiveOwnerThread属性。

三、主要方法源码解析

3.1 acquire(int)——独占模式

// 独占模式的acquire,忽略中断
public final void acquire(int arg) {
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

  acquire()方法是AQS的核心方法之一,它是独占模式下线程获取共享资源的顶层入口,在获得资源之前,是不响应中断的。它的具体流程如下:

  1. 先调用自定义的tryAcquire() 获取资源,成功则结束整个逻辑。
  2. 没能直接获取资源,调用addWaiter() 方法通过CAS操作将当前包装线程的节点进行尾插
  3. 执行acquireQueued() 逻辑,它主要是实现节点进入队列,安全地进入阻塞状态,直到其它线程唤醒,并不断尝试获取资源。每次被唤醒,它都会进入循环,如果自己是第二个节点(即head后面一个),才能尝试请求资源,只有获取到资源后才会返回。如果在等待过程中被中断过,会返回true。
  4. acquireQueued()调用的shouldParkAfterFailedAcquire() 保证线程被阻塞前能够找到安全的前驱,并设置前为SIGNAL。
  5. acquireQueued()调用的parkAndCheckInterrupt()让线程进入阻塞状态,如果有中断,返回true。
  6. 如果线程在等待过程中被中断,它是不响应的,但会记录中断。在获取资源后才调用selfInterrupt() 进行自我中断。
  7. acquire方法的基本流程图如下所示(图片来源于《Java并发之AQS详解》以及《逐行分析AQS源码》):

在这里插入图片描述

在这里插入图片描述

  • tryAcquire(int)
// 该逻辑需要自定义实现
protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
}
  • addWaiter(Node)enq(Node)

  由下面的代码,可以发现该方法不仅保证是CAS入队,也负责在队列为空时, 初始化队列,即队列是延时初始化(懒加载) 的。

// 节点入队
private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
		// 通过 CAS 进行一次快速尝试,尾插入队
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        // 1、队列是个空队列
        // 2、未能 CAS 快速插入,则通过 enq() 入队
        enq(node);
        return node;
}

// 循环 CAS 入队
private Node enq(final Node node) {
	// 循环 CAS 直到入队成功
        for (;;) {
            Node t = tail;
            // 队列为空,创建一个空节点作为 head 
            // 然后进入下一个循环进行尾插
            if (t == null) { 
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
                // 队列不空,CAS 尾插
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
}

尾分叉问题

  在enq进行尾插,三个步骤不是原子操作。其中第一步容易成功,第二步CAS在并发条件下有可能失败,第三步只有在第二步成功的条件下才执行。
  所以,当有大量的线程在同时入队时,只有一个线程能完整地完成这三步,而其它线程只能完成第一步(即,node.prev = t),于是就出现了尾分叉。

  基于上述原因,一个节点如果能入队(完成了第二步),则它的prev属性一定是有值的,但是它的next属性可能暂时还没有值。因此,在AQS相关的源码中,常常会出现从尾节点逆向遍历链表

  • acquireQueued(Node, int)

线程在队列中阻塞,并且不断尝试获取资源,成功则返回,记录中断

final boolean acquireQueued(final Node node, int arg) {
	    // 标记资源是否获取成功
        boolean failed = true;
        try {
            // 记录阻塞过程中的中断
            boolean interrupted = false;
            // 无限循环,直到获取成功
            for (;;) {
                // 拿到前驱节点
                final Node p = node.predecessor();
                // 只有前驱是 head 时,才有资格去尝试获取资源
                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);
        }
}
  • shouldParkAfterFailedAcquire(Node, Node)
  • 线程阻塞前的状态检查。
  • SIGNAL:一个节点的waitStatus为SIGNAL(由后继节点设置),说明它的后继节点已经被挂起了,当释放锁或者放弃获取锁时,该节点还要唤醒它的后继节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	// 前驱的状态
        int ws = pred.waitStatus;
        if (ws == Node.SIGNAL)
            // 如果前驱直接是 SIGNAL,直接返回,并进行阻塞操作
            return true;
        // 前驱状态为 CANCELLED,需要找到还在队列中等待的有效节点
        if (ws > 0) {
	    // 循环一直查找到有效节点为止
            do {
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
	    // 通过 CAS 设置前驱的状态为 SIGNAL
	    // 保证前驱释放资源时能够唤醒自己
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        return false;
}
  • parkAndCheckInterrupt()
private final boolean parkAndCheckInterrupt() { 
	// 调用 park() 使线程阻塞,等待 unpark() 或 interrupt() 唤醒自己
        LockSupport.park(this);
        // 返回阻塞过程中的中断信息
        return Thread.interrupted();
}

3.2 release(int)——独占模式

  此方法是独占模式下线程释放共享资源的顶层入口,它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其它线程来获取资源,这也是unlock()的语义。

  即,它是根据tryRelease()的返回值来判断该线程是否已经完全释放掉资源了。所以自定义实现tryRelease()的时候一定要明确这一点。

public final boolean release(int arg) {
        if (tryRelease(arg)) {
            Node h = head;
            // 当一个 head 节点的 waitStatus 为0,说明 head 后面没有在挂起等待中的后继节点了
            // 只有 shouldParkAfterFailedAcquire 将前驱节点的 waitStatus 设为 Node.SIGNAL,否则默认为 0
            if (h != null && h.waitStatus != 0)
            	// 唤醒等待队列里的下一个线程
                unparkSuccessor(h);
            return true;
        }
        return false;
}
  • tryRelease()

  一般情况下,tryRelease()都会成功的,因为线程来释放资源,则说明它肯定已经拿到独占资源了,是线程安全的,直接减掉相应量的资源即可(state-=arg)。
  所以自定义操作时,也不需要CAS操作(例如ReentrantLock)。此外,如果已经彻底释放资源了(state=0),要返回true,否则返回false。

// 该逻辑需要继承的子类自定义实现
protected boolean tryRelease(int arg) {
        throw new UnsupportedOperationException();
}
  • unparkSuccessor(Node)

此方法用于唤醒等待队列中的下一个线程。

private void unparkSuccessor(Node node) {
        int ws = node.waitStatus;
        // 如果 head 的 waitStatus 比 0 小, 则直接将它设为 0
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);
        // 通常情况下, 要唤醒的节点就是自己的后继节点
        // 如果后继节点存在且也在等待锁, 那就直接唤醒它
        // 找到下一个需要唤醒的结点 s
        Node s = node.next;
        // 如果后继节点为 null 或已取消,直接从后往前遍历
        // 直到找到距离 head 节点最近的 ws<=0 的节点
        if (s == null || s.waitStatus > 0) {
            s = null;
            for (Node t = tail; t != null && t != node; t = t.prev)
                // 只要waitStatus <= 0,就是有效节点
                if (t.waitStatus <= 0)
                    s = t;
        }
        // 直接唤醒下一个线程
        if (s != null)
            LockSupport.unpark(s.thread);
}

  在调用了unpark()方法以后,从parkAndCheckInterrupt()的挂起状态恢复,直接向下执行。在acquireQueued()中的无限循环中继续进行判断(if (p == head && tryAcquire(arg))并执行相应逻辑。

3.3 acquireShared(int)——共享模式

  此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断

  在共享模式下,当一个节点获取到了共享锁,就可以立刻唤醒其后继节点。这是因为共享锁可以被多个线程同时持有,一个锁获取到了,其后继的节点也可以直接来获取。因此,在共享模式下,在获取锁和释放锁时,都会唤醒后继节点

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
}
  • tryAcquireShared()

  是继承的子类需要自定义实现的方法,返回的是一个整型值。负值代表获取失败,0代表获取成功,但没有剩余资源,正数表示获取成功,且还有剩余资源,其它线程还可以去获取。
  tryAcquireShared()尝试获取资源,不成功则通过doAcquireShared()进入等待队列,直到获取到资源。

  • doAcquireShared()

  逻辑与独占模式的acquireQueued()几乎一致,只是尾插放到里面一并执行。此外,下一个线程获取到资源后,如果还剩余资源,可以继续唤醒后面的线程。

private void doAcquireShared(int arg) {
	    // CAS 进行尾插
	    // 标注为是共享模式
        final Node node = addWaiter(Node.SHARED);
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                // 只有当前节点是 head 的后继节点时,才能尝试获取锁
                if (p == head) {
                    int r = tryAcquireShared(arg);
                    // 获取资源成功
                    if (r >= 0) {
                    	// 将 head 指向自己,如果还有剩余资源可以再尝试唤醒之后的线程
                        setHeadAndPropagate(node, r);
                        p.next = null; // help GC
                        if (interrupted)
                            selfInterrupt();
                        failed = false;
                        return;
                    }
                }
                // 这些逻辑与独占模式下相同
                if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}
  • setHeadAndPropagate()

相比于setHead()方法,在设置自身为head后,还会调用doReleaseShared() 去唤醒后面的线程。

private void setHeadAndPropagate(Node node, int propagate) {
        Node h = head; 
        // 将当前被唤醒的节点设为头结点 head
        setHead(node);
        // 如果资源还有剩余,继续唤醒后面的线程
        if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
            Node s = node.next;
            if (s == null || s.isShared())
                doReleaseShared();
        }
}

3.4 releaseShared()——共享模式

  此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其它线程来获取资源。

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) {
            doReleaseShared();
            return true;
        }
        return false;
}
  • tryReleaseShared()

  继承的子类需要自定义实现的方法。独占模式下的tryRelease()需要在完全释放掉资源(state=0)后,才能返回true;而共享模式下则不需要,线程在释放掉部分资源时就可以唤醒后继等待结点。

protected boolean tryReleaseShared(int arg) {
        throw new UnsupportedOperationException();
}

  该方法涉及到两处调用,它可能会被同一个head调用两次,也可能两次是不同的head(因为共享模式下,线程可能有多个)。
  对于该方法,当head变化时,会回到循环中再次唤醒head的下一个节点。在当前节点唤醒后继节点之后,如果发现被唤醒后继节点已经成为了新的头节点,则会立即触发唤醒head节点的下一个节点的操作,继续新一轮的循环。
  一直会持续到head没能变化时,才会退出。

private void doReleaseShared() {
	    for (;;) {
            Node h = head;
            if (h != null && h != tail) {
                int ws = h.waitStatus;
                if (ws == Node.SIGNAL) {
                    if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
                        continue;       
                    // 唤醒下一个有效线程     
                    unparkSuccessor(h);
                }
                else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
                    continue;                
            }
            if (h == head)                   
                break;
        }
}

四、同步类继承AQS的逻辑

  同步类在进行实现时一般都会自定义一个同步器Sync,并将之定义为内部类。该同步器会继承AQS,同步类则对外提供接口。Sync只用自定义实现资源state的获取、释放方法(tryAcquire / tryRelelase),至于线程的排队、等待、唤醒等,上层的AQS都已经实现好了。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值