美文网首页
Java 源码分析-Condition

Java 源码分析-Condition

作者: 琼珶和予 | 来源:发表于2018-05-13 13:23 被阅读0次

      前面对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.最后,在唤醒线程。

    相关文章

      网友评论

          本文标题:Java 源码分析-Condition

          本文链接:https://www.haomeiwen.com/subject/nejfdftx.html