JAVA 中REENTRANT LOCK 中公平参数的用途是什么?

What is the purpose of fairness parameter in REENTRANT LOCK in JAVA?

我在查看 Java 重入锁文档时发现了以下文本:

fairness of locks does not guarantee fairness of thread scheduling. Thus, one of many threads using a fair lock may obtain it multiple times in succession while other active threads are not progressing and not currently holding the lock.

根据我的理解,这意味着,如果 OS 调度程序调度同一个线程(之前正在获取锁)并且它尝试再次获取相同的锁,Java 将允许它获得并不会服从公平参数值。有人能告诉我公平参数的目的是什么,在什么情况下应该使用它。
我只是在想如果它只是一个优先级值,这可能会影响调度程序但不能保证线程执行顺序。

fairness of locks does not guarantee fairness of thread scheduling. Thus, one of many threads using a fair lock may obtain it multiple times in succession while other active threads are not progressing and not currently holding the lock.

我将“没有进展”解释为“由于 与所讨论的锁无关的原因而没有进展。”我认为他们试图告诉你“公平" 仅当锁竞争如此激烈以至于经常有一个或多个线程等待轮到他们锁定它时才有意义。

如果线程 T 释放当前没有其他线程正在等待的“公平”锁,则“公平”不会影响下一个线程将获得它。这只是线程之间的直接竞争,由 OS 调度程序主持。

只有当多个线程在等待时,公平锁才应该“优先”等待时间最长的线程。特别是,我 希望 如果某个线程 T 释放了其他线程正在等待的“公平”锁,然后线程 T 立即 尝试再次锁定它,lock() 函数会注意到其他等待线程,并将 T 发送到队列的后面。

但是,我实际上并不知道它在任何特定的 JVM 中是如何实现的。


P.S., IMO,“公平”就像绷带一样,可以止住复合骨折的出血。如果你的程序有一个锁,竞争如此激烈以至于“公平”会产生任何影响,那么这就是一个严重的设计缺陷。

The same Javadoc 还说,

Programs using fair locks accessed by many threads may display lower overall throughput (i.e., are slower; often much slower) than those using the default setting.

在天真的观点中,使用公平锁的线程的行为就像

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

“尝试获取”指的是对 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() 次尝试的时间时,坚持以不可预测的、未知的顺序获取锁并不是很有用。开发人员仍然希望公平的一种解释是,即使结果顺序不可预测,也应确保每个线程都取得进展,而不是在其他线程反复超车时无限等待锁。但这又把我们带回了不公平的线程调度;即使根本没有锁,也不能保证所有线程都能取得进展。

那么为什么公平选项仍然存在?因为有时候,在大多数情况下,人们对它的工作方式没有意见,即使没有强有力的保证它会始终以这种方式工作。或者简单地说,因为如果它不存在,开发人员会反复要求它。支持公平性成本不高,不影响非公平锁的性能。

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:

One reason barging locks perform so much better than fair locks under heavy contention is that there can be a significant delay between when a suspended thread is resumed and when it actually runs. Let's say thread A holds a lock and thread B asks for that lock. Since the lock is busy, B is suspended. When A releases the lock, B is resumed so it can try again. In the meantime, though, if thread C requests the lock, there is a good chance that C can acquire the lock, use it, and release it before B even finishes waking up. In this case, everyone wins: B gets the lock no later than it otherwise would have, C gets it much earlier, and throughput is improved.

Fair locks tend to work best when they are held for a relatively long time or when the mean time between lock requests is relatively long. In these cases, the condition under which barging provides a throughput advantage ‐ when the lock is unheld but a thread is currently waking up to claim it ‐ is less likely to hold.