Java Concurrency in Practice 中的示例中的指令是否可以在编译器优化期间重新排序
Can instructions in this example from Java Concurrency in Practice be reordered during compiler optimizations
我正在阅读有关主题的书。
在 5.18 中,Brian Goetz 给出了一个具有 ConcurrentHashMap 类型的非易失性共享变量 cache
的半高效 memoizer 示例,如下所示:
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 内存重新排序的主题,该主题已突出显示,例如
编辑:
这个问题的一个原因是编译器在某些情况下使用共享变量破坏不同步的多线程代码片段的能力,另一个原因是引用本书作者 Doug Lea 的话:
The within-thread as-if-serial property is helpful only when only one
thread at a time is manipulating variables, due to synchronization,
structural exclusion, or pure chance. When multiple threads are all
running unsynchronized code that reads and writes common fields, then
arbitrary interleavings, atomicity failures, race conditions, and
visibility failures may result in execution patterns that make the
notion of as-if-serial just about meaningless with respect to any
given thread.
Even though JLS addresses some particular legal and illegal
reorderings that can occur, interactions with these other issues
reduce practical guarantees to saying that the results may reflect
just about any possible interleaving of just about any possible
reordering. So there is no point in trying to reason about the
ordering properties of such code.
每 http://gee.cs.oswego.edu/dl/cpj/jmm.html
换句话说,如果不遵循关于“先于发生”的 JLS 约束,锁或可变语义可能会导致使用共享变量的非同步代码出现损坏的结果。
P.S。感谢 Peter Cordes 对这个主题的评论。
如果指令违反了程序的顺序语义,则无法对其重新排序。
简单例子(假设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 而言:由于这两条指令之间的程序顺序,a=1
在 happens before 顺序中排在 b=a
之前。
请记住,JMM 不是根据方法调用指定的。它表示为 plain loads/stores volatile loads/stores、monitor lock release/acquire 等操作
[加法]
假设您有以下代码:
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.get
、ConcurrentHashMap.putIfAbsent
的一些调查,我可以说理解为什么B。
Goetz 的代码具有这样的结构需要了解 ConcurrentHashmap 的内部结构。
在下面的“重新排序”下,我的意思是由于启用的优化,编译器可能会重新排序完整代码行。
答案没有触及 CPU 内存重新排序的主题,该主题已突出显示,例如
在他之前使用序号 Map 版本的示例中,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
来防止多个线程同时访问 hashmap。由于序号哈希图的访问方法不是原子的,因此可能存在一个线程重写另一个线程生成的数据的不完整部分的情况,从而阻止后者完成其工作。出于同样的原因,阅读方法可能会看到部分构造的数据。
只要 synchronized 强制它实际 运行 一条存储指令,其他线程只需将其提交到本地 L1d 缓存即可看到该值,因为它是连贯的。 (并且内存屏障将稍后阻塞 loads/stores 直到发生这种情况)。
后来,B. Goetz 用 ConcurrentHashMap 替换了 ordinal HashMap,这允许他删除 synchronized 关键字。
https://www.burnison.ca/articles/the-concurrency-of-concurrenthashmap 在这里清楚地解释了为什么 ConcurrentHashMap.get 是第一个:
In comparisons to the previous methods, the get() and containsKey()
methods are fairly mundane. Also unlike the previous methods, both are
entirely lock-free. First, a Segment is retrieved from the segments
array using the applicable high-order bits of the key's hash. The
retrieval is performed using Unsafe.getObjectVolatile(). Next, a
HashEntry of the segment's table array is retrieved using the key's
hash. This retrieval is also performed using Unsafe.getObjectVolatile.
From this head node, the linked list of HashEntry objects is traversed
until the specified key is found (or not found) and the applicable
value is returned.
由于其可变读取语义,ConcurrentHashMap.get 无法在代码中被编译器向下移动。同时,它允许向上移动。
但是,易失性读取可能会与前面的行一起重新排序,因此这些行必须简单明了,并且无需深入的心理分析就可以理解它们对下面代码的影响。
ConcurrentHashMap.putIfAbsent 在其实现中有一个内部锁,因此它既不能在代码中向上移动也不能向下移动。
下面另一位作者“Java Concurrency in Practice”的引述与within thread as if serial semantics
不足以理解使用共享变量的多线程代码片段有关
The within-thread as-if-serial property is helpful only when only one
thread at a time is manipulating variables, due to synchronization,
structural exclusion, or pure chance. When multiple threads are all
running unsynchronized code that reads and writes common fields, then
arbitrary interleavings, atomicity failures, race conditions, and
visibility failures may result in execution patterns that make the
notion of as-if-serial just about meaningless with respect to any
given thread.
Even though JLS addresses some particular legal and illegal
reorderings that can occur, interactions with these other issues
reduce practical guarantees to saying that the results may reflect
just about any possible interleaving of just about any possible
reordering. So there is no point in trying to reason about the
ordering properties of such code.
(C) 道格·李
我正在阅读有关主题的书。
在 5.18 中,Brian Goetz 给出了一个具有 ConcurrentHashMap 类型的非易失性共享变量 cache
的半高效 memoizer 示例,如下所示:
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 内存重新排序的主题,该主题已突出显示,例如
编辑:
这个问题的一个原因是编译器在某些情况下使用共享变量破坏不同步的多线程代码片段的能力,另一个原因是引用本书作者 Doug Lea 的话:
The within-thread as-if-serial property is helpful only when only one thread at a time is manipulating variables, due to synchronization, structural exclusion, or pure chance. When multiple threads are all running unsynchronized code that reads and writes common fields, then arbitrary interleavings, atomicity failures, race conditions, and visibility failures may result in execution patterns that make the notion of as-if-serial just about meaningless with respect to any given thread.
Even though JLS addresses some particular legal and illegal reorderings that can occur, interactions with these other issues reduce practical guarantees to saying that the results may reflect just about any possible interleaving of just about any possible reordering. So there is no point in trying to reason about the ordering properties of such code.
每 http://gee.cs.oswego.edu/dl/cpj/jmm.html
换句话说,如果不遵循关于“先于发生”的 JLS 约束,锁或可变语义可能会导致使用共享变量的非同步代码出现损坏的结果。
P.S。感谢 Peter Cordes 对这个主题的评论。
如果指令违反了程序的顺序语义,则无法对其重新排序。
简单例子(假设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 而言:由于这两条指令之间的程序顺序,a=1
在 happens before 顺序中排在 b=a
之前。
请记住,JMM 不是根据方法调用指定的。它表示为 plain loads/stores volatile loads/stores、monitor lock release/acquire 等操作
[加法]
假设您有以下代码:
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.get
、ConcurrentHashMap.putIfAbsent
的一些调查,我可以说理解为什么B。
Goetz 的代码具有这样的结构需要了解 ConcurrentHashmap 的内部结构。
在下面的“重新排序”下,我的意思是由于启用的优化,编译器可能会重新排序完整代码行。
答案没有触及 CPU 内存重新排序的主题,该主题已突出显示,例如
在他之前使用序号 Map 版本的示例中,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
来防止多个线程同时访问 hashmap。由于序号哈希图的访问方法不是原子的,因此可能存在一个线程重写另一个线程生成的数据的不完整部分的情况,从而阻止后者完成其工作。出于同样的原因,阅读方法可能会看到部分构造的数据。
只要 synchronized 强制它实际 运行 一条存储指令,其他线程只需将其提交到本地 L1d 缓存即可看到该值,因为它是连贯的。 (并且内存屏障将稍后阻塞 loads/stores 直到发生这种情况)。
后来,B. Goetz 用 ConcurrentHashMap 替换了 ordinal HashMap,这允许他删除 synchronized 关键字。
https://www.burnison.ca/articles/the-concurrency-of-concurrenthashmap 在这里清楚地解释了为什么 ConcurrentHashMap.get 是第一个:
In comparisons to the previous methods, the get() and containsKey() methods are fairly mundane. Also unlike the previous methods, both are entirely lock-free. First, a Segment is retrieved from the segments array using the applicable high-order bits of the key's hash. The retrieval is performed using Unsafe.getObjectVolatile(). Next, a HashEntry of the segment's table array is retrieved using the key's hash. This retrieval is also performed using Unsafe.getObjectVolatile. From this head node, the linked list of HashEntry objects is traversed until the specified key is found (or not found) and the applicable value is returned.
由于其可变读取语义,ConcurrentHashMap.get 无法在代码中被编译器向下移动。同时,它允许向上移动。
但是,易失性读取可能会与前面的行一起重新排序,因此这些行必须简单明了,并且无需深入的心理分析就可以理解它们对下面代码的影响。
ConcurrentHashMap.putIfAbsent 在其实现中有一个内部锁,因此它既不能在代码中向上移动也不能向下移动。
下面另一位作者“Java Concurrency in Practice”的引述与within thread as if serial semantics
不足以理解使用共享变量的多线程代码片段有关
The within-thread as-if-serial property is helpful only when only one thread at a time is manipulating variables, due to synchronization, structural exclusion, or pure chance. When multiple threads are all running unsynchronized code that reads and writes common fields, then arbitrary interleavings, atomicity failures, race conditions, and visibility failures may result in execution patterns that make the notion of as-if-serial just about meaningless with respect to any given thread.
Even though JLS addresses some particular legal and illegal reorderings that can occur, interactions with these other issues reduce practical guarantees to saying that the results may reflect just about any possible interleaving of just about any possible reordering. So there is no point in trying to reason about the ordering properties of such code.
(C) 道格·李