深入理解条件变量Condition

概念

Condition是JDK1.5引入的新的标准库java.util.concurrent.locks.Condition接口。
Condition接口可作为wait/notify的替代品来实现等待/通知,它为解决过早唤醒问题提供了支持,并解决了Object.wait(long)不能区分返回是否是由于等待超时导致的问题。
Condition实例可以通过Lock.newCondition()来获取,也就是说任意一个显示锁实例的newCondition方法都可以获得一个Condition实例。
而Object.wait/notify则要求其执行线程持有这些方法所属对象的内部锁,比如:synchronize

使用

Condition使用方法与wait/notify类似。如下:

Consumer .java

public class Consumer implements Runnable {
    
    
    private Queue<String> msg;
    private int maxSize;
    private Lock lock;
    private Condition condition;

    public Consumer(Queue<String> msg, int maxSize, Lock lock, Condition condition) {
    
    
        this.msg = msg;
        this.maxSize = maxSize;
        this.lock = lock;
        this.condition = condition;
    }
    @Override
    public void run() {
    
    
        while (true) {
    
    
            lock.lock();
            while (msg.isEmpty()) {
    
    
                //如果消息队列为空了
                try {
    
    
                    System.out.println("第一次阻塞");
                    condition.await(); //阻塞当前线程
                    System.out.println("第二次唤醒");
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("消费者消费消息:" + msg.remove());
            condition.signal(); //唤醒处于阻塞状态下的生产者
            lock.unlock();
        }
    }
}

Product .java

public class Product implements Runnable{
    
    
    private Queue<String> msg;
    private int maxSize;
    private Lock lock;
    private Condition condition;

    public Product(Queue<String> msg, int maxSize, Lock lock, Condition condition) {
    
    
        this.msg = msg;
        this.maxSize = maxSize;
        this.lock = lock;
        this.condition = condition;
    }

    @Override
    public void run() {
    
    
        int i=0;
        while(true){
    
    
            i++;
            lock.lock();
            while(msg.size()==maxSize){
    
    
                //如果生产满了
                try {
    
    
                   condition.await();
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
            try {
    
    
                Thread.sleep(1000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println("生产者生产消息:"+i);
            msg.add("生产消息:"+i);
            condition.signal();
            lock.unlock();
        }
    }
}

源码分析

注意:我会把源码中每个方法的作用都注释出来,可以参考注释进行理解。代码之后的解释就会尽量少一点。

接下来我们重点分析两个方法,也是最常用的两个方法:await()和signal()

await()

public final void await() throws InterruptedException {
    
    
	//线程中断,抛出异常
   if (Thread.interrupted())
        throw new InterruptedException();
    //创建node节点,并将此节点加到Condition下的队列的最后一个
    Node node = addConditionWaiter();
    //让持有该锁的所有线程释放锁(包括重入),同时去唤醒AQS队列中的一个线程
    long savedState = fullyRelease(node);
    int interruptMode = 0;
    //判断此节点是否在AQS队列中
    while (!isOnSyncQueue(node)) {
    
    
     //第一次进来,肯定会返回false,接着将自己阻塞,等待被唤醒
        LockSupport.park(this);
        //signal唤醒之后,继续往下执行
        //判断在等待过程中线程是否被中断,如果线程没中断,则会继续执行循环
        //再次去isOnSyncQueue判断,改节点是否在AQS中
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
        	//表示线程被中断,跳出循环
            break;
    }
    // 当这个线程醒来,会尝试拿锁, 当 acquireQueued 返回 false 就是拿到锁了.
    // 将这个变量设置成 REINTERRUPT.表示需要重新中断,不会抛出异常
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
        interruptMode = REINTERRUPT;
     //清理掉当前节点之后状态为cancelled的节点
    if (node.nextWaiter != null) // clean up if cancelled
        unlinkCancelledWaiters();
    // 如果线程被中断了,需要抛出异常.或者什么都不做
    if (interruptMode != 0)
        reportInterruptAfterWait(interruptMode);
}

进来先判断当前线程有没有中断标记,如果有,直接抛出中断异常,交由上层处理;否则,就需要执行addConditionWaiter方法创建节点,并加载到Condition的队列中

addConditionWaiter()

private Node addConditionWaiter() {
    
    
   //获取最后一个节点
   Node t = lastWaiter;
    // 如果最后一个节点的状态是cancelled,直接out
    if (t != null && t.waitStatus != Node.CONDITION) {
    
    
        unlinkCancelledWaiters();
        t = lastWaiter;
    }
    //创建当前线程的节点,状态为CONDITION
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
        firstWaiter = node;
    else
        t.nextWaiter = node;
    lastWaiter = node;
    return node;
}

如果最后一个节点状态为cancelled,直接清理掉。然后创建当前线程的节点,并初始化当前节点的状态为CONDITION。

  • 如果最后一个节点为空,这将当前节点赋值给firstWaiter ,同时将当前节点赋值给lastWaiter
  • 如果不为空,将当前节点加入到lastWaiter 节点之后,将当前节点赋值给lastWaiter

紧接着,就要释放锁

fullyRelease(node)

final long fullyRelease(Node node) {
    
    
    boolean failed = true;
    try {
    
    
    	//获得当前锁的状态值state
        long savedState = getState();
        //去释放锁,包括重入锁,全部清0
        if (release(savedState)) {
    
    
            failed = false;
            return savedState;
        } else {
    
    
            throw new IllegalMonitorStateException();
        }
    } finally {
    
    
    	//如果失败,将当前节点状态置为CANCELLED
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}
public final boolean release(long arg) {
    
    
     if (tryRelease(arg)) {
    
    
         Node h = head;
         if (h != null && h.waitStatus != 0)
             unparkSuccessor(h);
         return true;
     }
     return false;
}

这里释放,就和之前所讲的ReentrantLock是一样的,这里就不在重复,可以直接去看之前的文章 深入理解ReentrantLock

isOnSyncQueue(node)

final boolean isOnSyncQueue(Node node) {
    
    
		//判断当前节点的状态
		//第一次进来的时候状态肯定为CONDITION ==》返回false
        if (node.waitStatus == Node.CONDITION || node.prev == null)
            return false;
        if (node.next != null) // If has successor, it must be on queue
            return true;
            
        //node.prev可以为非null,但尚未排队,
        //因为将CAS放入队列的CAS可能会失败。 
        //因此,我们必须从尾部开始遍历以确保它确实做到了。 
        //在此方法的调用中,它将始终处于尾部,除非CAS失败(这不太可能),
        //否则它将一直存在,因此我们几乎不会遍历太多。
        return findNodeFromTail(node);
    }

实际上,第一次总是会返回 fasle,从而进入循环, 阻塞自己LockSupport.park(this),至此,Condition 成功的释放了所在的 Lock 锁,并将自己阻塞。
唤醒之后,就会执行下面的if方法

checkInterruptWhileWaiting(node)

private int checkInterruptWhileWaiting(Node node) {
    
    
	 //是否有中断标记
   return Thread.interrupted() ?
        (transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
        0;
}

其中THROW_IEREINTERRUPT含义如下:

  • THROW_IE:等待退出时抛出InterruptedException
  • REINTERRUPT:在等待退出时重新中断,也就是无操作
    重新检查中断状态,如果中断了,就执行==transferAfterCancelledWait(node)==方法,将node状态变为0,同时加到AQS队列中
final boolean transferAfterCancelledWait(Node node) {
    
    
	//CAS操作将node状态变为0
	//然后添加到AQS的队列中
   if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
    
    
   		//添加到AQS的队列中
        enq(node);
        return true;
    }
    //如果我们失败,那么我们将无法继续
    //直到完成其enq()。 
    //因为几乎是不可能会失败的,所以采用自旋的方式
    while (!isOnSyncQueue(node))
        Thread.yield();
    return false;
}

退出while循环后,继续往下执行acquireQueued(node, savedState) 方法去抢占锁,同时还要去判断interruptMode值得状态不能等于THROW_IE也就是中断状态。
两个方法都成功后。将interruptMode值改为REINTERRUPT,即没有影响,无操作。
抢占锁的方法acquireQueued(node, savedState) 同样在之前的文章 深入理解ReentrantLock中讲到,这里不做解释。
最后,就是清理当前节点后面的状态为CANCELLED的节点

总结

  • 在 Condition 中, 维护着一个队列,当执行 await 方法时,就会创建一个节点,并添加到尾部.
  • 然后释放锁,并唤醒阻塞在锁的 AQS 队列中的一个线程,跟ReentrantLock逻辑一致
  • 阻塞自己
  • 在被别的线程唤醒后, 将刚刚这个节点放到 AQS 队列中

接着,我们来看一下它是如何被唤醒的

signal()

public final void signal() {
    
    
   // 如果当前线程不是持有该锁的线程.抛出异常
   if (!isHeldExclusively())
        throw new IllegalMonitorStateException();
    //获得第一个节点
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

从头部开始唤醒

private void doSignal(Node first) {
    
    
   do {
    
    
   		//第一个节点的下一个节点为null
   		//将lastWaiter变为nul
        if ( (firstWaiter = first.nextWaiter) == null)
            lastWaiter = null;
        first.nextWaiter = null;
    } while (!transferForSignal(first) &&
             (first = firstWaiter) != null);
}

采用do while循环,先去判断当前节点的下一个节点,再去执行transferForSignal(node) 方法

transferForSignal(node)

final boolean transferForSignal(Node node) {
    
    
    //如果CAS将状态值变为0失败,则返回false,继续循环,类似自旋
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        return false;

   //将节点node加到AQS队列
   //返回node的前一个节点
    Node p = enq(node);
    int ws = p.waitStatus;
    //CAS将前一个节点状态置为SIGNAL状态,唤醒当前线程
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
        LockSupport.unpark(node.thread);
    return true;
}

最后使用LockSupport.unpark(node.thread) 方法,唤醒node节点上的线程

猜你喜欢

转载自blog.csdn.net/xzw12138/article/details/106469832