jvm 没有同步顺序的程序

ql3eal8s  于 2022-11-07  发布在  其他
关注(0)|答案(3)|浏览(150)

我正在看一个我找到的最简单的例子,并开始推理SO(同步顺序),或者更准确地说,缺乏同步顺序。

int a, b; // two shared variables 

Thread-X:

 void threadX() {
     synchronized(this) {
         a = 1;
     }
     synchronized(this) {
         b = 1;
     }
 }

以及一个读取器线程Thread-Y

void threadY() {
     int r1 = b;
     int r2 = a;
 }

为了简单起见,我们假设Thread-Y完全按照以下顺序进行读取:它将肯定首先读取x1M3 N1 x,然后读取x1M4 N1 x(与写入相反)。
阅读线程是允许看到[1, 0]的(就像b=1发生在 * a=1之前一样).我想我也明白为什么:因为这两个动作之间没有同步顺序,因此不存在 happens-before,并且根据JLS,这是一个 * 数据竞争 *:
当程序包含两个不按happens-before关系排序的冲突访问时,就称为包含数据争用。
因此,阅读ab是两个活泼的读取,所以看到b=1a=0是允许的并且是可能的。
现在,这反过来又允许JVM在编写器中进行锁粗化,因此它变成:

void threadX() {
     synchronized(this) {
         a = 1;
         b = 1;
     }
 }

我的问题是,如果读者最初是这样写的:

void threadY() {
     synchronized(this) {
         int r1 = b;
     }

     synchronized(this) {
         int r2 = a;
     }
 }

锁粗化仍然被允许吗?我 * 想 * 我知道答案,但我也想听到一个有教养的解释。

mnemlml8

mnemlml81#

是的,允许。
这里有一个简单的解释。
请记住,synchronized块:

  • 自动执行:
  • 一个线程不能进入synchronized块,而另一个线程正在持有同一个锁
  • 当线程进入synchronized块时,它立即看到在先前执行的synchronized块中所做的一切
  • 以与每个线程的程序顺序一致的全局顺序执行(您提到的同步顺序)

换句话说,synchronized块总是以全局顺序原子执行。不同的执行可以在synchronized块的交叉方式上有所不同,但情况总是这样:

  1. threadX()中的第一个synchronized块总是在第二个块之前执行
    1.与threadY()中的synchronized块相同
    有6种可能的交织:
threadX          threadY           threadX          threadY             threadX          threadY       
-------------------------------    -------------------------------      -------------------------------
synchronized { |                   synchronized { |                     synchronized { |               
  a = 1;       |                     a = 1;       |                       a = 1;       |               
}              |                   }              |                     }              |               
synchronized { |                                  | synchronized {                     | synchronized {
  b = 1;       |                                  |   int r1 = b;                      |   int r1 = b; 
}              |                                  | }                                  | }             
               | synchronized {    synchronized { |                                    | synchronized {
               |   int r1 = b;       b = 1;       |                                    |   int r2 = a; 
               | }                 }              |                                    | }             
               | synchronized {                   | synchronized {      synchronized { |               
               |   int r2 = a;                    |   int r2 = a;         b = 1        |               
               | }                                | }                   }              | }             
           (Case A)                           (Case B)                             (Case C)            

threadX          threadY           threadX          threadY             threadX          threadY       
-------------------------------    -------------------------------      -------------------------------
               | synchronized {                   | synchronized {                     | synchronized {
               |   int r1 = b;                    |   int r1 = b;                      |   int r1 = b; 
               | }                                | }                                  | }             
               | synchronized {    synchronized { |                     synchronized { |               
               |   int r2 = a;       a = 1;       |                       a = 1;       |               
               | }                 }              |                     }              |               
synchronized { |                                  | synchronized {      synchronized { |               
  a = 1;       |                                  |   int r2 = a;         b = 1;       |               
}              |                                  | }                   }              |               
synchronized { |                   synchronized { |                                    | synchronized {
  b = 1;       |                     b = 1;       |                                    |   int r2 = a; 
}              |                   }              |                                    | }             
           (Case D)                          (Case E)                             (Case F)

当您合并threadY()中的synchronized块时:

void threadY() {                    void threadY() {          
     synchronized(this) {                synchronized(this) { 
         int r1 = b;                       int r1 = b;        
     }                        =>           int r2 = a;        
     synchronized(this) {                }                    
         int r2 = a;                }                         
     }                                                        
 }

这样,您实际上只将threadY()中的synchronized块彼此相邻的情况保留为允许的情况:即情况A、C和D。
由于在此优化之后没有出现新的可能执行,因此此优化是法律的的。
对于更严格和详细的解释,我建议:

  1. J. Manson's Ph.D. Thesis on JMM中的“锁定粗化”章节
  2. A. Shipilev文章中的锁定粗化示例,如the answer above中所推荐
ruarlubt

ruarlubt2#

锁粗化(和重排序)是允许的,因为同步读取器要么在粗化锁之前排序,要么在粗化锁之后排序。它们永远无法看到在粗化锁被持有时发生了什么,因此无法观察到锁定代码中的任何重排序。
如需详细信息,请参阅:https://shipilev.net/blog/2016/close-encounters-of-jmm-kind/#myth-barriers-are-sane
顺便说一句,这个问题问得好。一段时间前,我也在纠结这个特殊的例子:)

quhf5bfb

quhf5bfb3#

如果JVM能够证明后续的synchronized块将使用相同的对象,那么锁粗化总是可能的。

void threadY() {
     synchronized(this) {
         int r1 = b;
     }

     synchronized(this) {
         int r2 = a;
     }
 }

也可能得到优化

void threadY() {
     synchronized(this) {
         int r1 = b;
         int r2 = a;
     }
 }

但是如果两个方法都是同一个类的示例方法,换句话说,它们的this引用同一个对象,它们的执行就不可能重叠,因此,即使读和/或写被重新排序,结果[1, 0]也是不可能的。
只要执行环境能够确保结果[1, 0]永远不会发生,即使是锁消除也是允许的。一个众所周知的例子是对象被证明永远不会被另一个线程看到的场景。

相关问题