Java 源码分析-Condition

亡梦爱人 提交于 2020-08-04 17:00:57

前面对Java中的锁进行了简单的分析,锁的使用和原理整体来说还是比较简单。今天我们来分析一下Condition这个类,这个类通常来说是跟Lock搭配使用的。比如说,如果一个线程获得了Lock的同步状态(即锁),但是由于达不到运行的条件,可能不能成功运行完毕,此时一种方式就是将它自己阻塞,等到条件满足再来重新运行。
  本文的参考资料来源:

  1.方腾飞、魏鹏、程晓明的《Java 并发编程的艺术》

  2.Cay S.Horstmann的《Java 核心技术卷 I》

1.Condition的简单实用

  我们还是先来说说我们的synchronized关键字吧,我们知道每个对象都有一组自己的监视器方法,从Object类继承过来的,主要包括wait方法和notify方法,这些方法与synchronized关键字配合使用的。在Condition接口上面,也提供了类似Object的监视器方法,与Lock配合使用。
  我们来看看下面的例子:

public class ConditionUseCase {
    private Lock lock = new ReentrantLock();
    private Condition condition = lock.newCondition();


    public void conditionWait() {
        lock.lock();
        try {
            condition.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }

    public void conditionSignal(Thread thread) {
        lock.lock();
        try {
            condition.signalAll();
        } finally {
            lock.unlock();
        }
    }
}

  这里,我们可以看出来,Condition对象时从lock对象的newCondition创建的。同时,我们使用Condition的await方法来进行等待之前,必须获取获取lock的锁;同时如果一个线程被await方法阻塞了,我们可以通过Condition的signal方法来进行唤醒操作。
  这里需要注意几个地方:
  1.如果一个线程被一个Condition对象阻塞了,那么想要唤醒这个线程,必须调用同一个Condition对象的signal方法。我们可以这么来理解,一个线程被阻塞了,是阻塞在Condition对象上面的。
  2.如果一个线程从Condition的await方法返回, 表示当前的线程已经获得了锁。这里先详细的解释一下线程await的过程:当一个线程调用Condition的await方法进行阻塞时,此时线程先将自己获取的锁释放了,此时将自己从同步队列里面取出来,并且添加到等待队列里面去,此时当前这个线程相当于阻塞这里了,不会往下执行;如果一个线程来调用这个Condition的signal方法,对阻塞在Condition的线程进行唤醒,此时被阻塞的线程从等待队列转移到同步队列,参与锁的竞争。从这里我们可以看出来如果一个线程被signal唤醒,不会直接从await方法返回,而是去参与锁的竞争,换句话说,如果一个线程从await方法出返回了,那么这个线程肯定是获得了锁的。还有一种情况,就是我们调用await方法来阻塞当前线程时,如果此时调用这个线程的intercept方法进行中断,线程不会立即抛出InterceptException异常,此时它去参与锁的竞争,只有获取到了锁才会抛出InterceptException异常。总之,如果一个线程从await方法返回,那么这个线程肯定获得了锁

2.Condition的原理分析

  Condition本身是一个接口,所以如果我们想要分析Condition的话,必须从它的实现类入手。我们先来看看ReentrantLock的newCondition方法返回的是什么东西。

        final ConditionObject newCondition() {
            return new ConditionObject();
        }

  我们发现,它返回的是一个ConditionObject对象。这个ConditionObject是AbstractQueuedSynchronizer的内部类,因为Condition的操作需要获取相关联的锁,所以作为同步器的内部也是合理的。每个Condition对象都有这个一个队列,称为等待队列,该队列里面存储就是阻塞该Condition上面的线程。
  现在我们来看看Condition。

(1).等待队列

  等待队列是一个FIFO的队列,在队列中的每个节点都包含一个线程引用,该线程就是被阻塞在Condition上面的线程。还记得我们在之前分析AbstractQueuedSynchronizer 的Node内部类,锁的同步队列存储的是每个Node,这里的等待队列存储的也是Node对象,其中Node有一个nextWaiter属性表示等待队列中下一个Node。我们来看看Condition的等待队列的结构图:

 
 
 
 

  如图所示,Condition拥有一个firstWaiter对象,用来指向等待队列的队头,lastWaiter对象用来指向等待队列的队尾,而在更新队尾时,不用使用CAS来保证线程,因为在调用await方法时,该线程已经获得了锁,其他的线程已经被阻塞了。
  我们再结合Condition的等待队列和AbstractQueuedSynchronizer的同步队列,构造出整个模型的结构图:

 
 
 

(2).等待过程

  我们先来看看线程的等待过程,也就是线程调用await方法的过程。先来看看await方法的源代码:

        public final void await() throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            //创建一个等待的Node,并且添加到等待队列中去
            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
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

  从这个方法里面,我们可以看出来,整个等待过程分为3步:
  1.创建一个等待Node,添加到等待当前Condition的等待队列中去。
  2.当前线程释放锁。
  3.当前线程进行阻塞。
  其中addConditionWaiter方法进行第一步,fullyRelease方法进行第二步,LockSupport.park(this)方法进行第三步。是不是感觉非常的简单?现在我们来看看Condition的过程。

(3).通知过程

  调用Condition的signal方法,将会唤醒在等待队列中的首节点,在唤醒之前,会将节点转移到AbstractQueuedSynchronizer的同步队列中。我们先来看看signal方法的源代码。

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

  从这个过程,我们可以简单的知道,如果一个线程想要调用signal方法的话,那么前提是这个线程获得了锁,因为这里调用了isHeldExclusively方法来进行检查;检查之后,就会调用都doSignal方法将当前这个节点转移到同步队列。

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

  我们发现真正操作的是在transferForSignal方法里面,让我们来看看transferForSignal方法的源代码。

    final boolean transferForSignal(Node node) {
        //将Node状态改变
        if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
            return false;
        //进入同步队列
        Node p = enq(node);
        int ws = p.waitStatus;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
            //唤醒线程
            LockSupport.unpark(node.thread);
        return true;
    }

  我们从这个方法里面得出,整个唤醒过程分为3步:
  1.首先,将Node的状态改为初始态。
  2.状改变成功之后,将这个Node放入同步队列里面去。
  3.最后,在唤醒线程。



作者:琼珶和予
链接:https://www.jianshu.com/p/cebd1b10b920
来源:简书
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!