ReentrantLock源码解析(二) - - 条件锁

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

本文已参与「掘力星计划」,赢取创作大礼包,挑战创作激励金。

条件锁,是指在获取锁之后发现当前业务场景自己无法处理,而需要等待某个条件的出现才可以继续处理时使用的一种锁。

Condition中的await()方法相当于Object的wait()方法,Condition中的signal()方法相当于Object的notify()方法,Condition中的signalAll()相当于Object的notifyAll()方法。不同的是,Object中的这些方法是和同步锁捆绑使用的;而Condition是需要与互斥锁/共享锁捆绑使用的。

先来看下示例,看看条件锁的基本使用

public class ConditionDemo {

    public static void main(String[] args) throws InterruptedException {
        ReentrantLock reentrantLock = new ReentrantLock();
        Condition condition1 = reentrantLock.newCondition();
        new Thread(new Runnable() {
            @Override
            public void run() {
                reentrantLock.lock(); // 1
                try {
                    condition1.await(); // 2
                } catch (Exception e) {

                } finally {
                    System.out.println("释放咯"); // 6
                    reentrantLock.unlock();

                }
            }
        }).start();
        Thread.sleep(1000);
        reentrantLock.lock(); // 3
        try {
            System.out.println("我来释放你"); // 4
            condition1.signal(); // 5
        } catch (Exception e) {

        } finally {

            reentrantLock.unlock(); // 7
        }

    }
}

输出结果:

我来释放你
释放咯

复制代码

上面的代码很简单,一个线程等待条件,另一个线程通知条件已成立,后面的数字代表代码实际运行的顺序,如果你能把这个顺序看懂基本条件锁的用法就掌握得差不多了。

主要属性

图片

条件锁维护了一个条件队列,firstWaiter指向条件队列头节点,

lastWaiter指向条件队列尾节点。

新建条件锁

// ReentrantLock.newCondition()
  public Condition newCondition() {
        return sync.newCondition();
    }
   // ReentrantLock.Sync.newCondition()
        final ConditionObject newCondition() {
            return new ConditionObject();
        }

// AQS.ConditionObject
    public class ConditionObject implements Condition, java.io.Serializable {}

复制代码

新建一个条件锁最后就是调用的AQS的ConditionObject类完成实例化,从这也可以看到条件锁最终的实现都是由ConditionObject来完成的。

await方法

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();

            // 将当前线程加入到条件队列中
            Node node = addConditionWaiter();
            // 完全释放当前线程对应的锁,并唤醒下一个节点,将state置为0
            int savedState = fullyRelease(node);
            //0 默认值,condition队列挂起期间未接收过中断信号
            // -1 在condition队里挂起期间接收到中断信号
            // 1 在condition 队列挂起期间未接收到中断信号,但在迁移到阻塞队列之后接收过中断信号
            int interruptMode = 0;
            // TRUE:isOnSyncQueue:当前Node还在阻塞队列
            // FALSE:isOnSyncQueue: 当前Node还在条件队列中
            while (!isOnSyncQueue(node)) {
                // 挂起当前Node对应的线程
                LockSupport.park(this);
                //什么时候会被唤醒?都有几种情况
                // 1.外部线程获取到lock之后,调用signal(),会将条件队列的头节点转移到阻塞队列
                // 2. 转移至阻塞队列后,发现阻塞队列的前驱节点状态是取消状态,此时会唤醒当前节点


                // 就算在condition队列挂起期间 线程发生了中断,对应的node也会被迁移到“阻塞队列”。
                if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
                    break;
            }
            // 执行到这里,说明当前Node已经迁移到阻塞队列

            // 自旋直到获取到锁
            if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
                interruptMode = REINTERRUPT;

            // 清除取消的节点
            if (node.nextWaiter != null) // clean up if cancelled
                unlinkCancelledWaiters();
            // 挂起期间 发生过中断(1.条件队列内的挂起,2.条件队列之外的挂起)
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }


 private Node addConditionWaiter() {
            // 赋值条件队列尾节点
            Node t = lastWaiter;
            // 如果条件队列的尾节点已取消(已取消就是说该尾节点被中断了)
            // 从头节点开始清除所有已取消的节点
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                // 重新获取尾节点
                t = lastWaiter;
            }
            // 新建一个节点,它的等待状态是CONDITION
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            // 如果尾节点为空,则把新节点赋值给头节点(相当于初始化队列)
            // 否则把新节点赋值给尾节点的nextWaiter指针
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            // 尾节点指向新节点
            lastWaiter = node;
            // 返回新节点
            return node;
        }

  final int fullyRelease(Node node) {
        boolean failed = true;
        try {

            int savedState = getState();
            // 释放获得的锁,唤醒下一个节点
            if (release(savedState)) {
                failed = false;
                return savedState;
            } else {
                throw new IllegalMonitorStateException();
            }
        } finally {
            if (failed)
                node.waitStatus = Node.CANCELLED;
        }
    }

  final boolean isOnSyncQueue(Node node) {
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) // If has successor, it must be on queue
            return true;
        /**
         * 执行到这里 waitStatus==0
         *
         *findNodeFromTail:从阻塞队列的末尾往前查找Node,查找到返回true
         * 查找不到返回false
         */
        return findNodeFromTail(node);
    }

复制代码
  1. 首先加入条件队列

    加入条件队列会加入队列尾节点,并将节点的waitstatus状态设置condition状态

  2. 完全释放锁

    可以看到这里是调用的fullyRelease方法,而不是release方法,为什么呢,你想一下,如果出现可重入的情况,那么当前的state值就不是1了,所以fullyRelease方法就是为了完全的释放,保证彻底释放该线程持有的锁,并唤醒下一个线程。

  3. 判断当前线程在条件队列还是阻塞队列

  4. 如果在条件队列,就要将其park(),等待唤醒。

  5. 如果现在在阻塞队列,说明当前节点已经被迁移到阻塞队列,就调用上一篇文章说的acquireQueued方法自旋获取锁。

 signal方法

     // AQS.signal()
     public final void signal() {
            // 如果不是当前线程占有着锁,调用这个方法会出异常
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            // 条件队列的头节点
            Node first = firstWaiter;
            // 如果有等待条件的节点,则通知它条件已成立
            if (first != null)

                doSignal(first);
        }
       // ReentrantLock.isHeldExclusively()
      protected final boolean isHeldExclusively() {
            // While we must in general read state before owner,
            // we don't need to do so to check if current thread is owner
            return getExclusiveOwnerThread() == Thread.currentThread();
        }

// AQS. doSignal() 
private void doSignal(Node first) {
            do {
                if ((firstWaiter = first.nextWaiter) == null)
                    lastWaiter = null;
                //当前first节点处条件队列,断开和下一个节点的关系
                first.nextWaiter = null;
                // transferForSignal true:表示当前first节点迁移到阻塞队列成功
                // first = firstWaiter) != null:当前first迁移失败,即将first更新为first.next 继续尝试迁移

            } while (!transferForSignal(first) &&
                    (first = firstWaiter) != null);
        }

// AQS.transferForSignal()
   final boolean transferForSignal(Node node) {
        /**
         * 成功:当前节点在条件队列中状态正常
         */
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;

        // 将node放入到阻塞队列当中
        // 返回值是当前节点的前置节点
        Node p = enq(node);
        // 获取前驱结点状态
        int ws = p.waitStatus;
        // 前驱节点只要不是0或者-1 那么,就唤醒当前线程
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

复制代码
  1. 从条件队列的头节点开始寻找一个非取消状态的节点;

  2. 把它从条件队列移到AQS队列;

  3. 且只移动一个节点;

 signalAll方法

     public final void signalAll() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignalAll(first);
        }

   private void doSignalAll(Node first) {
            lastWaiter = firstWaiter = null;
            do {
                Node next = first.nextWaiter;
                first.nextWaiter = null;
                transferForSignal(first);
                first = next;
            } while (first != null);
        }

复制代码

signalAll方法是唤醒所有在条件队列中的结点,将他们一个一个加入阻塞队列。

总结

图片

  1. 条件锁是指为了等待某个条件出现而使用的一种锁;

  2. 条件锁比较经典的使用场景就是队列为空时阻塞在条件notEmpty上;

  3. ReentrantLock中的条件锁是通过AQS的ConditionObject内部类实现的;

  4. await()和signal()方法都必须在获取锁之后释放锁之前使用;

  5. await()方法会新建一个节点放到条件队列中,接着完全释放锁,然后阻塞当前线程并等待条件的出现;

  6. signal()方法会寻找条件队列中第一个可用节点移到AQS队列中;

  7. 在调用signal()方法的线程调用unlock()方法才真正唤醒阻塞在条件上的节点(此时节点已经在AQS队列中);

  8. 之后该节点会再次尝试获取锁,后面的逻辑与lock()的逻辑基本一致了。

猜你喜欢

转载自juejin.im/post/7014346315580571655
今日推荐