java - 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对这个主题的评论。
解决方案
如果指令违反程序的顺序语义,则不能重新排序。
简单示例(假设 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=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。
在不违反程序的顺序语义的情况下,可以对来自函数调用的指令进行重新排序。
推荐阅读
- python-3.x - 如何在 Angular 8 中访问响应标头和 Cookie
- spring-boot - 未调用 CommandLineRunner run() 方法
- javascript - Express 路由器:Router.use() 需要一个中间件函数但得到一个对象
- c# - 检索当前在 JSON.NET 中填充的对象
- math - Coq - 向上下文添加选择功能
- php - 在 Linux 上安装软件包 Maatwebsite/Excell 时如何解决问题
- c# - 将 HttpConnection.ChunkedEncodingReadStream 作为 PDF 流式传输到浏览器
- javascript - 如何在集合中每个文档的数组字段中插入 1000 个唯一字符串(URL)?
- javascript - 无法从 User.js 中找到方法
- reactjs - Gatsby:元素类型无效:需要字符串(用于内置组件)或类/函数