Java-并发-Condition

版权声明:欢迎转载,请注明作者和出处 https://blog.csdn.net/baichoufei90/article/details/85161987

Java-并发-Condition

摘要

本文介绍Condition,需要配合AQS使用,他也实现了一套类似wait/notify的逻辑。本文会简单分析其实现。

关于Reentrant的分析,请参见本文姊妹篇:Java-并发-锁-ReentrantLock

更多关于Java锁的信息,可参考文章:Java-并发-关于锁的一切

0x01 基本概念

Condition类其实是位于java.util.concurrent.locks的一个接口类。他的一个常用实现类是AQS的非静态内部类ConditionObject

public class ConditionObject implements Condition, java.io.Serializable

虽说ConditionObject是public修饰,但不能直接使用,因为他是非静态内部类,必须先实例化AQS的实例。而AQS定义如下:

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable

很明显,他是一个抽象类,不能直接实例化。也就是说必须使用继承他的子类才能实例化,从而使用ConditionObject

我们最常使用的是配套ReentrantLock和Condition使用:

// 创建一个可重入锁
ReentrantLock lock = new ReentrantLock(true);
// 基于此lock创建一个condition
Condition condition = lock.newCondition();
// 使线程wait在condition上
condition.await();
condition.signal();

下面简单分析下后面3步代码实现:

0x02 实现原理

称呼规约:

  • ReentrantLock专门使用的等待队列下文中称为wait_queue
  • Condition专门使用的等待队列下文中称为condition_queue

2.1 condition构建-lock.newCondition

首先会执行以下代码:

public Condition newCondition() {
    return sync.newCondition();
}

继续看sync.newCondition();在做什么:

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

怎么又这么短,下面看看ConditionObject在干啥:

public class ConditionObject implements Condition, java.io.Serializable {
    private static final long serialVersionUID = 1173984872572414699L;
    // 指向condition_queue头结点
    private transient Node firstWaiter;
    // 指向condition_queue末尾结点
    private transient Node lastWaiter;

    public ConditionObject() { }

好吧,这ConditionObject构造方法啥也没做。只不过要留意firstWaiterlastWaiter,表明这个condition也维护了一个拥有两个指针的链表即condition_queue如下图:

ConditionNodes

2.2 condition.await

  • await
public final void await() throws InterruptedException {
    if (Thread.interrupted())
    // 如果线程被中断,直接抛出InterruptedException
        throw new InterruptedException();
    // 创建conditionNode(waitStatus=-2),并以尾插法加入condition_queue
    Node node = addConditionWaiter();
    // 释放拥有的所有锁许可
    int savedState = fullyRelease(node);
    int interruptMode = 0;
    // !isOnSyncQueue(node)说明该线程结点仍为CONDITION状态,需要继续阻塞等待
    // 否则说明该线程结点已经被放入了wait_queue
    while (!isOnSyncQueue(node)) {
        // 调用我们熟悉的LockSupport.park阻塞当前线程
        LockSupport.park(this);
        // 当线程park被唤醒时,需要执行checkInterruptWhileWaiting判断:
        // 	如果是发生在唤醒前的中断,就返回-1
        // 	如果是发生在唤醒后的中断,就返回1
        // 	如果不是,该方法就返回0
        if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
        // 中断发生,跳出循环
            break;
    }
    if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
    // 以排队方式请求锁,acquireQueued方法如果在线程等待时调用了中断请求,才会返回true
    // 若发生过中断,且该中断不是在唤醒前,就将中断模式设为REINTERRUPT 1
        interruptMode = REINTERRUPT;
    if (node.nextWaiter != null) 
    // 下一个CONDITION结点存在,就顺便从condition_queue清理掉处于CANCEL状态的结点
        unlinkCancelledWaiters();
    if (interruptMode != 0)
    // -1就立刻抛出InterruptedException,
    // 1就对当前线程发起中断操作
        reportInterruptAfterWait(interruptMode);
}

这await倒是简单,就是将持有的锁许可全释放,然后阻塞等待唤醒。下面看看其中用的几个主要方法。

  • addConditionWaiter
private Node addConditionWaiter() {
    // 获取condition_queue尾结点
    Node t = lastWaiter;
    if (t != null && t.waitStatus != Node.CONDITION) {
    // 尾结点且状态不为CONDITION,从condition_queue清理掉处于CANCEL状态的结点
        unlinkCancelledWaiters();
        // 此时lastWaiter可能已经被更新,所以这里需要让t指向最新的lastWaiter
        t = lastWaiter;
    }
    // 创建一个状态为CONDITION的node,指向当前线程
    Node node = new Node(Thread.currentThread(), Node.CONDITION);
    if (t == null)
    // condition_queue为空,就将node作为头结点
        firstWaiter = node;
    else
    // condition_queue不为空,就将node尾插法插入condition_queue
        t.nextWaiter = node;
    // node作为condition_queue新的尾节点    
    lastWaiter = node;
    return node;
}
  • unlinkCancelledWaiters
// 这个方法作用就是将非CONDITION状态的节点从condition_queue中清除
private void unlinkCancelledWaiters() {
    Node t = firstWaiter;
    Node trail = null;
    while (t != null) {
    // 从头结点往后遍历 
        Node next = t.nextWaiter;
        if (t.waitStatus != Node.CONDITION) {
       // 这里的几步操作就是断开非CONDITION结点与其他结点的联系
            t.nextWaiter = null;
            if (trail == null)
            // 直接让头结点指向下一个结点
                firstWaiter = next;
            else
            // 之前的状态为CONDITION的节点nextWaiter指向当前结点的下一个结点
                trail.nextWaiter = next;
            if (next == null)
            // 如果遍历结束了,就更新lastWaiter指向最后一个CONDITION结点
                lastWaiter = trail;
        }
        else
        // trail指向当前循环到的最后一个状态CONDITION的结点
            trail = t;
        t = next;
    }
}
  • fullyRelease
final int fullyRelease(Node node) {
    boolean failed = true;
    try {
        // 锁许可个数
        int savedState = getState();
        if (release(savedState)) {
        // 释放锁许可完毕
            failed = false;
            // 返回释放前的总许可个数
            return savedState;
        } else {
        // 否则抛出IllegalMonitorStateException,代表锁状态异常
            throw new IllegalMonitorStateException();
        }
    } finally {
        // 锁释放失败,标记当前线程为撤销状态,会在不久后被移除
        if (failed)
            node.waitStatus = Node.CANCELLED;
    }
}
  • isOnSyncQueue
final boolean isOnSyncQueue(Node node) {
    if (node.waitStatus == Node.CONDITION || node.prev == null)
    // 当waitStatus仍是CONDITION
    // 或node还没加入lock wait_queue(加入后node.prev!=null)
    // 此时就返回false,说明仍为CONDITION状态,需要继续阻塞等待
        return false;
    if (node.next != null)
    // 在wait_queue中,node已经有后继节点了,说明肯定已经加入了wait_queue
    // 相当于是快速查找的一种trick,jdk里面这种trick很多
        return true;
    //  这里就很自然的从尾向前找,可以最快速度确认该节点是否在wait_queue中
    return findNodeFromTail(node);
}

2.3 condition.signal

public final void signal() {
    if (!isHeldExclusively())
    // 当前线程不持有锁,直接抛异常
        throw new IllegalMonitorStateException();
    Node first = firstWaiter;
    if (first != null)
        doSignal(first);
}

接着看doSignal方法:

// 该方法主要作用就是从condition_queue中头结点开始往后遍历
// 尝试将每次遍历的节点放回wait_queue
// 只要一个结点放回成功,就结束该循环
// 也就是说,只会从condition_queue中挑选一个线程结点放回wait_queue并按需唤醒。
// 当然,在遍历过程中会从condition_queue清理掉那些状态不再是CONDITION的结点
// 最终,firstWaiter指向移到wait_queue的那个节点的下一个节点(可能为null)
private void doSignal(Node first) {
   do {
       // 注意,每次循环让firstWaiter指向下一个结点
       if ( (firstWaiter = first.nextWaiter) == null)
       // 如果当前结点的下一个结点为空,
       // 就把lastWaiter也置为null,代表condition_queue为空
           lastWaiter = null;
       // 从condition_queue中头结点开始唤醒
       // 这里就先把他的nextWaiter置空
       first.nextWaiter = null;
   } while (!transferForSignal(first) &&
            (first = firstWaiter) != null);
}

看来,问题的关键在transferForSignal方法:

// 当该结点指向的线程状态不是CONDITION(如CANCEL),返回false
// 否则,将结点放回wait_queue,并根据前驱结点的状态按需unpark当前线程,返回true
final boolean transferForSignal(Node node) {
     // CAS方式将CONDITION_WAIT状态的node的waitStatus重置为0
    if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
        // 失败,代表已经该Node指向的线程被撤销了(waitStatus=-1),返回false
        return false;
        
    // 将该node重新放入等待锁的wait_queue中
    // 成功入队后,返回该节点的前驱结点p
    Node p = enq(node);
    int ws = p.waitStatus;
    if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
    // 前驱结点为撤消状态(ws=1)或是结点p的状态修改CAS(ws,SIGNAL)失败
        // 就unpark唤醒当前线程,
        // 然后走前面提到的AQS.acquireQueued方法里的唤醒流程
        LockSupport.unpark(node.thread);
    // 最后返回true    
    return true;
}

2.4 源码小结

代码分析完了,其实Condition思想很朴素:

  1. await方法,将当前线程加入AQS.condition_queue,且会顺便清理其中不为CONDITION状态的结点
  2. await方法,让当前线程释放所有的锁许可(state归0)
  3. await方法,将当前线程阻塞,直到该线程被放入AQS.wait_queue
  4. signal方法,将从AQS.condition_queue队列的头结点开始往后遍历,从AQS.condition_queue中将该线程结点移除,并放回AQS.wait_queue,并根据前驱结点是否已经撤销或异常按需唤醒当前结点。注意,此过程只要成功移动一个节点,遍历就结束了,也就是说每次signal方法最多只能从AQS.condition_queue中移动一个结点到AQS.wait_queue
  5. signal方法,在上述遍历移动节点过程中会顺便清理掉AQS.condition_queue中那些状态不为CONDITION的结点
  6. await方法,阻塞的线程因为被signal方法重新放入AQS.wait_queue而被其他前驱结点唤醒,此时有几种情况:
    1. 意外情况。LockSupport有可能会因为意外导致线程唤醒,该情况和情况2处理相同,需要再次判断节点是否已经放入AQS.wait_queue
    2. wait_queue中该结点的前驱结点执行unlock方法时唤醒。处理同情况1
    3. 其他线程调用signal方法前调用中断方法唤醒,需要重设interruptMode
    4. 其他线程调用signal方法后调用中断方法唤醒,需要重设interruptMode
  7. await方法,该节点调用acquireQueued走申请锁许可流程。注意,如果此时申请不到锁,线程又会被LockSupport.park阻塞。
  8. await方法,会又一次顺便清理其中不为CONDITION状态的结点
  9. await方法,按阻塞前后收到中断请求的情况按需发起中断
  10. await方法返回,可继续执行用户代码

0x03 await/signal对比wait/notify

关于wait/notify介绍可以参考这篇文章:Java-多线程-wait/notify

3.1 Condition和Object关系

等待 唤醒 唤醒全部
Object wait notify notifyAll
Condition await signal signalAll

3.2 wait和await对比

中断 超时精确 Deadline
wait 可中断 可为纳秒 不支持
await 支持可中断/不可中断 可为纳秒 支持

3.3 notify和signal对比

全部唤醒 唤醒顺序 执行前提 逻辑
notify 支持,notifyAll 随机(jdk写的,其实cpp源码是一个wait_queue,FIFO) 拥有锁 从wait_list取出,放入entry_list,重新竞争锁
signal 支持,signalAll 顺序唤醒 拥有锁 从condition_queue取出,放入wait_queue,重新竞争锁

3.4 底层原理对比

  • Object的阻塞和唤醒,是基于synchronized的。底层实现是在cpp级别,调用synchronized的线程对象会放入entry_list,竞争到锁的线程处于active状态。调用wait方法后,线程对象被放入wait_queue。而notify会按FIFO方法从wait_queue中取得一个对象并放回entry_list,这样该线程可以重新竞争synchronized同步锁了。
  • Condition的阻塞唤醒,是基于lock的。lock维护了一个wait_queue,用于存放等待锁的线程。而Condition也维护了一个condition_queue。当拥有锁的线程调用await方法,就会被放入condition_queue;当调用signal方法,会从condition_queue选头一个满足要求的节点移除然后放入wait_queue,重新竞争lock。

3.5 应用场景对比

  • Object使用比较单一,只能针对一个条件。
  • 一个ReentrantLock可以有多个Condition,对应不同条件。比如在生产者消费者可以这样实现:
private static ReentrantLock lock = new ReentrantLock();
	
private static Condition notEmpty = lock.newCondition();
private static Condition notFull = lock.newCondition();

// 生产者
public void produce(E item) {
	lock.lock();
	try {
		while(isFull()) {
		// 数据满了,生产者就阻塞,等待消费者消费完后唤醒
			notFull.await();
		}
		
		// ...生产数据代码
		
		// 唤醒消费者线程,告知有数据了,可以消费
		notEmpty.signal();
	} catch (InterruptedException e) {
		e.printStackTrace();
	} finally {
		lock.unlock();
	}
}

// 消费者
public E consume() {
	lock.lock();
	try {
		while(isEmpty()) {
		// 数据空了,消费者就阻塞,等待生产者生产数据后唤醒
			notEmpty.await();
		}
		
		// ...消费数据代码
		
		// 唤醒生产者者线程,告知有数据了,可以消费
		notFull.signal();
		return item;
	} catch (InterruptedException e) {
		e.printStackTrace();
	} finally {
		lock.unlock();
	}
	return null;
}

这样好处就很明显了。如果使用Object,那么唤醒的时候也许就唤醒了同类的角色线程。而使用condition可以在只有一个锁的情况下,实现我们想要的只唤醒对方角色线程的功能。

0x04 总结

Condition特点如下:

  • Condition必须搭配AQS.sync使用
  • await过程可中断
  • Condition的阻塞唤醒,是基于lock的。lock维护了一个wait_queue,用于存放等待锁的线程。而Condition也维护了一个condition_queue。当拥有锁的线程调用await方法,就会被放入condition_queue;当调用signal方法,会从condition_queue选头一个满足要求的节点移除然后放入wait_queue,重新竞争lock。
  • 一个lock可以对应多个Condition,在多条件情况十分方便

关于Reentrant的分析,请参见本文姊妹篇:Java-并发-锁-ReentrantLock

0xFF 参考文档

Condition.await, signal 与 Object.wait, notify 的区别

更多关于Java锁的信息,可参考文章:Java-并发-关于锁的一切

猜你喜欢

转载自blog.csdn.net/baichoufei90/article/details/85161987
今日推荐