java锁:AQS详解(二)

996 阅读1分钟

本文已参与掘金创作者训练营第三期「话题写作」赛道,详情查看:掘力计划|创作者训练营第三期正在进行,「写」出个人影响力

java锁:AQS详解(一)这篇文章中,从源码的角度深度剖析了AQS独占锁模式下的获取锁与释放锁的逻辑,下面继续从源码中分析共享锁的实现原理和condition条件。

获取锁

//获取共享锁,忽略中断;
public final void acquireShared(int arg) {
    //子类实现,CAS方式获取共享锁,若获取失败,调用doAcquireShared继续获取共享锁
    if (tryAcquireShared(arg) < 0)
        //尝试获取共享锁,获取失败则将当前线程节点入队,直到被通知或被中断
        doAcquireShared(arg);
}

这里的tryAcquireShared方法是留给实现方去实现获取锁的具体逻辑的,需要看的是doAcquireShared方法的实现逻辑:

//获取共享锁,若CAS获取失败,则将当前节点入队并阻塞当前线程,直到获取锁
private void doAcquireShared(int arg) {
    //将当前节点插入队尾
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        for (; ; ) {
            //获取当前节点的前驱节点,若前驱节点为头节点,则尝试获取锁;
            //若获取失败,则检查节点状态;当节点状态为SIGNAL时将节点线程阻塞
            final Node p = node.predecessor();
            if (p == head) {
                //CAS获取锁
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    //成功则设置当前节点为头节点并将其他节点状态设为PROPAGAE
                    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方法同时唤醒后继节点,实现共享模式,下面是唤醒后继节点代码逻辑:

//设置当前节点为头节点并唤醒共享模式下的线程
private void setHeadAndPropagate(Node node, int propagate) {
    Node h = head; 
    //设为头节点
    setHead(node);
    //若propagate > 0或头结点为空且头节点状态为 < 0 
    if (propagate > 0 || h == null || h.waitStatus < 0 ||
            (h = head) == null || h.waitStatus < 0) {
        Node s = node.next;
        //若头节点的后继节点为共享模式,则获取头结点
        if (s == null || s.isShared())
            doReleaseShared();
    }
}

该方法主要做了两个重要的步骤:

1.将当前节点设置为新的头节点,即当前节点的前置节点(旧头节点)已经获取共享锁了,从队列中去除; 2.调用doReleaseShared方法,它会调用unparkSuccessor方法唤醒后继节点。

释放锁

//释放共享锁
public final boolean releaseShared(int arg) {
    //cas方式释放锁,若失败则doReleaseShared释放锁
    if (tryReleaseShared(arg)) {
        doReleaseShared();
        return true;
    }
    return false;
}

下面是释放锁逻辑:

private void doReleaseShared() {
  for (;;) {
    // 从头节点开始执行唤醒操作
    // 这里需要注意,如果从setHeadAndPropagate方法调用该方法,那么这里的head是新的头节点
    Node h = head;
    if (h != null && h != tail) {
      int ws = h.waitStatus;
      //表示后继节点需要被唤醒
      if (ws == Node.SIGNAL) {
        // 初始化节点状态
        //这里需要CAS原子操作,因为setHeadAndPropagate和releaseShared这两个方法都会顶用doReleaseShared,避免多次unpark唤醒操作
        if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
          // 如果初始化节点状态失败,继续循环执行
          continue;            // loop to recheck cases
        // 执行唤醒操作
        unparkSuccessor(h);
      }
      //如果后继节点暂时不需要唤醒,那么当前头节点状态更新为PROPAGATE,确保后续可以传递给后继节点
      else if (ws == 0 &&
               !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
        continue;                // loop on failed CAS
    }
    // 如果在唤醒的过程中头节点没有更改,退出循环
    // 这里防止其它线程又设置了头节点,说明其它线程获取了共享锁,会继续循环操作
    if (h == head)                   // loop if head changed
      break;
  }
}

共享锁的释放锁逻辑比独占锁的释放锁逻辑稍微复杂,原因是共享锁需要释放队列中所有共享类型的节点,因此需要循环操作,由于释放锁过程中会涉及多个地方修改节点状态,此时需要CAS原子操作来并发安全。共享锁的主要特征是当有一个线程获取到锁之后,那么它就会依次唤醒等待队列中可以跟它共享的节点,当然这些节点也是共享锁类型。

condition条件

Condition接口一共定义了以下几个方法:

await(): 当前线程进入等待状态,直到被通知(siginal)或中断。

awaitUninterruptibly(): 当前线程进入等待状态,直到被通知,对中断不敏感。

awaitNanos(long timeout): 当前线程进入等待状态直到被通知(siginal),中断或超时。

awaitUnitil(Date deadTime): 当前线程进入等待状态直到被通知(siginal),中断或到达某个时间。

signal(): 唤醒一个等待在Condition上的线程,该线程从等待方法返回前必须获得与Condition关联的锁

signalAll(): 唤醒所有等待在Condition上的线程,能够从等待方法返回的线程必须获得与Condition关联的锁。

condition的实现:首先内部存在一个Condition队列,存储着所有在此Condition条件等待的线程。

await系列方法:让当前持有锁的线程释放锁,并唤醒一个在CLH队列上等待锁的线程,再为当前线程创建一个node节点,插入到Condition队列(注意不是插入到CLH队列中)

signal系列方法:其实这里没有唤醒任何线程,而是将Condition队列上的等待节点插入到CLH队列中,所以当持有锁的线程执行完毕释放锁时,就会唤醒CLH队列中的一个线程,这个时候才会唤醒线程。

await相关方法

//让当前持有锁的线程阻塞等待,并释放锁。如果有中断请求,则抛出InterruptedException异常
public final void await() throws InterruptedException {
    //若当前线程已被中断,则抛出中断异常
    if (Thread.interrupted())
        throw new InterruptedException();
   // 为当前线程创建新的Node节点,并且将这个节点插入到Condition队列中
    Node node = addConditionWaiter();
    //释放当前线程持有的锁,并唤醒同步队列中的头结点
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    //如果当前节点补足同步队列中;
    //阻塞当前线程,当前当前线程被signal信号唤醒后,将当前节点加入同步队列中;
    //等待获取获取锁
    while (!isOnSyncQueue(node)) {
        LockSupport.park(this);
        //检查是否被中断并入队等待锁
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
            break;
    }
    // 如果节点node已经在同步队列中了,获取同步锁,只有得到锁才能继续执行,否则线程继续阻塞等待
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
    // 清除Condition队列中状态不是Node.CONDITION的节点    
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    // 是否要抛出异常,或者发出中断请求
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}
//为当前线程创建新的Node节点,并且将这个节点插入到Condition队列中
private Node addConditionWaiter() {
    Node t = lastWaiter;
    // 如果等待队列尾节点状态不是CONDITION,则进行清除操作;
    // 清除队列中状态不是CONDITION的节点
    if (t != null && t.waitStatus != Node.CONDITION) {
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    //为当前线程创建一个状态为CONDITION的节点,并将节点插入队尾
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}
//从头到尾部遍历等待队列,去除状态不是CONDITION的节点
private void unlinkCancelledWaiters() {
    //记录下一个待处理的节点
    Node t = firstWaiter;
    //记录上一个状态为CONDITION的节点
    Node trail = null;
    while (t != null) {
        Node next = t.nextWaiter;
        if (t.waitStatus != Node.CONDITION) {
            t.nextWaiter = null;
            if (trail == null)
                firstWaiter = next;
            else
                trail.nextWaiter = next;
            if (next == null)
                lastWaiter = trail;
        } else
            trail = t;
        t = next;
    }
}

调用的isOnSyncQueue方法和reportInterruptAfterWait方法分別是:判断节点释放在同步队列中 ,根据当前的模式判断是否抛出异常或重新中断等

signal相关方法

//如果等待队列不为空,则将队列头节点插入同步队列中
public final void signal() {
    //如果当前线程不是独占锁,则抛出异常
    if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    //将等待队列中的头结点插入同步队列中
    if (first != null)
        doSignal(first);
}
//将等待队列总的头结点插入同步队列中
private void doSignal(Node first) {
    do {
        // 原先的Condition队列头节点取消,所以重新赋值Condition队列头节点
        // 如果新的Condition队列头节点为null,表示Condition队列为空了
        // ,所以也要设置Condition队列尾lastWaiter为null
        if ((firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
            (first = firstWaiter) != null);
}
// 返回true表示节点node插入到同步队列中,返回false表示节点node没有插入到同步队列中
final boolean transferForSignal(Node node) {
    //如果无法将节点状态由CONDITION修改为0,表示节点已在同步队列中,直接返回false
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

    //将节点node插入到同步队列中,p是原先同步队列尾节点,也是node节点的前一个节点
    Node p = enq(node);
    int ws = p.waitStatus;
    // 如果前一个节点是已取消状态,或者不能将它设置成Node.SIGNAL状态。
    // 就说明节点p之后也不会发起唤醒下一个node节点线程的操作,
    // 所以这里直接调用 LockSupport.unpark(node.thread)方法,唤醒节点node所在线程
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}