AQS源码分析总结(1)

AQS源码分析总结(1)

@author:Jingdai
@date:2021.07.20

最近研究了一下AQS源码,记录一下,水平有限,不免理解有错,欢迎讨论指正。

整体思路

AQS是一个提供了简化同步类设计的框架,利用AQS可以比较容易的实现同步和互斥等功能。

AQS主要就是利用一个同步状态 state 来表示目前的同步状态,AQS负责管理这个同步状态。当线程无法得到同步资源时,需要将线程加入同步队列中,所以AQS 还负责管理一个同步队列。加入同步队列的同时,也涉及到线程的阻塞和唤醒,所以 AQS 还负责线程的阻塞和唤醒。AQS是一个抽象类,无法直接使用,子类必须定义更改同步状态的 protected 方法,并定义这个同步状态在获取或释放此对象方面的含义。换句话说,同步状态的具体含义是子类定义的。实现AQS的子类应定义为非公共内部帮助类。

整体来看,就是当一个线程想要获取锁,它需要先修改 state 的状态,如果可以修改,就直接利用CAS进行修改,如果现在锁被其他现在占用,则当前线程就进入同步队列中等待。当一个获得锁的线程运行完成后,它会去同步队列中唤醒队首节点。

AQS支持独占模式和共享模式中的一种或两种。 当以独占模式工作时,其他线程尝试获取不会成功。多个线程获取的共享模式可能(但不一定)成功。当共享模式获取成功时,下一个等待线程(如果存在)也必须确定它是否也可以获取。在不同模式下等待的线程共享同一个 FIFO 队列。通常,实现子类只支持这些模式中的一种,但也可能支持两种模式,如 ReadWriteLock 中。仅支持独占或共享模式的子类不需要定义未使用模式的方法。

相关的内部类

Node 源码

static final class Node {
    
    // 标记这个节点是共享模式
    static final Node SHARED = new Node();
    
    // 标记这个节点是独占模式
    static final Node EXCLUSIVE = null;

    // waitStatus值:标记这个线程已经被取消
    // 由于超时或中断,该节点被取消。
    // 节点永远不会离开这个状态。 
    // 取消节点的线程永远不会再次阻塞。
    // a thread with cancelled node never again blocks
    static final int CANCELLED =  1;
    
    // waitStatus值:标记这个节点的下一个节点的线程需要unparking
    // 当这个节点 release 或 cancel 时需要unpark下一个节点
    // 为了避免竞争,acquire必须首先表名它们需要signal,然后重试
    // 原子获取,在失败时阻塞
    static final int SIGNAL    = -1;
    
    // waitStatus值:标准这个节点在条件队列中
    // 在传输前它不会用作同步队列节点,这个status
    // 在传输时将会被设置为0
    static final int CONDITION = -2;
    
    // waitStatus值:下一个acquireShared应该无条件传播,很少用
    // releaseShared 应该传播到其他节点。 
    // 这在 doReleaseShared 中设置(仅适用于头节点)
    // 以确保传播继续,即使其他操作已经介入。
    static final int PROPAGATE = -3;
    
    //  0:          None of the above
	
    // 非负值意味着节点不需要发出信号。 因此,大多数代码不需要检查特定值,只需检查符号。
    // 对于普通同步节点,该字段被初始化为 0,对于条件节点被初始化为 CONDITION。
 
    volatile int waitStatus;

	// 节点前驱(同步队列)
    volatile Node prev;

	// 节点后继(同步队列)
    volatile Node next;

	// 当前节点对应的线程
    volatile Thread thread;

	// 链接到下一个等待条件的节点,或特殊值 SHARED。
    // 因为条件队列只有在独占模式下才会被访问,
    // 所以当它们正在等待条件时,我们只需要一个简单的链接队列来保存节点
    // 然后它们将被转移到队列以 re-acquire。
    // 用在条件队列中,条件队列的连接不用prev和next。
    // 条件队列是单向队列
    Node nextWaiter;
   
    // 共享模式没有条件队列,所以设置nextWaiter=SHARED表示共享模式
    // 独占模式有条件队列,所以nextWaiter为节点。
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

}

AQS 类

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    // 等待队列的头部,延迟初始化。 
    // 除初始化外,仅通过 setHead 方法进行修改。
    private transient volatile Node head;

    // 同步队列的队尾,延迟初始化。 
    // 仅通过方法 enq 方法修改以添加新的等待节点
    private transient volatile Node tail;

	// 同步状态,最重要的属性
    private volatile int state;
    
}

仅仅列出 AQS 重要的属性。

工作流程源码分析

为了减少篇幅,工作流程这里仅仅介绍独占模式,共享模式差不太多,理解了独占模式看共享模式也很容易。

acquire流程:

acquire方法

public final void acquire(int arg) {
    // 尝试去获得锁,如果成功直接返回
    if (!tryAcquire(arg) &&
        // 获取不成功入队
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

acquire是独占模式的获取资源方法。

tryAcquire

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

可以看出这个方法直接抛出异常,所以子类如果需要实现独占模式的语意的话,需要自定义这个方法的含义。这样实现的好处是如果子类不用这个方法的话就不用实现,子类只需要实现自己需要的方法,减轻开发者的压力。

addWaiter方法

private Node addWaiter(Node mode) {
    // 此时mode为 Node.EXCLUSIVE
    Node node = new Node(Thread.currentThread(), mode);
    Node pred = tail;
    if (pred != null) {
        node.prev = pred;
        // 尝试一次CAS改变同步队列的队尾,如果成功,直接返回
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    // 失败,则用enq方法入队
    enq(node);
    return node;
}

这个方法就是将当前线程加入到同步队列的队尾。

enq方法

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;
            // 通过CAS改变tail值
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

acquireQueued 方法

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (;;) {
            final Node p = node.predecessor();
            // 如果node是第二个节点,则还有一次机会去获得同步状态
            if (p == head && tryAcquire(arg)) {
                // 获得同步状态后会将head指向null
                // 同时断开node和p的连接
                // 再将 node 的 thread 设为null
                setHead(node);
                p.next = null; // help GC
                failed = false;
                // 返回是否中断过
                return interrupted;
            }
            // 判断是否应该park,并检查中断状态
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

下面看 shouldParkAfterFailedAcquire 和 parkAndCheckInterrupt 方法。

shouldParkAfterFailedAcquire 方法

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
    // 前驱节点已经设为SIGNAL,可以park了
    // 之后 parkAndCheckInterrupt 方法就会park
    if (ws == Node.SIGNAL)
        return true;
    if (ws > 0) {
        // ws > 0 只有1,1代表取消
        // 前驱被取消,就跳过前驱
        // 然后进行下一轮重试(外面的函数中)
        do {
            // 先算后面的等号
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // waitStatus 是 0 or PROPAGATE
        // 通过CAS修改,不一定能成功
        // 也会进行下一轮重试
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

parkAndCheckInterrupt 方法

private final boolean parkAndCheckInterrupt() {
    // 阻塞线程
    LockSupport.park(this);
    // 返回线程是否被中断
    return Thread.interrupted();
}

结合这3个方法一起,看一下 acquireQueued 方法具体做了什么:

步骤1:得到node的前驱,如果前驱是head,尝试获取锁,成功则设置自己为头结点并断开和原来头结点的连接,函数返回。

步骤2:如果前驱不是head,则判断是否可以park(park的前提是前驱的waitStatus为-1)。如果其前驱的waitStatus 为-1,则阻塞并检测中断状态,等待被唤醒,唤醒后会回到步骤1。如果前驱的 waitStatus 是1,则一直跳过其前驱直到前驱的 waitStatus 小于等于 0,然后返回步骤1。如果前驱的 waitStatus 是-2,则尝试改为-1,之后返回步骤1。

再反过头来看这个 acquire 方法就很简单了。

acquire方法

public final void acquire(int arg) {
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        selfInterrupt();
}

*acquire流程总结(重要):

步骤1:尝试获取锁,如果获取成功则直接返回。如果获取失败进入步骤2。

步骤2:调用 addWaiter 方法利用CAS进行入队,方法中尝试一次直接加入队尾,如果一次加入不成功就调用 enq 方法利用CAS加自旋入队。方法会返回入队的 node。进入步骤3。

步骤3:调用 acquireQueued 方法,一进入方法,设置 failed 和 interrupted 的初始值后,进入步骤4死循环。

步骤4:判断node的前驱,如果前驱是head,进入步骤5;如果前驱不是head,进入步骤6。

步骤5:尝试获取锁,成功则设置自己为头结点并断开和原来头结点的连接,函数返回,整个过程结束。也就是说如果入队的是第一个节点,则它还有一次获取锁的机会。进入步骤6。

步骤6:判断前驱的waitStatus,如果waitStatus值为-1,则阻塞并检测中断状态,等待被唤醒,唤醒后会回到步骤4。如果waitStatus值为1,则将node跳过前驱移动到前驱之前的位置上,重复这个动作直到前驱的waitStatus值不为1,然后回到步骤4。如果waitStatus值为-2,则尝试修改为-1,不管成功与否,都返回步骤4。

最后,如果步骤1没有获取到锁,后面返回的结果会有整个过程是否被中断过,如果中断过,会调用selfInterrupt 方法来设置本线程的中断状态。如下:

private static void selfInterrupt() {
    Thread.currentThread().interrupt();
}

例子:

这里定义0是无锁,1是有锁,独占模式。

假如有A、B、C三个线程。

A 首先调用 acquire 方法,因为现在没有线程占有锁,所以A直接获得锁并返回。

B 调用 acquire 方法,首先调用 tryAcquire 方法尝试获得锁,因为 A 占用着锁,所以获取失败,然后会调用 addWaiter 方法进入同步队列,由于之前没有线程初始化同步队列,B就先去初始化同步队列,先创建一个头节点,然后将自己插入到同步队列中。接着就是调用 acquireQueued 方法,由于 B 前面就是head节点,所以 B 还会在去 tryAcquire 一次获取锁,由于 A 没有释放锁,所以还是失败。之后就将 B 的前驱改为 -1 ,然后调用 park阻塞,等待被中断或唤醒。

C 调用 acquire 方法,同样首先会调用 tryAcquire 方法尝试获得锁,因为 A 占用着锁,所以获取失败,然后会调用 addWaiter 方法进入同步队列,由于 B 已经初始化过同步队列,所以线程C不需要去初始化同步队列。直接创建一个自己的节点,将 waitStatus 设置为0,插入到队列中。然后调用 acquireQueued 方法,由于它前面是 B 节点,不是头节点,所以不会再去尝试获取锁,而是将自己的前驱节点的 waitStatus 设置为 - 1,然后挂起线程。

release 流程:

release 要比 acquire 简单许多,下面看具体的代码。

release方法

public final boolean release(int arg) {
    // 尝试释放成功
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            // 唤醒 h 下一个节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}

由于是独占模式,所以如果是获得锁的线程去 tryRelease 一定是可以成功的。

tryRelease方法

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

和 tryAcquire 方法一样,也需要子类去实现。

unparkSuccessor 方法

private void unparkSuccessor(Node node) {
	
    // 如果node的的waitStatus 小于0
    // 将 node 的waitStatus 改为0
    int ws = node.waitStatus;
    if (ws < 0)
        // 为什么会有失败的情况???
        compareAndSetWaitStatus(node, ws, 0);

    // 如果node的next被取消了或者为null,
    // 则从后往前找到第一个需要唤醒的node
    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)
        // 唤醒node的线程,被唤醒的线程将会从 
        // parkAndCheckInterrupt 方法处接着执行
        // 会去尝试获得锁,获得锁后 acquire 就返回,
        // 否则会再次park
        LockSupport.unpark(s.thread);
}

同样总结一下release方法的整个过程。

*release整个过程(重要):

这个过程相对acquire来说简单许多,调用 tryRelease 尝试 release,如果成功则去唤醒等待队列的下一个元素,失败则直接返回false。

例子:

还是上面的例子,当 A 任务做完后,准备释放锁。

A 首先调用 tryRelease 方法,修改 state 的值为0,然后去唤醒同步队列中的元素。首先修改头节点的 waitStatus 值,改为0,然后叫起头结点的下一个元素,即B。

B 被唤醒之后,从 parkAndCheckInterrupt 方法中返回,又回到 acquireQueued 方法中,由于它的前驱就是head元素,所以会调用tryAcquire 方法获得锁,此时没有线程占有锁,所以可以获取到,然后将 head 指向自己,自己的线程设为null,并断开和之前 head 的连接(帮助GC),然后就返回。即 B 的 acquire 返回,接着就可以做后面的事情了。

其他细节部分

  • AQS的同步队列比普通的队列多了一个头结点,这个头节点不记录线程信息,只有辅助作用,它在第一个node入队列的时候初始化。
  • AQS虽然使用FIFO的队列,但并不能保证公平性,从上面可以看出 acquire 执行时,总会先去调用 tryAcquire 方法,而不管同步队列中是否有元素。所以当一个线程刚刚释放锁,同步队列的第一个等待节点还没有获得锁时,新来的线程就可能先获得锁,即插队。

参考

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值