条件锁

ReentrantLock类有一个方法newCondition用来生成这个锁对象的一个条件(ConditionObject)对象,它实现了Condition接口。

Condition提供了线程通讯的一套机制await和signal等线程间进行通讯的方法。。


1、适用场景
     当某线程获取了锁对象,但由于某些条件没有满足,须要在这个条件上等待,直到条件满足才可以往下继续运行时。就须要用到条件锁。

     这样的情况下,线程主动在某条件上堵塞,当其他线程发现条件发生变化时,就能够唤醒堵塞在此条件上的线程。

2、使用演示样例
     以下是来自JDK的一段演示样例代码,须要先获得某个锁对象之后,才干调用这个锁的条件对象进行堵塞。

     
class BoundedBuffer {
   final Lock lock = new ReentrantLock();
   final Condition notFull  = lock.newCondition(); 
   final Condition notEmpty = lock.newCondition(); 

   final Object[] items = new Object[100];
   int putptr, takeptr, count;

   public void put(Object x) throws InterruptedException {
     lock.lock(); 
     try {
       while (count == items.length)
         notFull.await();
       items[putptr] = x;
       if (++putptr == items.length) putptr = 0;
       ++count;
       notEmpty.signal();
     } finally {
       lock.unlock();
     }
   }

   public Object take() throws InterruptedException {
     lock.lock();
     try {
       while (count == 0)
         notEmpty.await();
       Object x = items[takeptr];
       if (++takeptr == items.length) takeptr = 0;
       --count;
       notFull.signal();
       return x;
     } finally {
       lock.unlock();
     }
   }
 }
注意上面的代码,先是通过lock.lock获得了锁对象,然后发现条件不满足时(count==items.length),缓存已满,无法继续往里面写入数据,这时候就调用条件对象notFull.await()进行堵塞。

假设条件满足,就会往缓存中写入数据,同一时候通知等待缓存非空的线程,notEmpty.signal.

这样就实现了读线程和写线程之间的通讯

3、线程堵塞对锁的影响
     上面的样例中。线程是先获得了锁对象之后。然后调用notFull.await进行的线程堵塞。在这样的情况下,拥有锁的线程进入堵塞,是否可能会造成死锁。
     
     答案当然是否定的。

由于线程在调用条件对象的await方法中,首先会释放当前的锁,然后才让自己进入堵塞状态,等待唤醒。


4、线程的条件等待、唤醒与锁对象的关系
     在ReentrantLock解析中说过。AbstractQueuedSynchronizer的内部维护了一个队列,等待该锁的线程是在这个队列中。类似的,ConditionObject内部也是维护了一个队列,等待该条件的线程也构成了一个队列。

     当现成调用await进入堵塞时。便会增加到ConditionObject内部的等待队列中。

注意,这里是自动进入堵塞。除非被其他线程唤醒或者被中断,否则线程将一直堵塞下去。


     当其他线程调用signal唤醒堵塞的线程时,便把等待队列中的第一个节点从队列中移除,同一时候把节点增加到AbstractQueuedSynchronizer 锁对象内的等待队列中。为什么是进入到锁的等待队列中?由于线程被唤醒之后,并不意味着就能立马运行。

此时,其他线程有可能正好拥有这个锁,前面也已经有现成在等待这个锁,所以被唤醒的线程须要进入锁的等待队列中,在前面的线程运行完毕后,才干继续兴许的操作。


     可參考下图
     
     
5、线程能否同一时候处于条件对象的等待队列中和锁对象的等待队列中

     不能。

线程仅仅有调用条件对象的await方法,才干进入这个条件对象的等待队列中。而线程在调用await方法的前提是线程已经获取了锁,所以线程是在拥有锁的状态下进入条件对象的等待队列的。拥有锁的线程也就是正在执行的线程,是不在锁对象的等待队列中的。

     仅仅有当一个线程试着获取锁的时候。而这个锁正好又由其他线程占领的时候。线程才会进入锁的等待队列中,等待拥有锁的线程运行完毕。释放锁的时候被唤醒。

6、实现原理

     相关代码在AbstractQueuedSynchronizer的内部类ConditionObject中能够看到。

     ConditionObject有两个属性firstWaiter和lastWaiter,分别指向的是这个条件对象等待队列的头和尾。
     队列的各个节点都是Node(AbstractQueuedSynchronizer的内部类)对象,通过Node对象的nextWaiter之间进行向下传递,所以,条件对象的等待队列是一个单向链表。


以下是await的源码
       public final void await () throws InterruptedException {
            if (Thread.interrupted())
                throw new InterruptedException();
            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)
                unlinkCancelledWaiters();
            if (interruptMode != 0)
                reportInterruptAfterWait(interruptMode);
        }

首先是调用addConditionWaiter把当前线程增加到条件对象的等待队列中,然后fullyRelease来释放锁,然后通过isOnSyncQueue来检查当前线程节点是否在锁对象的等待队列中。
为什么要做这个检查?由于线程被signal唤醒的时候,是首先增加到锁对象的等待队列中的。

假设没有在锁对象的等待队列中,那么说明事件还没有发生(也就是没有signal方法没有被调用)。所以线程须要堵塞来等待被唤醒。


在addConditionWaiter方法中完毕了等待队列的构建过程,代码例如以下

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;
        }
线程增加队列的顺序与增加的时间一致,刚增加的线程是在队列的最后面。

以下来看线程的唤醒
 public final void signal() {
            if (!isHeldExclusively())
                throw new IllegalMonitorStateException();
            Node first = firstWaiter;
            if (first != null)
                doSignal(first);
        }

唤醒操作实际上是通过doSignal完毕。注意这里传递的是firstWaiter指向的节点,也就是唤醒的时候,是从队列头開始唤醒的。
从尾部进入,从头部唤醒。所以这里的等待队列是一个FIFO队列。


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

doSignal方法把第一个节点从条件对象的等待队列中移除,然后终于是走到transferForSignal中来进行操作。
final boolean transferForSignal (Node node) {
        /*
         * If cannot change waitStatus, the node has been cancelled.
         */
        if (!compareAndSetWaitStatus(node, Node. CONDITION, 0))
            return false ;

        /*
         * Splice onto queue and try to set waitStatus of predecessor to
         * indicate that thread is (probably) waiting. If cancelled or
         * attempt to set waitStatus fails, wake up to resync (in which
         * case the waitStatus can be transiently and harmlessly wrong).
         */
        Node p = enq(node);
        int ws = p.waitStatus ;
        if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node. SIGNAL))
            LockSupport. unpark(node.thread);
        return true ;
    }

通过enq方法,把线程所在的节点增加到锁对象的等待队列中,这样在条件合适的时候,线程被唤醒,获得锁,然后运行。
原文地址:https://www.cnblogs.com/liguangsunls/p/7360190.html