Java 并发编程:AQS 的公平性
所谓公平是指所有线程对临界资源申请访问权限的成功率都一样,它不会让某些线程拥有优先权。通过几篇文章的分析我们知道了 JDK 的 AQS 的锁是基于 CLH 锁进行优化的,而其中使用了 FIFO 队列,也就是说等待队列是一个先进先出的队列。那是否就可以说每条线程获取锁时就是公平的呢?关于公平性,严格来说应该分成三个点来看:入队阶段、唤醒阶段以及闯入策略。
友情链接:
入队阶段
唤醒阶段
当线程节点成功加入等待队列后便成为等待队列中的节点,而且这是一个先入先出队列,那么我们可以得到一个结论:队列中的所有节点是公平的。因为等待队列中的所有节点都按照顺序等待自己被前驱节点唤醒并获取锁,所以等待队列中的节点具有公平性。
闯入策略
闯入策略是 AQS 框架为了提升性能而设计的一个策略,具体是指一个新线程到达共享资源边界时不管等待队列中是否存在其它等待节点,新线程都将优先尝试去获取锁,这看起来就像是闯入行为。闯入策略破坏了公平性,AQS 框架对外体现的公平性主要也由此体现。
AQS 提供的锁获取操作运用了可闯入算法,即如果有新线程到来先进行一次获取尝试,不成功的情况下才将当前线程加入等待队列。如下图,等待队列中节点线程按照顺序一个接一个尝试去获取共享资源的使用权。而某一时刻头结点线程准备尝试获取的同时另外一条线程闯入,新线程并非直接加入等待队列的尾部,而是先跟头结点线程竞争获取资源。闯入线程如果成功获取共享资源则直接执行,头结点线程则继续等待下一次尝试。如此一来闯入线程成功插队,后来的线程比早到的线程先执行,说明 AQS 锁获取算法是不严格公平的。
闯入逻辑
下图是包含了闯入策略的锁获取算法伪代码,我们主要关注红色方框的逻辑。它会优先直接去尝试获取锁,如果获取失败(即闯入失败)才创建节点并加入到等待队列的尾部。
为什么需要闯入策略
为什么要使用闯入策略呢?闯入策略通常可以提升总吞吐量。由于一般同步器颗粒度比较小,也可以说共享资源的范围较小,而线程从阻塞状态到被唤醒所消耗的时间周期可能是通过共享资源时间周期的几倍甚至几十倍。
如此一来线程唤醒过程中将存在一个很大的时间周期空窗期,导致资源没有得到充分利用,同时如果每个线程都先入队再唤醒的话也会导致效率低下。为了避免没必要的线程挂起和唤醒,也为了提高吞吐量,于是引入这种闯入策略。它可以充分利用阻塞唤醒空窗期,也避免了无谓的挂起和唤醒操作,从而大大增加了吞吐率。
闯入机制的实现对外提供一种竞争调节机制,开发者可以在自定义同步器中定义闯入尝试获取的次数。假设次数为 n 则不断重复获取直到 n 次都获取不成功才把线程加入等待队列中,随着次数 n 的增加可以增大成功闯入的几率。 同时,这种闯入策略可能导致等待队列中的线程饥饿,因为锁可能一直被闯入的线程获取。但由于一般持有同步器的时间很短暂所以能避免饥饿的发生,反之如果持有锁的时间较长,则将大大增加等待队列无限等待的风险。
总结
实际情况中我们要根据需求制定策略,在一个公平性要求很高的场景,则可以把闯入策略去除掉以达到公平。在自定义同步器中可以通过 AQS 预留方法 tryAcquire 方法实现,只需判断当前线程是否为等待队列中头结点对应的线程即可。若不是则直接返回 false,尝试获取失败。但这种公平性是相对于 Java 语义层面上的公平性,在现实中 JVM 的实现可能也会直接影响线程执行的顺序。
版权声明: 本文为 InfoQ 作者【码农架构】的原创文章。
原文链接:【http://xie.infoq.cn/article/9bbe09425c805f7706ad73125】。文章转载请联系作者。
评论