首页 > 解决方案 > Java Concurrency in Practice 示例中的指令可以在编译器优化期间重新排序吗

问题描述

我正在阅读有关主题的书。

在 5.18 中,Brian Goetz 给出了一个半高效的 memoizer 示例,其中包含一个具有 ConcurrentHashMap 类型的非易失性共享变量cache,如下所示:

public class Memoizer3<A, V> implements Computable<A, V> {
    private final Map<A, Future<V>> cache
        = new ConcurrentHashMap<A, Future<V>>();
    private final Computable<A, V> c;

    public Memoizer3(Computable<A, V> c) { this.c = c; }

    public V compute(final A arg) throws InterruptedException {
        Future<V> f = cache.get(arg);
        if (f == null) {
            Callable<V> eval = new Callable<V>() {
                public V call() throws InterruptedException {
                    return c.compute(arg);
                }
            };
            FutureTask<V> ft = new FutureTask<V>(eval);
            f = ft;
            cache.put(arg, ft); // Can it be put at the very beginning of compute?
            ft.run();
        }
        try {
            return f.get();
        } catch (ExecutionException e) {
            throw launderThrowable(e.getCause());
        }
    }
}

问题是我不明白cache.put(arg, ft);编译器可以根据哪些规则重新排序以Future<V> f = cache.get(arg);在 JLS 方面放在前面(缓存变量的重新排序是否可能?)。

在“重新排序”下,我的意思是由于启用了优化,编译器可能会重新排序完整的代码行。

该问题不涉及 CPU 内存重新排序的主题,例如在https://stackoverflow.com/a/66973124中突出显示的主题

编辑:

这个问题的一个原因是编译器在某些情况下使用共享变量破坏不同步的多线程代码片段的能力,另一个原因是这本书的作者 Doug Lea 的一句话:

由于同步、结构排斥或纯粹的机会,线程内的 as-if-serial 属性仅在一次只有一个线程正在操作变量时才有用。当多个线程都在运行读取和写入公共字段的非同步代码时,任意交错、原子性故障、竞争条件和可见性故障可能会导致执行模式使得 as-if-serial 的概念对于任何给定线程。

尽管 JLS 解决了可能发生的一些特定的合法和非法重新排序,但与这些其他问题的交互降低了实际保证,即结果可能反映了几乎任何可能的重新排序的任何可能的交错。因此,尝试推理此类代码的排序属性是没有意义的。

根据http://gee.cs.oswego.edu/dl/cpj/jmm.html

换句话说,不遵循 JLS 关于“之前发生”、锁定或易失性语义的约束可能会导致使用共享变量的非同步代码中的破坏结果。

PS 感谢Peter Cordes对这个主题的评论。

标签: javamultithreadingconcurrencythread-safetymemory-barriers

解决方案


如果指令违反程序的顺序语义,则不能重新排序。

简单示例(假设 a=b=0):

a=1
b=a

所以根据上述程序的顺序语义,唯一允许的结果是a=1, b=1。如果将重新排序 2 条指令,那么我们会得到结果a=1, b=0。但是这个结果违反了顺序语义,因此被禁止

这也被非正式地称为within thread as if serial semantics。因此允许编译器(或 CPU)重新排序指令。但最基本的限制是不允许违反顺序语义的重新排序。

如果允许 JVM 违反程序的顺序语义,我今天将辞去开发人员的工作 :)

就 JMM 而言:由于这 2 条指令之间的程序顺序,在发生前顺序之前排序a=1b=a

请记住,在方法调用方面没有指定 JMM。它表现为普通加载/存储易失性加载/存储、监控锁释放/获取等操作。

[添加]

假设您有以下代码:

int a,b,c,d=0;

void foo(){
   a=1
   b=1
}

void bar(){
  c=1
  d=a
}

void foobar(){
   foo();  
   bar();
}

那么唯一允许的结果是 'a=1,b=1,c=1,d=1'

由于内联,我们可以摆脱函数调用:

void foobar(){
  a=1 //foo
  b=1 //foo
  c=1 //bar
  d=a //bar
}

以下执行保留了顺序语义:

  c=1 //bar
  a=1 //foo
  b=1 //foo
  d=a //bar

由于结果是 'a=1,b=1,c=1,d=1'

但是下面的执行违反了顺序语义。

   d=a //bar
   a=1 //foo
   b=1 //foo
   c=1 //bar

因为我们最终得到 'a=1,b=1,c=1,d=0',其中 d 是 0 而不是 1。

在不违反程序的顺序语义的情况下,可以对来自函数调用的指令进行重新排序。


推荐阅读