首页 > 解决方案 > 关于同步关键字?

问题描述

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

解决方案


在我深入之前要注意几点:

  • 未指定 Java 线程调度程序的行为。这里实际发生的一切都在正确行为的“范围”内。

  • 您的测试代码没有测试ReentrantLock这里的行为。您实际上将锁实例用作原始监视器或互斥体。您不synchronizedLock对象一起使用。


看来,在您运行代码的平台上,最后执行的线程似乎确实得到了偏好……在一定程度上。我的行为与您报告的类似,但是当线程循环更长时,我确实看到了切换。相比之下,皮罗看到了不同的行为。(我会这样做可能是由操作系统差异引起的。)

那么这里发生了什么?好吧,实际行为部分取决于操作系统的本机线程调度程序实现,部分取决于实际发生的情况。

默认情况下,典型的本地线程调度程序不会实现“公平”调度。如果两个本机线程具有相同的优先级,则不能保证每个线程都会获得公平的份额。相反,调度器可以优化以最小化上下文切换开销或调度开销。因此,可以预期不同操作系统的不同行为。

要考虑的另一件事是此示例中线程的行为。持有监视器的线程执行以下操作:

  1. 它从sleep
  2. 它释放监视器
  3. 它解开另一个线程
  4. 它尝试重新获取监视器

未停放的线程也尝试获取监视器。

那么,问题是哪家将竞相收购显示器?看起来最初持有监视器的线程通常会获胜(至少在我的机器上)。

为什么?这很难说:

  • 可能是线程在调度另一个线程之前从 unpark 系统调用返回。
  • 可能是未停放的线程通常受到 TLAB 未命中或内存缓存未命中的阻碍。
  • 可能是其他事情;例如我没有想到的事情。

但无论如何,这不太可能是一个深思熟虑的设计决定,将一个线程优先于另一个线程。它可能就是这样发生的。

(JVM 中监视器的实际实现非常复杂。看看OpenJDK 11 代码库中的synchronizer.cppand objectMonitor.cpp...。那只涉及 JVM 端。)


这是编译器优化吗?

我不这么认为。JIT 编译器确实优化了监视器的进入和退出序列,但仅在监视器未竞争时处理“快速路径”进入/退出。在这个例子中,监视器是竞争的。


推荐阅读