JAVA中REENTRANT LOCK中公平参数的目的是啥?
Posted
技术标签:
【中文标题】JAVA中REENTRANT LOCK中公平参数的目的是啥?【英文标题】:What is the purpose of fairness parameter in REENTRANT LOCK in JAVA?JAVA中REENTRANT LOCK中公平参数的目的是什么? 【发布时间】:2022-01-09 22:29:24 【问题描述】:我在浏览可重入锁的Java文档时发现了以下文字:
锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的许多线程之一可能会连续多次获得它,而其他活动线程没有进展并且当前没有持有锁。
根据我的理解,这意味着,如果操作系统调度程序调度相同的线程(之前获取锁)并尝试再次获取相同的锁,Java 将允许它获取并且不会遵守公平参数值.有人可以告诉我们公平参数的目的是什么以及应该在什么情况下使用它。 我只是在想它是否只是一个优先级值,这可能会影响调度程序但不能保证线程执行顺序。
【问题讨论】:
线程调度的不同之处在于,您可以拥有比系统拥有的内核多很多倍的活动线程,因此有些线程正在等待获得处理时间。另一方面,如果多个线程正在积极尝试获取锁,则锁的公平性参数将影响它们中的哪一个获得它。文档所说的是,您可能有许多线程可能想要锁定,但如果线程调度程序只执行其中一个线程,即使它之前已经持有它也会获得锁定。 @Thomas 所以它就像对操作系统的提示。与优先级值相同。 不,不是,锁自己处理公平性。 Thead 调度也将由 JVM 处理,但也会受到操作系统的影响,即 JVM 进程本身的调度方式。对于锁,请考虑以下情况:线程 A 获得锁并释放它,现在线程 A 和 B 会再次想要锁,但由于 B 的调度晚于 A,A 再次获得锁,因为没有人在等待它(B 没有'不要尝试获取锁)。 @Thomas 你知道线程 B 不会被考虑,因为当 A 再次尝试获取锁时,它处于阻塞状态而不是可运行状态? 不,B 可能处于活动状态且可运行,但调度程序可能只是不运行它或出于任何原因足够早地运行它。这两个概念只是松散相关 - 检查所罗门和霍尔格的答案,他们正在更详细地描述这一点。 【参考方案1】:在幼稚的观点中,使用公平锁的线程的行为会像
Thread 1 | Thread 2 | Thread 3 |
---|---|---|
Acquire | Do something | Do something |
Critical Section | Try Acquire | Do something |
Critical Section | Blocked | Try Acquire |
Release | Acquire | Blocked |
Do something | Critical Section | Blocked |
Try Acquire | Release | Acquire |
Blocked | Do something | Critical Section |
Acquire | Do something | Release |
“Try Acquire”是指对lock()
的调用,它不会立即成功,因为另一个线程拥有锁。它不是指tryLock()
,这通常是不公平的。
在这种幼稚的观点中,线程按“线程 1”、“线程 2”、“线程 3”的顺序获取锁,因为这是获取尝试的顺序。尤其是当“线程 1”试图在“线程 2”释放它的同时立即获取锁时,它不会像不公平锁那样超过,而是“线程 3”得到它,因为它等待的时间更长。
但是,正如文档所说,线程调度是不公平的。所以可能会发生以下情况。
Thread 1 | Thread 2 | Thread 3 |
---|---|---|
Acquire | Do something | Do something |
Critical Section | Do something | |
Critical Section | ||
Release | ||
Do something | ||
Acquire | Try Acquire | Try Acquire |
Critical Section | Blocked | Blocked |
Critical Section | Blocked | Blocked |
空单元格表示线程根本没有获得任何 CPU 时间的阶段。线程可能比 CPU 内核多,其中包括其他进程的线程。操作系统甚至可能更愿意让“线程 1”在内核上继续运行,而不是切换到其他线程,这仅仅是因为该线程已经运行并且切换需要时间。
一般来说,尝试预测到达某个点的相对时间(如前面的工作负载获取锁)并不是一个好主意。在具有优化 JIT 编译器的环境中,即使是两个线程在完全相同的输入下执行完全相同的代码也可能具有完全不同的执行时间。
因此,当我们无法预测 lock()
尝试的时间时,坚持以不可预测的未知顺序获取锁并不是很有用。开发人员仍然希望公平的一种解释是,即使结果顺序不可预测,它也应该确保每个线程都取得进展,而不是在其他线程反复超车时无限等待锁。但这让我们回到了不公平的线程调度;即使根本没有锁,也不能保证所有线程都会进行。
那么为什么公平选项仍然存在?因为有时,人们对它在大多数情况下的工作方式感到满意,即使没有强有力的保证它会一直以这种方式工作。或者简单地说,因为如果它不存在,开发人员会反复要求它。支持公平的成本并不高,也不会影响不公平锁的性能。
【讨论】:
首先非常感谢您付出了这么多努力使其变得简单易懂。我假设 1) 当 Thread2,3 不在 CPU 上运行时,Thread1 发出锁获取请求(上表中缺少)。 2) 来自 JAVA 文档的“未进行”注释是指线程没有获得任何 CPU 时间而不是处于“阻塞状态”的时间。 没有 CPU 时间是“没有进展”的最常见原因。其他原因可能是“运行异常缓慢”,因为 JIT 还没有启动,或者“因为堆栈代码替换而停止”,因为 JIT 确实启动或“等待垃圾收集器”分配尝试。 阻塞状态和等待 CPU 在这里实际上是相同的,因为根据文档“如果一个线程正在等待其他线程获取的锁,它会被禁用以用于线程调度目的”。在阻塞状态下,它没有给任何 cpu 时间。 阻塞状态是公平锁考虑的lock()
尝试的结果。在lock()
尝试之前没有从调度程序获取 CPU 时间是锁不知道的。在这两种情况下,线程都不会获得 CPU 时间,但与锁有关的区别就是这一切。【参考方案2】:
锁的公平性并不能保证线程调度的公平性。因此,使用公平锁的许多线程之一可能会连续多次获得它,而其他活动线程没有进展并且当前没有持有锁。
我将“没有进展”解释为“没有进展的原因与所讨论的锁定无关。”我认为他们试图告诉你“公平”仅在以下情况下才有意义锁的竞争非常激烈,以至于经常有一个或多个线程在等待轮到它们来锁定它。
如果线程 T 释放当前没有其他线程正在等待的“公平”锁,那么“公平”不会影响下一个线程将获得它。这只是线程之间的直接竞赛,由 OS 调度程序主持。
只有当多个线程在等待时,一个公平的锁才应该“支持”等待时间最长的那个。特别是,我希望如果某个线程 T 释放了其他线程正在等待的“公平”锁,然后线程 T 立即尝试再次锁定它,那么lock()
函数会注意到其他正在等待的线程,并将 T 发送到队列的后面。
但是,我实际上并不知道它是如何在任何特定的 JVM 中实现的。
P.S.,IMO,“公平”就像绷带,可以止血复合骨折。如果您的程序有一个竞争激烈的锁,以至于“公平”会产生任何影响,那么这是一个严重的设计缺陷。
The same Javadoc 还说,
使用由许多线程访问的公平锁的程序可能会显示出比使用默认设置的程序更低的整体吞吐量(即,速度较慢;通常要慢得多)。
【讨论】:
您好,那么当fairness设置为true时,是否会破坏可重入锁的属性? @Turtle 不,线程可以重新获取锁,如果它已经持有。这里的场景是线程在释放锁后尝试获取锁。 “如果某个线程 T 释放了其他线程正在等待的“公平”锁,”。这里其他线程指的是当时处于运行状态并试图获取锁的线程,对吧?让我们说例如共有 3 个线程 T1、T2、T3 按到达时间排序。在T释放锁并再次尝试获取它的时候,只有T2处于运行状态,其他处于等待状态。 T2 会获取锁吗?请纠正我在这里有点迷路。感谢您的帮助 @Tarun,Java 中没有RUNNING
状态。只有RUNNABLE
,这意味着JVM 不知道为什么不允许线程运行的任何原因。 (即,实际上正在运行的线程仍然被JVM称为RUNNABLE
。等待监视器锁的线程的状态是BLOCKED
。我不知道顶部在我的脑海中,JVM 分配给等待ReentrantLock
的线程的状态是什么,但如果我负责,那也将是BLOCKED
。
一个实际的例子是项目 Loom 的虚拟线程,它在当前实现中没有抢占式切换,也许永远不会有。因此,一个成功获得锁且永远没有理由放弃 CPU 的线程将继续运行,而当没有其他本机线程可用时,其他虚拟线程将无法继续运行。【参考方案3】:
ReentrantLock
是基于 AbstractQueuedSynchronizer
实现的,这是一个先进先出 (FIFO) 等待队列。
假设A、B、C三个线程依次尝试获取锁,A获取了锁,那么B、C会转化为AbstractQueuedSynchronizer#Node
进入队列。这两个线程将被挂起。
当A线程释放锁时,会唤醒其后继节点(AbstractQueuedSynchronizer#unparkSuccessor
),即线程B。线程B被唤醒后会再次尝试获取锁。
假设当B线程被唤醒时,突然有一个D线程来尝试获取这个锁。对于公平锁,D线程看到队列中还有其他节点在等待获取锁(AbstractQueuedSynchronizer#hasQueuedPredecessors
),直接挂掉。
而且对于不公平锁,D线程会立即尝试获取这个锁,这意味着它可以尝试“跳队列”一次。如果这次“队列跳转”成功,那么可以立即获取锁(这意味着节点B将再次被挂起:它在与D线程的竞争中输了,它被切线了)。如果失败则挂起,作为Node进入队列。
为什么不公平锁表现更好以及何时使用公平锁?
这是来自Java-Concurrency-Practice:
在激烈争用的情况下,插入锁比公平锁性能好得多的一个原因是,在暂停的线程恢复和实际运行之间可能存在显着延迟。假设线程 A 持有一个锁,而线程 B 请求该锁。由于锁忙,B被挂起。当 A 释放锁时,B 会恢复,以便再次尝试。同时,如果线程 C 请求锁,C 很有可能在 B 完成唤醒之前获取锁、使用它并释放它。在这种情况下,每个人都会获胜:B 获得锁的时间不会晚于其他情况,C 获得锁的时间要早得多,并且吞吐量得到了提高。
公平的锁在持有时间相对较长时往往效果最佳 时间或锁定请求之间的平均时间相对较长时。 在这些情况下,驳船提供 吞吐量优势 - 当锁未被持有但线程被持有时 目前醒来声称它 - 不太可能持有。
【讨论】:
确实,正如this answer 中所解释的,公平锁和不公平锁之间唯一的实现区别是当锁刚刚释放时对到达线程的处理。 和·when the lock just have been released.
有关系吗?根据我对源码的理解,unfair
锁无论如何都会尝试获取一次锁,如果恰好此时释放,那么线程就有可能获得锁。但是fair
lock 会先调用hasQueuedPredecessors
来查看当前是否有其他线程在等待锁。 @霍尔格
是的,当当前所有者释放锁时,它会从队列中取消一个线程,如果存在的话。然后,未停放的线程将尝试获取锁。到目前为止,如果锁不公平或使用的线程tryLock()
通常忽略公平性,则另一个线程(包括刚刚释放锁的线程)可能会超越并(重新)绕过队列获取锁政策。为先前排队的线程提供新的 CPU 时间所花费的时间越多,其他人超车的机会就越大,收益也越高。
我明白你的意思。如果锁没有被释放,那么公平锁和不公平锁实际上没有区别。我想表达的是,如果锁没有被释放,那么两个锁的行为(代码级别)还是不一样的……但最终的结果其实是一样的。我的发言没有说明这一点。谢谢@Holger以上是关于JAVA中REENTRANT LOCK中公平参数的目的是啥?的主要内容,如果未能解决你的问题,请参考以下文章
Redission 最常用的可重入锁(Reentrant Lock)