java - 关于同步关键字?
问题描述
ReentrantLock 不是重点,关键是同步的代码块。连续“获得AAA”的概率很高。
public class ReentrantLockTest {
private static final Lock lock = new ReentrantLock(true);
public static void main(String[] args) {
new Thread(()->test(),"AAA").start();
new Thread(()->test(),"BBB").start();
new Thread(()->test(),"CCC").start();
new Thread(()->test(),"DDD").start();
}
public static void test() {
for(int i = 0 ; i < 3;i++) {
try{
synchronized (lock) {
System.out.println(Thread.currentThread().getName()+" get,");
TimeUnit.SECONDS.sleep(2);
}
}catch(InterruptedException e) {
e.printStackTrace();
}finally {
}
}
}
}
结果:
AAA get,
AAA get,
AAA get,
DDD get,
DDD get,
DDD get,
CCC get,
CCC get,
CCC get,
BBB get,
BBB get,
BBB get,
如果锁刚刚被释放,这似乎更容易获得锁,那为什么呢?这是编译器优化吗?
解决方案
在我深入之前要注意几点:
未指定 Java 线程调度程序的行为。这里实际发生的一切都在正确行为的“范围”内。
您的测试代码没有测试
ReentrantLock
这里的行为。您实际上将锁实例用作原始监视器或互斥体。您不synchronized
与Lock
对象一起使用。
看来,在您运行代码的平台上,最后执行的线程似乎确实得到了偏好……在一定程度上。我的行为与您报告的类似,但是当线程循环更长时,我确实看到了切换。相比之下,皮罗看到了不同的行为。(我会这样做可能是由操作系统差异引起的。)
那么这里发生了什么?好吧,实际行为部分取决于操作系统的本机线程调度程序实现,部分取决于实际发生的情况。
默认情况下,典型的本地线程调度程序不会实现“公平”调度。如果两个本机线程具有相同的优先级,则不能保证每个线程都会获得公平的份额。相反,调度器可以优化以最小化上下文切换开销或调度开销。因此,可以预期不同操作系统的不同行为。
要考虑的另一件事是此示例中线程的行为。持有监视器的线程执行以下操作:
- 它从
sleep
- 它释放监视器
- 它解开另一个线程
- 它尝试重新获取监视器
未停放的线程也尝试获取监视器。
那么,问题是哪家将竞相收购显示器?看起来最初持有监视器的线程通常会获胜(至少在我的机器上)。
为什么?这很难说:
- 可能是线程在调度另一个线程之前从 unpark 系统调用返回。
- 可能是未停放的线程通常受到 TLAB 未命中或内存缓存未命中的阻碍。
- 可能是其他事情;例如我没有想到的事情。
但无论如何,这不太可能是一个深思熟虑的设计决定,将一个线程优先于另一个线程。它可能就是这样发生的。
(JVM 中监视器的实际实现非常复杂。看看OpenJDK 11 代码库中的synchronizer.cpp
and objectMonitor.cpp
...。那只涉及 JVM 端。)
这是编译器优化吗?
我不这么认为。JIT 编译器确实优化了监视器的进入和退出序列,但仅在监视器未竞争时处理“快速路径”进入/退出。在这个例子中,监视器是竞争的。
推荐阅读
- javascript - 如何防止基于日期比较的表单提交
- ios - SwiftUI ScrollView 和 OnLongPressGesture
- python - 你可以让程序在python中创建变量吗?
- javascript - 本文档需要 Angular 11 中的“TrustedScriptURL”分配
- linux - 如何创建一个运行五个 osmocom 脚本的 sh 脚本?
- php - 尝试在 null 上读取属性“名称”
- typescript - Aurelia Webpack 路由器使用时出错
- python - openpiv 的构建轮 (PEP 517)
- mysql - mysql 如果列为空,则选择,如果不为空,则应用条件
- flutter - flutter int got to zero