AbstractQueuedSynchronizer 工作原理 JDK8

7 篇文章 0 订阅

AbstractQueuedSynchronizer 是Java中有名的同步队列器,提供同步锁支持,包括共享同步锁,排它同步锁。还支持尝试获取锁机制(毫秒值),如果线程不能立即获取锁,还提供阻塞等待唤醒机制。

ReentrantLock的Sync NonSync就是由AQS实现的。

acquire(int arg) 是AQS对外提供的获取独占锁的重要方法,实际获取锁的逻辑交给用户的自定义同步器实现,获取锁失败后的阻塞队列逻辑则由AQS实现。

tryAcquire(arg)就是抽象方法交给自定义同步器实现获取锁的逻辑。acquireQueued() 是阻塞式竞争锁的逻辑。addWaiter(Node.EXCLUSIVE, arg)是线程抢锁失败后,把自己包装成等待节点,根据锁不同分为独占式和共享式节点。共享式节点比起独占式节点多了propagate特性,即在持有资源的线程释放锁时,会利用propagate状态尽可能唤醒排队中的shared阻塞节点。

具体的propagate特性需要结合doReleaseShared()和setHeadAndPropagate()看,有很多种情况。

总的来说propagate状态是为了尽快唤醒shared阻塞的节点,有些独占式节点被唤醒或因为资源不足而唤醒shared节点,在一次假唤醒后会因为acquired()或acquireShared()再次挂起。

先看看节点的状态枚举

/** 标识节点因获取读锁而被阻塞 */
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;
/**
 * 标识头结点的传播特性,在后继节点是shared模式阻塞时,因当前头结点waitStauts是被其它拥有锁资源* 
 * 的线程从0设置成-3,代表传播唤醒读锁模式阻塞的后继节点
 */
static final int PROPAGATE = -3;

几个重要状态的解释。

Signal是比较重要的状态,当一个节点是signal代表它的继任节点正在挂起并且需要被通知唤醒,那么当这个节点要被cancel的时候或释放锁的时候,会检查自己的状态是否Signal,从而去唤醒继任节点。

0是每个在同步队列节点入队的初始状态,在后续该节点有了后继节点后会被改为signal状态。

cancelled状态只会出现在获取锁中途线程被中断的情况,例如acquireInterruptible(),被挂起的线程中断后进入cancelAcquire()。

propagate是只给头结点设置的,并且只在doReleaseShared()中设置。若head.ws=0 则改成 head.ws=-3,目的在一个读锁资源被释放时尽快唤醒后面的shared阻塞的节点。如果因为第一次唤醒的头节点的后继节点是独占节点则因为醒后抢不到写锁,再次set head.ws=0 -> head.ws=-1 然后挂起自己。

如果唤醒的头结点的后继节点是shared节点,在tryAcquireShared()后抢到读锁资源,进入setHeadAndPropagate()继续唤醒shared节点。但是被唤醒的shared节点不一定能抢到读锁资源,所以可能再次park起来。就这个过程来看,达到了释放读锁时尽快唤醒后续的想要获取读锁的阻塞节点的目的。

接下来主要分析的源码是以独占锁模式分析的,加锁、排队、释放锁、线程被唤醒的过程加锁逻辑,tryAcquire是抽象方法由子类实现具体的加锁逻辑。如果获取不到所应返回false,剩下的交给框架去做排队获取锁的机制。

public final void acquire(int arg) {

    if (!tryAcquire(arg) &&

        acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

        selfInterrupt();

}

核心方法。线程排队竞争锁,是AQS的核心逻辑 

// 自旋阻塞获取锁的方法
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; // help GC
                failed = false;
                return interrupted;
            }
            // 走到这里,要么还没轮到当前节点,要么竞争锁失败了。根据前任节点的状态决定是否挂起这个线程
            // (被阻塞条件:前驱节点的waitStatus为-1),挂起后不会浪费CPU资源
            // 当线程从parkAndCheckInterrupt()恢复,代表前任节点unpark了它,此时前任节点要么canceled
            // 要么释放了锁,当前节点再次去和其它线程竞争锁
            // shouldParkAfterFailedAcquire方法会将前继节点的ws从0改为-1
            if (shouldParkAfterFailedAcquire(p node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

竞争锁失败,当前线程进入竞争队列机制

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;
        }
    }
    // 有其它线程竞争,重新入队
    enq(node);
    return node;
}

当快速入队失败,死循环继续CAS替换尾结点

private Node enq(final Node node) {
    // 工作原理就是死循环不断的尝试读最新的等待队列的尾节点,将当前线程节点前指针指向新的尾节点,
    // 然后CAS将线程节点替换成新的尾节点,如果失败就下一轮循环读取新的尾节点继续尝试
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
        // 尾节点是空代表头结点也是空,创建一个DummyNode充当头和尾节点
        // 同步队列的头结点永远是不存真实数据的辅助节点 DummyNode
        // 也就意味着所有同步队列的真实节点都会有前继
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t node)) {
                t.next = node;
                return t;
            }
        }
    }
}

靠前驱节点判断当前线程是否应该被阻塞。如果前继的ws已经是-1,则线程安心park。如果是0则将前继ws改成-1,如果前继已经取消等待了,则一直向前找到一个是0或-1的前继,并且一路把已取消的前继节点踢出等待队列。这个时候会再给线程一次抢锁的机会,因为可能前面所有节点都取消了。

private static boolean shouldParkAfterFailedAcquire(Node pred Node node) {
    // 获取头结点的节点状态
    int ws = pred.waitStatus;
    // 说明头结点处于唤醒状态,前驱节点状态是SIGNAL代表当它释放锁时会唤醒它的继任节点,
    // 所以前驱节点还未释放锁,当前节点的线程应该被挂起
    if (ws == Node.SIGNAL)
        return true; 
    // 通过枚举值我们知道waitStatus>0是取消状态
    if (ws > 0) {
        do {
            // 循环向前查找取消节点,把取消节点从队列中剔除
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        // 设置前任节点等待状态为SIGNAL
        // 现在知道了:当一个线程竞争锁失败后被挂起前,会将前任节点waitStatus设为signal
        // 当前任节点cancel或释放锁时会唤醒自己
        compareAndSetWaitStatus(pred ws Node.SIGNAL);
    }
    return false;
}

判断线程是否要挂起的流程:

Condition和AQS的协作

Condition队列的结构?

条件队列也是双向FIFO结构。但是它没有DummyNode,第一个在队列上wait的线程就是头结点。

/** First node of condition queue. */

private transient Node firstWaiter;

/** Last node of condition queue. */

private transient Node lastWaiter;

Condition 等待和唤醒流程(等效于入队和出队逻辑)?

条件队列等待流程。

condition的等待默认是可中断的,要不可中断调用awaitUniterruptibly()。

逻辑中没看到检查是否已获取锁的逻辑?实际上在fullyRelease()里如果tryRelease()释放不了锁会抛出IlleglMonitorException异常。

所以算是做了隐式的判锁逻辑。

第一件事判断线程有没有中断。然后加到条件队列尾部,入队逻辑看下面解析。

第二件事完全释放锁,所以要求当前线程必须持有锁。

第三件事进入循环中判断当前节点有没有进入到同步队列中,期间每次醒来后要检查线程有没有被中断,如果中断了要进入

transferAfterCancelledWait()逻辑,transferAfterCancelledWait()逻辑中将节点从condition状态转为0,并且入队同步队列。

第四件事,出了循环后当前节点肯定在同步队列中了,所以进入acquireQueued()竞争锁。

第五件事,拿到锁之后如果线程被中断过抛出InterruptException异常,否则等待流程结束,返回到业务代码正常执行。

public final void await() throws InterruptedException {
    if (Thread.interrupted())
        throw new InterruptedException();
    Node node = addConditionWaiter();
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    if (acquireQueued(node savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) // clean up if cancelled
        unli<x>nkCancelledWaiters();
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

条件队列唤醒流程

总是唤醒条件队列的第一个节点也就是等待最久的。首先把节点的后继设为新的头结点,然后清空一下节点的后继引用。进入transferForSignal()逻辑。transferForSignal逻辑也简单,把节点状态从condition改为0,初始入同步队列的节点状态都是0,然后将节点入队,enq()逻辑。enq()返回前继节点,如果前继已被取消或已释放锁。那么当前节点尝试立即竞争锁。否则把前继的状态改为signal,前继解锁时会唤醒当前节点。

private void doSignal(Node first) {
    do {
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) && (first = firstWaiter) != null);
}

Condittion如何转移到AQS同步队列?

其实这个问题等价于条件队列唤醒流程。每一次总是其它线程唤醒条件队列头结点,然后将头结点状态改为0,执行enq()逻辑入同步队列。

ReentrantLock和AQS的协作

ReentrantLock解锁不分公平和非公平锁,统一使用Sync内部类,

Sync.release(arg) 调用的是AQS的 release()

public final boolean release(int arg) {

    // 调用子类自定义的释放锁逻辑

    if (tryRelease(arg)) {

        // 成功后(必然成功),唤醒当前头结点的下一个节点如果继任节点非空的话

        Node h = head;

        if (h != null && h.waitStatus != 0)

            unparkSuccessor(h);

        return true;

    }

    return false;

}

ReentrantLock.tryRelease()

protected final boolean tryRelease(int releases) {
    // 因为是可重入,释放几次?做个运算
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        // 当state被减到0之后,锁就被释放了,设置空的占有线程
        free = true;
        setExclusiveOwnerThread(null);
    }
    // 上面是计算,这里才CAS设置state=0,一旦设置成功有可能立刻被其它线程占有锁
    setState(c);
    return free;
}

private void unparkSuccessor(Node node) {
    // 获取头结点waitStatus
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node ws 0);
    // 获取当前节点的下一个节点
    Node s = node.next;
    // 如果下个节点是null或者下个节点被cancelled,就找到队列最开始的非cancelled的节点
    if (s == null || s.waitStatus > 0) {
        s = null;
        // 就从尾部节点开始找,到队首,找到队列第一个waitStatus<0的节点。
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    // 如果当前节点的继任节点不为空,而且当前节点状态<=0(代表继任节点被挂起了),就把当前节点unpark
    if (s != null)
        LockSupport.unpark(s.thread);
}

总结加锁和释放锁时AQS的逻辑:

设现在有2个线程A、B竞争锁,A和B线程调用ReentrantLock.lock(),这2个线程都会经历

ReentrantLock.lock() -> AQS.acquire(1) -> ReentrantLock.tryAcquire(),其中A线程抢到锁直接返回。

B线程就倒霉了它要去经历排队等待机制,由于 tryAcquire()失败,它先去addWaiter()被包装成等待节点,如果是AQS还没初始化,则enq()中会创建一个不带数据的虚拟节点设置为AQS队列的头和尾节点,然后当前线程代替新建立的尾结点,同时指向头结点。然后进入到acquireQueued()中,线程会一直在死循环中运行处于要么被挂起要么竞争锁的状态。拿到线程的前任节点如果是头结点,它就去竞争锁,假设此时A线程还没释放锁,B线程就进入 shouldParkAfterFailedAcquire(),B线程将头结点的waitStatus设置为signal状态,然后就被挂起了。

OK,A线程执行完逻辑,调用 ReentrantLock.unlock() -> AQS.release() -> ReentrantLock.tryRelease() -> 回到AQS.release()中,此时会判断队列头结点的waitStatus状态,如果!=0的话代表头结点的继任节点要被唤醒,进入unParkSuccessor(),进一步判断是signal状态,B线程就会被成功唤醒了,然后获取到了锁(如果没有其它线程竞争的话)。

参考文章:

美团技术团队 从ReentrantLock的实现看AQS的原理及应用

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值