重入锁中的Condition

497 阅读5分钟

之前给大家仅仅介绍了下ReentrantLock中相关的API流程,接下来就讲讲里面的Condition。在之前介绍JAVA并发编程中有提及到,它与重入锁的关系就相当于Object里面的等待和通知方法一样,只不过比Object实现线程间协作更加安全和高效。

Condition等待

在实现线程里面等待的时候,我们都知道调用condition.await()方法,使得当前线程进入等待状态。

public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            Node node = addConditionWaiter();//将当前线程包装为Node类型存入到Condition里面的链表中
            int savedState = fullyRelease(node);//释放当前线程占有的锁
            int interruptMode = 0;
            while (!isOnSyncQueue(node)) {//释放完毕后,遍历AQS的队列,看当前节点是否在队列中,不在 说明它还没有竞争锁的资格,所以继续将自己沉睡。直到它被加入到队列中。
                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
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

上面就能看到方法内部的执行流程。我们就详细的进去看看。首先进入的是封装当前线程节点的方法:

private Node addConditionWaiter() {
            Node t = lastWaiter;
            // If lastWaiter is cancelled, clean out.
            if (t != null && t.waitStatus != Node.CONDITION) {
                unlinkCancelledWaiters();
                t = lastWaiter;
            }
            //包装当前节点
            Node node = new Node(Thread.currentThread(), Node.CONDITION);
            if (t == null)
                firstWaiter = node;
            else
                t.nextWaiter = node;
            lastWaiter = node;
            return node;
        }

上面函数的作用就是将当前的线程包装为一个Node节点存储在Condition接口的实现类ConditionObject里面,这里面维护了一个等待的队列。然后我们继续往下看:

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) {
        //如果当前节点状态是CONDITION或node.prev是null,则证明当前节点在等待队列上而不是同步队列上。因为一个节点如果要加入同步队列,在加入前就会设置好prev字段。
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        //如果node.next不为null,则一定在同步队列上,因为node.next是在节点加入同步队列后设置的
        if (node.next != null) 
            return true;
        
        return findNodeFromTail(node);
    }

    //遍历同步队列,从队尾开始遍历查找当前节点是否在队列中
    private boolean findNodeFromTail(Node node) {
        Node t = tail;
        for (;;) {
            if (t == node)
                return true;
            if (t == null)
                return false;
            t = t.prev;
        }
    }

这上面的while循环就帮助我们确定了当前的节点肯定是在同步队列中,否则是不会进行到下面的步骤中,我们继续往下:

//当前的节点目前已经在同步队列上,但是不能保证在队首,所以这里将阻塞直到当前节点变成队首,同时让当前进程获得锁。然后继续执行当前等待的剩下代码。
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;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

剩下的就是取消链接的节点以及等待退出时候重新中断。

Condition唤醒

public final void signal() {
            if (!isHeldExclusively())//判断是否当前线程持有锁
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);//通知等待队列队首的节点。
        }

接着往下走:

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

上面的transferForSignal方法尝试唤醒当前节点,如果唤醒失败,则继续尝试唤醒当前节点的后继节点。

final boolean transferForSignal(Node node) {
        //如果当前节点状态为CONDITION,则将状态改为0准备加入同步队列;如果当前状态不为CONDITION,说明该节点等待已被中断,则该方法返回false
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        //将节点加入同步队列,返回的p是节点在同步队列中的先驱节点
        Node p = enq(node);
        int ws = p.waitStatus;
        //如果先驱节点状态是大于0或者设置先驱节点的状态为SIGNAL失败。那么立即唤醒当前节点对应的线程。如果当前设置前驱节点状态为SIGNAL成功,那么就不需要马上唤醒线程了,当它的前驱节点成为同步队列的首节点且释放同步状态后,会自动唤醒它。
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            LockSupport.unpark(node.thread);
        return true;
    }

Condition说明

当持有锁的线程开始进行await()调用:

  1. 构造一个等待节点加入等待队列队尾;
  2. 对应线程释放锁,以及从同步队列队首移除该节点
  3. 自旋等待,直到等待队列上面的节点移动到了同步队列或者被中断。
  4. 阻塞当前及诶单,直到它获得锁,也就是此时该节点排队排到了同步队列的队首。

当持有锁的线程开始进行signal()调用:

  1. 从等待队列队首开始,尝试对节点唤醒操作;如果节点是取消状态,则唤醒下一个节点。

  2. 唤醒该节点,首先将节点加入同步队列中,此时await()中的步骤三的解锁条件开始。这里会有一个简单的判断:

    • 如果先驱节点的状态为取消或者设置先驱节点的状态为SIGNAL失败,则立即唤醒当前线程。此时await()会完成步骤3,进入步骤4阶段。

    • 如果状态设置成功为SIGNAL,那么就不立即唤醒,等到先驱节点变成同步队列的队首并释放了同步状态后,会自动唤醒当前节点对应的线程。这个时候步骤3才执行完成。

这个上面就讲解了Condition里面的等待以及唤醒操作。我们主要理解里面有等待队列以及同步队列相互之间的合作。如果从同步变成等待以及等待变成同步的操作。