写入volatile字段并读取另一volatile字段:happens-before规则是否有效?
初始疑问
"对一个volatile字段的写入先行发生于后续对该字段或另一个volatile字段的读取"这一表述是否正确?
先行发生规则参考
- 先行发生(Happens-before):两个操作可通过先行发生关系排序,若A先行发生于B,则A对B可见且执行顺序在B之前。
- Volatile核心规则:对一个volatile字段的写入,仅先行发生于后续对该字段的读取。
- Volatile可见性特性:线程写入volatile变量时,会将之前的所有变更同步至主内存,让其他线程能看到所有非volatile变量的最新值。
示例代码验证
示例1:非volatile字段无法保证可见性
这段代码无法保证控制台输出"thread end":
public class VolatileQuestion { static boolean finish = false; public static void main(String[] args) throws Exception { new Thread(() -> { while (!finish); System.out.println("thread end"); }).start(); Thread.sleep(2000); finish = true; System.out.println("main end"); } }
原因:非volatile的finish变量写入后,主线程的修改无法强制同步到主内存,子线程可能一直读取本地缓存的旧值,陷入死循环。
示例2:volatile字段直接保证可见性
将finish声明为volatile后,可保证输出"thread end":
public class VolatileQuestion { static volatile boolean finish = false; public static void main(String[] args) throws Exception { new Thread(() -> { while (!finish); System.out.println("thread end"); }).start(); Thread.sleep(2000); finish = true; System.out.println("main end"); } }
这里volatile的写入-读取规则生效:主线程对finish的写入先行发生于子线程对它的读取,子线程能及时感知到finish的最新值,退出循环。
示例3:同个volatile字段读写传递可见性
未将finish声明为volatile,但借助另一个volatile字段flush的读写,可保证输出"thread end":
public class VolatileQuestion { static boolean finish = false; static volatile boolean flush = false; public static void main(String[] args) throws Exception { new Thread(() -> { while (!finish) { boolean dummy = flush; } System.out.println("thread end"); }).start(); Thread.sleep(2000); finish = true; flush = true; System.out.println("main end"); } }
逻辑链:主线程中finish = true按程序顺序先行发生于flush = true,而flush = true的写入先行发生于子线程对flush的读取(volatile规则),通过先行发生的传递性,finish的修改会被子线程感知。
核心技术问询:不同volatile字段的读写能否传递可见性?
如果主线程写入一个volatile字段,子线程读取另一个volatile字段,是否仍能保证输出"thread end"?对应代码:
public class VolatileQuestion { static boolean finish = false; static volatile boolean flush = false; static volatile boolean refresh = false; public static void main(String[] args) throws Exception { new Thread(() -> { while (!finish) { boolean dummy = refresh; } System.out.println("thread end"); }).start(); Thread.sleep(2000); finish = true; flush = true; System.out.println("main end"); } }
答案
首先明确:初始表述是错误的。Java内存模型中,volatile的先行发生规则仅针对同一个volatile字段,不同volatile字段的读写之间没有直接的先行发生关系。
回到核心代码:主线程写入flush = true,子线程读取refresh,这两个操作完全独立,没有任何规则能将它们的读写关联起来。虽然主线程中finish = true先行发生于flush = true,但flush和refresh是两个无关的volatile变量,无法通过传递性让子线程感知finish的修改。因此,子线程可能一直读取缓存中finish的旧值,无法退出循环,不能保证输出"thread end"。
只有当子线程读取的volatile字段和主线程写入的是同一个时,才能通过先行发生的传递性,让非volatile变量的修改被感知。
内容的提问来源于stack exchange,提问作者dibo

