【问题标题】:Program without synchronization order没有同步顺序的程序
【发布时间】:2021-11-16 11:35:38
【问题描述】:

我正在查看我找到的最简单的示例之一,并开始推理 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 完全按照以下顺序进行读取:它肯定会先读取b,然后再读取a(与写入相反)。

允许阅读线程查看[1, 0](就像b=1 发生在之前 a=1)。我想我也明白为什么:因为在两个动作之间有no synchronization order,因此没有happens-before,根据JLS,这是一场数据竞赛:

当一个程序包含两个冲突的访问,这些访问没有按照发生前的关系排序时,就被称为包含数据竞争。

因此阅读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;
     }
 }

是否仍允许锁粗化?我认为我知道答案,但我也想听听有根据的解释。

【问题讨论】:

  • 锁粗化不考虑其他方法的动作,因为它不能详尽地识别所有的动作。类可以是动态子类化、反射等。简单的答案是锁粗化可能无论如何都会发生。我没有将此评论归类为答案,因为我认为自己在这个领域没有受到足够的“教育”。

标签: java multithreading jvm locking jit


【解决方案1】:

是的,这是允许的。


这里是一个简单的解释。

记住synchronized 会阻止:

  1. 原子执行:
    • 当另一个线程持有相同的锁时,一个线程不能进入synchronized
    • 当线程进入synchronized 块时,它会立即看到之前执行的synchronized 块中所做的一切
  2. 以与每个线程的程序顺序一致的全局顺序执行(您提到的同步顺序)

换句话说,synchronized 块总是以全局顺序原子地执行。 不同的执行在synchronized 块的交错方式上可能会有所不同,但情况总是如此:

  1. threadX() 中的第一个 synchronized 块始终在第二个之前执行
  2. 同样适用于synchronized 阻止来自threadY()

有 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() 中的 threadY() 块彼此相邻:即情况 A、C 和 D。

由于本次优化后没有出现新的可能执行,所以本次优化是合法的。


对于更严格和详细的解释,我建议:

  1. J. Manson's Ph.D. Thesis on JMM 中的“锁定粗化”章节
  2. 按照the answer above 的建议,在 A. Shipilev 的文章中锁定粗化 example

【讨论】:

  • 很好的答案。但必须强调的是,“以全局顺序原子执行”仅适用于同一个对象上的同步。
【解决方案2】:

锁粗化(和重新排序)是允许的,因为同步读取器要么在粗化锁之前或之后进行排序。他们永远无法看到粗化锁被持有时发生了什么,因此无法观察到锁定代码内部的任何重新排序。

有关详细信息,请参阅: https://shipilev.net/blog/2016/close-encounters-of-jmm-kind/#myth-barriers-are-sane

顺便说一句,好问题。前段时间我也在为这个特殊的例子而苦苦挣扎:)

【讨论】:

  • 我完全理解这一点,很明显,当作者拥有锁时,读者无法获得锁。写入(重新排序后)的可感知结果当然是使[1, 0] 不可能的结果。我真的明白了,这里没有问题。
  • 现在,考虑到我的第一个示例,[1, 0] 是可能的,Shipilev 在那篇文章中说:“在模型中允许这种行为是有意的,原因有两个。[...] 其次,这使得有趣且重要的优化。例如,在上面的示例中,我们可以合并背靠背锁”。我没有得到的部分是:“这使”,究竟是什么“这”?他是在谈论允许看到[1, 0] 可以实现这种优化(合并)的事实吗?或者他的意思是 JMM 允许锁合并,即使有障碍的心智模型不允许。
  • 在阅读了他在那里写的大概 10 倍之后,我倾向于说(除了我是个白痴的事实之外)他的真正意思是,在简单地查看锁会产生的障碍时,我们无法合并锁。但是 JMM 完全允许这样做,因此一旦合并发生,将允许重新排序写入,从而为硬件提供更多优化空间。对于读者来说,这种合并的可感知结果仍然保持不变(就像没有合并一样)。花了一段时间,但我想我明白了。
  • “或者他的意思是 JMM 允许锁合并,即使有障碍的心智模型不允许。-”正确。这就是栅栏对 JMM 来说是一个糟糕的心智模型的原因之一。
【解决方案3】:

如果 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]永远不会发生。一个众所周知的例子是对象被证明从未被其他线程看到的场景。

【讨论】:

  • 我真的明白,一旦你和/或读者联合起来,[1, 0] 是不可能的。你的“他们的执行不能重叠”中是否有隐藏的信息,因为那应该很明显,对吧?他们持有相同的锁。
  • “它们的执行不能重叠”的意思是,重新排序不会影响交错。同this answer详细解释。这也意味着一个必须按照同步顺序在另一个之后,因此,建立 happens-before 关系。也许,这很明显,但仍然是您问题的答案。
  • 我明白了,谢谢 Holger。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2018-08-01
  • 1970-01-01
  • 2017-01-21
相关资源
最近更新 更多