关于同步关键字



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,    

如果锁刚刚松开,这似乎会让你更容易拿到锁,那么为什么呢?这是编译器优化吗?

如果锁刚刚被释放,这似乎会让获取锁变得更容易,为什么呢?

这样看:想象一下,每次到达同步块时,线程都会发生变化。这将不是很有效(因为更改线程是有成本的)。

在您的代码中,当前线程并不表示其他线程可以运行,除非在同步块中它仍然持有锁。

当你将睡眠转移到同步块之外时,可以看到这一点:

try{
synchronized (lock) {
System.out.println(Thread.currentThread().getName()+" get,");
}
TimeUnit.SECONDS.sleep(2);
}catch(InterruptedException e) {
e.printStackTrace();
}finally {
} 

在我的案例中,结果是:

AAA获得,BBB得到,CCC获得,DDD得到,CCC获得,AAA获得,DDD得到,BBB得到,DDD得到,AAA获得,BBB得到,CCC获取,

所以线程每次都会更改,因为当前线程表示它想停止(调用睡眠),而其他线程可用。

最后JVM将决定交换线程,即使不调用sleep,例如100次迭代:

for(int i = 0 ; i < 100;i++) {
synchronized (lock) {
System.out.println(Thread.currentThread().getName()+" get,");
} 
} 

就我而言,我确实得到了:

AAA获得,AAA获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,CCC获得,AAA获得,AAA获得,AAA获得,AAA获得,AAA获得,。。。。

和再次:

CCC获取,CCC获得,CCC获得,DDD得到,DDD得到,DDD得到,DDD得到,DDD得到,DDD得到,DDD得到,DDD得到,DDD得到,DDD得到,DDD得到,DDD得到,DDD得到,DDD得到,DDD得到,DDD得到,DDD得到,CCC获得,CCC获得,CCC获得,CCC获得,AAA获得,AAA获得,AAA获得,AAA获得,AAA获得,AAA获得,CCC获得,CCC获得,CCC获得,CCC获得。。。。

因此线程被更改,但时间和顺序根本无法保证。

在深入研究之前需要注意的几件事:

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

  • 您的测试代码没有在此处测试ReentrantLock的行为。实际上,正在使用锁实例作为基元监视器或互斥对象。不能将synchronizedLock对象一起使用。


在运行代码的平台上,最后一个执行的线程似乎得到了首选项。。。。在一定程度上。我得到了与您报告的类似的行为,但当线程循环更长时,我确实看到了切换。相比之下,皮罗看到了不同的行为。(我认为这可能是操作系统差异造成的。)

那么这里发生了什么?实际行为部分由操作系统的本机线程调度程序实现决定,部分由实际发生的事情决定

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

另一件需要考虑的事情是本例中线程的行为。固定监视器的线程执行以下操作:

  1. 它从sleep中唤醒
  2. 它释放监视器
  3. 它取消标记另一个线程
  4. 它试图重新获取监视器

未标记的线程也会尝试获取监视器。

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

为什么?很难说:

  • 可能是线程在调度其他线程之前从unpark系统调用返回
  • 这可能是因为未标记的线程通常会受到TLAB未命中或内存缓存未命中的阻碍
  • 可能是其他事情;例如,我没有想到的事情

但无论如何,这都不太可能是一个深思熟虑的设计决定,即优先考虑一个线程而不是另一个线程。可能就是这样。

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


这是编译器优化吗?

我不这么认为。JIT编译器确实优化了监视器的进入和退出序列,但它只处理监视器未被控制时的"快速路径"进入/退出。在本例中,监控器被争用。

最新更新