AQS中的阻塞队列

转载自:码海,按照自己的思路梳理了一遍

1.原理

AQS数据结构

public abstract class AbstractQueuedSynchronizer
  extends AbstractOwnableSynchronizer
    implements java.io.Serializable {
    // 以下为双向链表的首尾结点,代表入口等待队列
    private transient volatile Node head;
    private transient volatile Node tail;
    // 共享变量 state
    private volatile int state;
    // cas 获取 / 释放 state,保证线程安全地获取锁
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }
    // ...
 }

大致流程:

state 初始化 0,在多线程条件下,线程要执行临界区的代码,必须首先获取 state,某个线程获取成功之后, state 加 1,其他线程再获取的话由于共享资源已被占用,所以会到 FIFO 等待队列去等待,等占有 state 的线程执行完临界区的代码释放资源( state 减 1)后,会唤醒 FIFO 中的下一个等待线程(head 中的下一个结点)去获取 state。

图片

head 结点代表当前占用的线程,其他节点由于暂时获取不到锁所以依次排队等待锁释放。

 head 结点为虚结点,它只代表当前有线程占用了 state至于占用 state 的是哪个线程,其实是调用了上文的 setExclusiveOwnerThread(current) ,即记录在 exclusiveOwnerThread 属性里

2.基于AQS实现的ReentrantLock的原理(非公平锁(NonfairSync)的实现方式)

2.1ock原理

  1. 使用 CAS 来获取 state 资源,如果成功设置 1,代表 state 资源获取锁成功,此时记录下当前占用 state 的线程 setExclusiveOwnerThread(Thread.currentThread());

  2. 如果 CAS 设置 state 为 1 失败(代表获取锁失败),则执行 acquire(1) 方法,这个方法是 AQS 提供的方法,如public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }

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

addWaiter实现逻辑:

首先是获取 FIFO 队列的尾结点,如果尾结点存在,则采用 CAS 的方式将等待线程入队,如果尾结点为空则执行 enq 方法;

enq逻辑:

判断 tail 是否为空,如果为空说明 FIFO 队列的 head,tail 还未构建,此时先构建头结点,构建之后再用 CAS 的方式将此线程结点入队

acquireQueued 实现逻辑:

竞争失败的线程入队后怎么处理?

是马上阻塞吗,马上阻塞意味着线程从运行态转为阻塞态 ,涉及到用户态向内核态的切换,而且唤醒后也要从内核态转为用户态,开销相对比较大

AQS 对这种入队的线程采用的方式是让它们自旋来竞争锁,如下图示

图片

基于每个 Node 可能所处的状态,AQS 为其定义了一个变量 waitStatus,根据这个变量值对相应节点进行相关的操作,我们一起来看这看这个变量都有哪些值,是时候看一个 Node 结点的属性定义了

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;  // 节点阻塞(park)必须在其前驱结点为 SIGNAL 的状态下才能进行,如果结点为 SIGNAL,则其释放锁或取消后,可以通过 unpark 唤醒下一个节点,
    static final int CONDITION = -2;//表示线程在等待条件变量(先获取锁,加入到条件等待队列,然后释放锁,等待条件变量满足条件;只有重新获取锁之后才能返回)
    static final int PROPAGATE = -3;//表示后续结点会传播唤醒的操作,共享模式下起作用

    //等待状态:对于condition节点,初始化为CONDITION;其它情况,默认为0,通过CAS操作原子更新
    volatile int waitStatus;
...
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)) {
                //  将 head 结点指向当前节点,原 head 结点出队
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            // 如果前一个节点不是 head 或者竞争锁失败,则进入阻塞状态
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            // 如果线程自旋中因为异常等原因获取锁最终失败,则调用此方法
            cancelAcquire(node);
    }
}

当前结点的前一个节点是 head 结点,且获取锁(tryAcquire)成功的处理;

前一个节点不是 head 或者竞争锁失败,则首先调用  shouldParkAfterFailedAcquire 方法判断锁是否应该停止自旋进入阻塞状态:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    int ws = pred.waitStatus;
        
    if (ws == Node.SIGNAL)
       // 1. 如果前置顶点的状态为 SIGNAL,表示当前节点可以阻塞了
        return true;
    if (ws > 0) {
        // 2. 移除取消状态的结点
        //如果前驱节点为取消状态,则前驱节点需要移除
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 3. 如果前置节点的 ws 不为 0,则其设置为 SIGNAL,
        //如果前驱节点小于等于 0,则需要首先将其前驱节点置为 SIGNAL,
        //因为前文我们分析过,当前节点进入阻塞的一个条件是前驱节点必须为 SIGNAL
        //这样下一次自旋后发现前驱节点为 SIGNAL,就会返回 true(即步骤 1)
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

shouldParkAfterFailedAcquire 返回 true 代表线程可以进入阻塞中断,那么下一步 parkAndCheckInterrupt 就该让线程阻塞了

private final boolean parkAndCheckInterrupt() {
    // 阻塞线程
    LockSupport.park(this);
    // 返回线程是否中断过,并且清除中断状态(在获得锁后会补一次中断)
    return Thread.interrupted();
}

最后解释下为什么会有Cancel状态

最后一个 cancelAcquire 方法,如果线程自旋中因为异常等原因获取锁最终失败,则会调用此方法,这里主要的逻辑是将node设置为取消状态后,将这个节点在链表中删掉

private void cancelAcquire(Node node) {
    // 如果节点为空,直接返回
    if (node == null)
        return;
    // 由于线程要被取消了,所以将 thread 线程清掉
    node.thread = null;

    // 下面这步表示将 node 的 pre 指向之前第一个非取消状态的结点(即跳过所有取消状态的结点),waitStatus > 0 表示当前结点状态为取消状态
    Node pred = node.prev;
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;

    // 获取经过过滤后的 pre 的 next 结点,这一步主要用在后面的 CAS 设置 pre 的 next 节点上
    Node predNext = pred.next;
    // 将当前结点设置为取消状态
    node.waitStatus = Node.CANCELLED;

    // 如果当前取消结点为尾结点,使用 CAS 则将尾结点设置为其前驱节点,如果设置成功,则尾结点的 next 指针设置为空
    if (node == tail && compareAndSetTail(node, pred)) {
        compareAndSetNext(pred, predNext, null);
    } else {
    // 这一步看得有点绕,我们想想,如果当前节点取消了,那是不是要把当前节点的前驱节点指向当前节点的后继节点,但是我们之前也说了,要唤醒或阻塞结点,须在其前驱节点的状态为 SIGNAL 的条件才能操作,所以在设置 pre 的 next 节点时要保证 pre 结点的状态为 SIGNAL,想通了这一点相信你不难理解以下代码。
        int ws;
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                compareAndSetNext(pred, predNext, next);
        } else {
        // 如果 pre 为 head,或者  pre 的状态设置 SIGNAL 失败,则直接唤醒后继结点去竞争锁,之前我们说过, SIGNAL 的结点取消(或释放锁)后可以唤醒后继结点
            unparkSuccessor(node);
        }
        node.next = node; // help GC
    }
}

2.2 unLock原理

锁释放成功后,唤醒之后 head 之后节点,让它来竞争锁

public final boolean release(int arg) {
    // 锁释放是否成功
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            unparkSuccessor(h);
        return true;
    }
    return false;
}

tryRelease 方法定义在了 AQS 的子类 Sync 方法里

// java.util.concurrent.locks.ReentrantLock.Sync

protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    // 只有持有锁的线程才能释放锁,所以如果当前锁不是持有锁的线程,则抛异常
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    // 说明线程持有的锁全部释放了,需要释放 exclusiveOwnerThread 的持有线程
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

这里释放锁的条件为啥是 h != null && h.waitStatus != 0 呢。

  1. 如果 h == null, 这有两种可能,一种是一个线程在竞争锁,现在它释放了,当然没有所谓的唤醒后继节点,一种是其他线程正在运行竞争锁,只是还未初始化头节点,既然其他线程正在运行,也就无需执行唤醒操作

  2. 如果 h != null 且 h.waitStatus == 0,说明 head 的后继节点正在自旋竞争锁,也就是说线程是运行状态的,无需唤醒。

  3. 如果 h != null 且 h.waitStatus < 0, 此时 waitStatus 值可能为 SIGNAL,或 PROPAGATE,这两种情况说明后继结点阻塞需要被唤醒

唤醒方法 unparkSuccessor:

private void unparkSuccessor(Node node) {
    // 获取 head 的 waitStatus(假设其为 SIGNAL),并用 CAS 将其置为 0,为啥要做这一步呢,之前我们分析过多次,其实 waitStatus = SIGNAL(< -1)或 PROPAGATE(-·3) 只是一个标志,代表在此状态下,后继节点可以唤醒,既然正在唤醒后继节点,自然可以将其重置为 0,当然如果失败了也不影响其唤醒后继结点
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    // 以下操作为获取队列第一个非取消状态的结点,并将其唤醒
    Node s = node.next;
    // s 状态为非空,或者其为取消状态,说明 s 是无效节点,此时需要执行 if 里的逻辑
    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);
}

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值