在编译器优化期间,实践5.18中Java并发中的指令是否可以重新排序



我正在读这本关于这个主题的书。

在5.18中,Brian Goetz给出了一个半高效存储器的例子,其中非易失性共享变量cache的类型为ConcurrentHashMap,如下所示:

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);进行重新排序,以使其在JLS方面领先于Future<V> f = cache.get(arg);(缓存变量的重新排序可能吗?)。

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

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

编辑:

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

线程内好像串行属性只有在只有一个线程一次由于同步而操纵变量,结构性排斥或纯粹的偶然性。当多个线程全部运行读取和写入公共字段的非同步代码,然后任意穿插、原子性故障、竞赛条件,以及可见性故障可能导致执行模式就任何人而言,似乎连续的概念几乎毫无意义给定线程。

尽管JLS解决了一些特定的合法和非法问题可能发生的重新排序,与这些其他问题的交互将实际保证简化为说结果可能反映几乎任何可能的交织重新排序。因此,试图对这类代码的排序属性。

Perhttp://gee.cs.oswego.edu/dl/cpj/jmm.html

换句话说,不遵循关于";发生在"之前";,锁或易失性语义可能会导致使用共享变量的未同步代码中出现损坏的结果。

附言:感谢彼得·科德斯对这个主题的评论。

如果指令违反程序的顺序语义,则无法对其进行重新排序。

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

a=1
b=a

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

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

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

就JMM而言:由于这两条指令之间的程序顺序,a=1的顺序在b=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。

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

在对ConcurrentHashMap.getConcurrentHashMap.put进行了一些研究之后,我可以说理解了为什么是B。Goetz的代码具有这样的结构,需要了解ConcurrentHashmap的内部结构。

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

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

在他之前使用有序映射版本的例子中,B.Goetz使用了compute:的同步版本

public class Memoizer1<A, V> implements Computable<A, V> {
@GuardedBy("this")
private final Map<A, V> cache = new HashMap<A, V>();
private final Computable<A, V> c;
public Memoizer1(Computable<A, V> c) { this.c = c; }
public synchronized V compute(final A arg) throws InterruptedException {
V result = cache.get(arg);
if (result == null) {
result = c.compute(arg);
cache.put(arg, result);
}
return result;
}
}

这里需要Synchronized来防止多个线程同时访问哈希映射。由于有序哈希映射的访问方法不是原子的,因此可能存在这样的情况:一个线程重写另一个线程生成的数据的不完整部分,从而阻止后者完成其工作。出于同样的原因,读取方法可能会看到部分构建的数据。

只要synchronized强制它实际运行存储指令,那么只需将该值提交到本地L1d缓存,其他线程就会看到该值,因为它是一致的。(在发生这种情况之前,内存屏障将阻止以后的加载/存储)。

后来,B.Goetz用ConcurrentHashMap替换了序数HashMap,这允许他删除synchronized关键字。

https://www.burnison.ca/articles/the-concurrency-of-concurrenthashmap清楚地解释了为什么ConcurrentHashMap.get是这里的第一个:

与前面的方法相比,get()和containsKey()方法相当普通。与以前的方法不同,两者都是完全无锁。首先,从分段中检索分段数组,使用密钥哈希的适用高阶位。这个检索是使用Unsafe.getObjectVolatile()执行的。接下来使用键的搞砸此检索也使用Unsafe.getObjectVolatile执行。从这个头节点,遍历HashEntry对象的链表直到找到(或找不到)指定的密钥并且适用返回值。

由于其不稳定的读取语义,编译器无法在代码中下移ConcurrentHashMap.get。同时,它允许向上移动。

然而,Volatile读取可能会与前几行一起重新排序,因此这些行必须是简单的,并且它们对下面代码的影响应该是可以理解的,而无需深入的心理分析。

ConcurrentHashMap.put具有易失性写入语义,因此不能用上面的操作对其进行重新排序,但可以用下面的操作对它进行重新排序。但是FutureTask.run(ft.run();这里)内部使用全围栏CompareAndSetState(请参阅FutureTask是异步计算和compareAndSwap的方式。一个通用成员(非易失性成员)仍然具有易失性读写的内存语义),因此它生成了全内存屏障,因此cache.put(arg, ft);不能用ft.run();重新排序。

引用另一位作者的";Java并发在实践中";以下内容与within thread as if serial semantics不足以理解使用共享变量的多线程代码片段有关。

线程内好像串行属性只有在只有一个线程一次由于同步而操纵变量,结构性排斥或纯粹的偶然性。当多个线程全部运行读取和写入公共字段的非同步代码,然后任意穿插、原子性故障、竞赛条件,以及可见性故障可能导致执行模式就任何人而言,似乎连续的概念几乎毫无意义给定线程。

尽管JLS解决了一些特定的合法和非法问题可能发生的重新排序,与这些其他问题的交互将实际保证简化为说结果可能反映几乎任何可能的交织重新排序。因此,试图对这类代码的排序属性。

(C)Doug Lea

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

相关内容

最新更新