OOP编译器内存模型是否会在线程作用域前提升并缓存字段?
在代码简化与提升操作前,OOP编译器的内联逻辑似乎遵循不同规则。起初我认为,完整的内联加上提升操作会迫使字段在引用进入线程上下文前被提升。
static class Scope { private int field = 1; ScheduledExecutorService e = Executors.newSingleThreadScheduledExecutor(); ScheduledExecutorService e2 = Executors.newSingleThreadScheduledExecutor(); void write() { e.execute( () -> { field = field + 3; Printer.out.print("written..."); e.shutdown(); } ); } void read() { e2.schedule( () -> { Printer.out.print(val() + "...read"); e2.shutdown(); } , 5, TimeUnit.MILLISECONDS ); } int val() { return field; } } public static void main(String[] args) { Printer.setAutoFlush(true); //If you use System you'll get delays, so use a flushable version. Scope scope = new Scope(); scope.read(); scope.write(); }
我对字节码或处理器/汇编指令并不十分熟悉,但可以用伪代码描述编译器可能执行的操作:
在代码完全内联前的某个阶段,read()中的Runnable可能会呈现如下形式:
() -> { Scope self = Scope.this; int cacheRead = self.val(); Printer.out.print(cacheRead + "...read"); e.shutdown(); }
最终Printer类会被展开,read方法本身也会被内联,直到所有内容都作为单条指令序列整合到main()中。此时编译器会开始进行推断以降低线程间延迟,这意味着所有非屏障的读写操作都会被重排序或简化。
比如这段代码:
Scope self = Scope.this; int cacheRead = self.field; //simplified to reference directly
可被简化为:
int cacheRead = Scope.this.field;
这意味着,如此深度的内联会让编译器认为我们执行的是直接整数加载操作。
(顺带一提,.val()也会被展开为字节码,随后转为C语言、汇编指令及处理器指令……在这些转换过程中,代码会经历两次重排序:JIT编译器重排序与处理器重排序……)
问题在于,由于int并非volatile类型,编译器可能推断该整数不属于任何进程间通信内容……因此为了避免延迟,它可能会将该值提升到Runnable实例化之外(即Runnable的作用域内):
new Runnable() { int cacheRead = Scope.this.field; @Override public void run() { Printer.out.print(cacheRead + "...read"); e.shutdown(); } }
这会引发内存可见性问题。不过我认为这种情况可能(或肯定)不会发生,原因如下:
OOP语法规则约束
如果内联操作遵循语言的语法与规则,那么提升操作不会破坏内存可见性……即便尝试进行提升,最终也只会是:
new Runnable() { Scope cachedScope = Scope.this; @Override public void run() { Printer.out.print(cachedScope.field + "...read"); //即便发生提升,仍可能存在延迟 e.shutdown(); } }
这意味着OOP编译器可能不会在执行提升/缓存步骤前进行深度内联……(假设所有编译步骤按内联→简化→消除→提升的顺序执行)
因此,若程序员未显式解引用基本类型,被提升的将是对象引用而非其独立字段。
我了解到,若对同一非volatile字段进行多次检查……即便通过this.访问,基本类型的提升也会在首次读取时发生(这种情况下需要使用opaqueness/memory_order_relaxed)。
Java语言在这方面有明确规定:若lambda捕获局部值,会强制要求该值为final,并在进入lambda前完成解引用;若lambda捕获字段,则无此要求。对我而言,这是区分局部变量与字段的教学工具……明确字段始终会强制所有子作用域共享寄存器读取操作。
当然,Java语言可对其JIT编译器的行为进行控制……但处理器优化呢?
我们明确知道,内存延迟不受任何语言影响……这意味着非volatile字段不会因延迟问题影响其“可见性”……所有处理器均具备缓存一致性,因此这一点无需担忧。
那么,OOP编译器的内存模型是否真的会在线程作用域形成前提升并缓存字段?
根据Java文档,VarHandle.getOpaque()会阻止基本类型的完全内联……但相关示例并未提及对象引用的情况。它不仅会阻止完全内联,还会彻底阻止提升与简化操作。
根据文档,opaqueness类似于“readOnce”或“memory_order_relaxed”……而我们仅需阻止一种更轻微的情况:在Runnable创建前的顶层完全提升操作。
我了解到,若代码中存在双重检查,无论该整数是字段还是局部变量,都会在Runnable内部的第一行被提升(而非在Runnable创建前),这会导致双重检查失效,编译器会将其简化并移除,若确实需要双重检查,则必须在每次读取时使用getOpaque。
但在本示例中仅执行一次读取操作,因此只要内存规则正确应用,就无需使用opaque。
内容的提问来源于stack exchange,提问作者Delark

