【问题标题】:Can the synchronized statements be re-ordered by Java compiler for optimization?Java编译器可以对同步语句重新排序以进行优化吗?
【发布时间】:2013-10-07 01:39:14
【问题描述】:

同步语句是否可以重新排序。 IE : 可以:

synchronized(A) {
   synchronized(B) {
     ......
   }
}

成为:

synchronized(B) { 
    synchronized(A) { 
     ...... 
     }  
}

【问题讨论】:

  • 当然可以,但是如果你想避免死锁,你应该选择一个一致的顺序。
  • 我认为我的问题不清楚。我稍微更新了一下。
  • 编译器不会对语句重新排序,如果这是您所要求的。

标签: java multithreading


【解决方案1】:

同步语句可以重新排序吗?

我假设您是在询问编译器是否可以重新排序 synchronized 块,以便锁定顺序与代码的顺序不同。

答案是否定的。 synchronized 块(和 volatile 字段访问)对编译器施加了排序限制。在您的情况下,您不能在另一个监视器输入之前移动一个监视器输入,也不能在另一个监视器退出之后移动监视器退出。请参阅下面的网格。

引用JSR 133 (Java Memory Model) FAQ:

例如,编译器不可能在获取之前或发布之后移动您的代码。当我们说获取和释放作用于缓存时,我们是在使用速记来表示许多可能的影响。

Doug Lea 的JSR-133 Cookbook 有一个显示重新排序可能性的网格。网格中的空白条目表示允许重新排序。在您的情况下,输入synchronized 块是“MonitorEnter”(与加载volatile 字段相同的重新排序限制),退出synchronized 块是“MonitorExit”(与存储到volatile 字段相同) )。

【讨论】:

    【解决方案2】:

    是和不是。

    顺序必须一致。

    假设您要在两个银行账户之间创建交易,并且始终先获取发送方的锁,然后再获取接收方的锁。问题是 - 假设 Dan 和 Bob 都想同时互相转账。

    线程 1 可能会获取 Dan 的锁,因为它处理 Dan 与 Bob 的交易。
    然后线程 2 获取 Bob 的锁,因为它处理 Bob 与 Dan 的交易。

    然后,砰,死锁。

    道德是:

    1. 少锁。
    2. 阅读Java: Concurrency in Practice。我的例子取自那里。我喜欢和其他人一样争论书籍在编程方面的优点,但是你很少能在两个封面之间全面覆盖一个困难的话题,所以好好享受吧。

    所以这是答案的一部分,我猜你可能一直试图问其他事情,因为我坚信我会表现出通灵的期望。

    JVM 不会以不同于您编程的顺序获取锁。我怎么知道这个?因为否则就无法解决我回答的前半部分的问题。

    【讨论】:

    • 所以你的答案是 Java 编译器不会交换锁定顺序?
    • 全部正确但不回答问题。
    • @djechlin 是的。一切都是真的,但我的问题很简单。这是否可能。
    • @EJP 我编辑了我的答案,在开头包含“是和否”。
    • @Amber 查看编辑。 “有可能”是错误的,如果你相信这一点,你就会犯错。 “不可能”是错误的,如果你相信这一点,你会犯不同的错误。
    【解决方案3】:

    编译器永远不会重新排序同步语句,因为它对最终发生的事情有很大影响。

    同步块用于获得对放置在同步括号之间的特定对象的锁定。

    private final Object LOCK_1 = new Object();    
    
    public void foo(){
        synchronized(LOCK_1){
            //code here...
        }
    }
    

    获取对象 LOCK_1 的锁,并在同步块完成时释放它。由于同步块用于防止并发访问,因此有时可能需要使用多个锁,尤其是在写入/读取多个线程不安全对象时。

    考虑以下使用嵌套同步块的代码:

    private final Object LOCK_1 = new Object();    
    private final Object LOCK_2 = new Object();
    
    public void bar(){
        synchronized(LOCK_1){
            //Point A
            synchronized(LOCK_2){
                //Point B
            }
    
            //Point C
        }
        //Point D
    }
    

    如果我们查看 A、B、C、D 点,我们可以了解为什么同步顺序很重要。

    首先在 A 点,获得 LOCK_1 的锁,因此任何其他试图获得 LOCK_1 的线程都被放入队列中。
    在 B 点,当前执行的线程拥有 两个 LOCK_1 和 LOCK_2 的锁。
    在 C 点,当前正在执行的线程已经释放 LOCK_2 的锁
    在 D 点,当前正在执行的线程已经释放了所有的锁。

    如果我们翻转这个例子并决定将LOCK_2放在外部块上,你会意识到线程获取锁的顺序发生了变化,这对它最终会做什么有很大的影响。通常,当我使用同步块编写程序时,我对每个正在访问的线程不安全资源使用一个 MUTEX 对象(或每组一个 MUTEX)。假设我想使用 LOCK_1 从流中读取并使用 LOCK_2 写入流。认为交换锁定顺序意味着同样的事情是不合逻辑的。

    考虑LOCK_2(写锁)被另一个线程持有。如果我们在外部块上有 LOCK_1,则当前执行的线程至少可以在被放入写入锁队列之前处理所有读取代码(本质上是在 A 点执行代码的能力)。如果我们颠倒锁的顺序,当前正在执行的线程最终将不得不等待写入完成,然后继续读取写入同时保持写入锁(一直到也读)。

    在切换锁的顺序时出现的另一个问题(并非始终如一,有些代码先有 LOCK_1,而另一些代码先有 LOCK_2)。考虑两个线程都急切地尝试执行具有不同锁定顺序的代码。线程 1 在外部块中获取 LOCK_1,线程 2 从外部块中获取 LOCK_2。现在当线程 1 尝试获取 LOCK_2 时,它不能因为线程 2 拥有它。而当线程 2 尝试获取 LOCK_1 时,它也不能,因为线程 1 拥有它。两个线程本质上是永远互相阻塞,形成死锁的情况。

    要回答您的问题,如果您想立即锁定两个对象而不在锁定之间进行任何类型的处理,那么顺序是无关紧要的(在 A 点或 C 点基本上没有处理)。 但是必须在整个程序中保持顺序一致以避免死锁。

    【讨论】:

    • 这很罗嗦,没有回答 IMO 的问题。
    • @Gray 我的最后一句话没有说明问题的答案吗?不会。编译器不会这样做,因为它可能对代码产生影响。
    • 你应该带头,因为这是真正的答案。现在我不能取消投票抱怨。如果您编辑答案以将其移至顶部,我想我可以删除反对票。
    • @Gray 将语句移到顶部。
    猜你喜欢
    • 1970-01-01
    • 2010-12-27
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-09-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多