java多线程解说【陆】_锁实现:Condition的实现


上篇文章:java多线程解说【伍】_锁实现:ReentrantLock的实现


上文中我们分析了ReentrantLock的底层如何通过AQS来实现锁的申请和释放的,以及公平锁、非公平锁的区别。那么在ReentrantLock的锁机制中,还支持一种基于条件锁的实现,这就是Condition。那么这个Condition的底层是如何实现的呢,下面我们尝试进行分析。


ConditionObject的结构


还是先看看juc包的类继承图:




我们可以看到Condition和Lock一样,也是一个接口,在AQS中有一个内部类实现了它,就是ConditionObject。那么这个ConditionObject的构造是什么样的呢?


public class ConditionObject implements Condition, Serializable{
    private static final long serialVersionUID = 1173984872572414699L;
    private transient AbstractQueuedSynchronizer.Node firstWaiter;
    private transient AbstractQueuedSynchronizer.Node lastWaiter;
    private static final int REINTERRUPT = 1;
    private static final int THROW_IE = -1;
}



我们这里先只看一下它的成员变量,比较主要的就是firstWaiter和lastWaiter,他们分别表示第一个等待者和最后一个等待者。既然有第一个和最后一个,说明在Condition模式里也维护了一个队列。那么这个队列和我们上文中说过的CLH队列有何关系呢?


答案是没有关系,或者说在创建阶段是相互独立的,虽然它们共用了AQS中的节点。在Condition等待队列中,只使用到了节点对象Node中的waitStatus(枚举值为-2)和nextWaiter两个字段。而且Condition等待队列是单向的链表,只有后置引用(nextWaiter),这不像CLH队列的双向的(prev和next)。


而作为Condition的实现ConditionObject,也和CLH队列一样,维护了队列的队头和队尾(firstWaiter和lastWaiter)。但有一点区别是,CLH队列的头结点(head)是个虚节点,没有关联线程;而Condition等待队列的头结点是关联线程的。


Condition的实现


和上文保持一样的结构,这里我们说一下Condition是如何实现的吧。


1.当使用lock.newCondition创建一个Condition对象时,其实就是new出来了一个新的ConditionObject对象;


2.当线程1调用ConditionObject.await()方法时,会进入如下逻辑:

  2.1 判断线程1是否被中断,如果是则抛出InterruptedException异常;

  2.2 进入创建当前ConditionObject对象的等待队列逻辑:

    2.2.1 判断等待队列的最后一个等待者(lastWaiter)不为空且等待状态(waitStatus)不等于-2(Condition),则将等待队列中所有等待状态不为-2的等待者清除掉,重新关联队列;

    2.2.2 重新获取最后一个等待者(lastWaiter),判断是否为空:

      2.2.2.1 如果为空,则说明等待队列已经空了,把第一个等待者(firstWaiter)和最后一个等待者(lastWaiter)都指向线程1;

      2.2.2.2 如果不为空,则把最后一个等待者(lastWaiter)都指向线程1;

  2.3 清理最后一个等待者(其实就是线程1)上的所有锁(让state=0),此举目的就是ran线程1脱离CLH队列;

  2.4 判断线程1是否不在CLH队列上(等待状态为-2 或 前序节点(prev)为空),如果不在则阻塞线程1;


(到这里,线程1已经进入了等待队列,此时如果有线程2也调用上面ConditionObject的await()方法,那么它也会进入等待队列,排在线程1之后)


3.当线程3调用ConditionObject.signal()方法时,会进入如下逻辑:

  3.1 判断持有对象锁的线程(exclusiveOwnerThread)是否为当前线程,如果不是则抛出IllegalMonitorStateException异常;

  3.2 获取第一个等待者(firstWaiter),如果其不为空,则进入唤醒逻辑:

    3.2.1 判断第一个等待者(firstWaiter)的后续节点是否为空,如果为空则设置最后一个等待者(lastWaiter)为空(其实这种情况说明等待队列中只有一个等待者);

    3.2.2 尝试将第一个等待者(firstWaiter)的等待状态(waitStatus)由-2(Condition)置为0(初始状态),如果失败则说明该节点已被中断(waitStatus>0),则重复3.2.1操作;

    3.2.3 将线程3加入到CLH队列(具体逻辑仍然是自旋循环,伪代码可参照上篇文章2.3.1和2.3.2的描述);

    3.2.4 如果线程3的前序节点的等待状态(waitStatus)大于0,说明它已经被中断,直接唤醒线程3;

    3.2.5 如果设置线程3的前序节点的等待状态(waitStatus)为-1(Runnable)失败,则说明可能存在暂时的错误,一样还是直接唤醒线程3;

直接唤醒线程3


(当signal方法调用完毕后,线程2并没有马上开始执行,因为此刻只是说它已经加入了CLH队列而已,具体的获得锁还需要排队以及线程3对锁的释放(Lock.unlock())。)


至此,Condition的await()和signal()方法的底层实现伪代码就说完了,和ReentrantLock的lock()和unLock()方法一样,其实现逻辑并不是特别直观易懂,建议结合JDK源码及这篇文章一起看,再回来看上面的伪代码时或许就会有新的感悟。


await()/signal()的使用


很多书上都说,Condition的await()/和signal()是用来替代Object的wait()和notify()方法的。它们之间有什么相同点呢?


1.都要放在临界区内操作。Condition的await()/和signal()的调用都要发生在显式锁Lock的lock()和unlock()之间,Object的wait()和notify()一样要在synchronized(Object)的同步块中;


2.在ReentrantLock的默认实现(非公平锁下),condition的唤醒是和Object的唤醒都是随机实现的(但也不完全一样,因为非公平锁只是会有未入队列的线程和队列中第一个线程进行锁的竞争,随机性仅出自这里);


3.同样因为可能存在虚假唤醒的可能,Condition的await()和Object的wait()方法都要在一个循环里调用;


它们的不同点如下:


1.一个Lock可以创建多个Condition,这样就可以实现基于多个业务场景进行不同维度的唤醒;而Object的唤醒只能是单维度的;


2.Condition的等待/唤醒是基于一段业务逻辑的处理,而Object的等待/唤醒只是基于一个对象的操作;


后面一篇文章,我们写一个Condition模式的等待/唤醒demo并解析一下。



  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值