Java内存模型:线程间赋值可见性的保障分析及延伸疑问
start()外的线程启动场景分析 先看你给出的这段代码:
public class Test { private int i = 1; public void f() { Runnable runnable = new Runnable() { @Override public void run() { if (i != 2) throw new AssertionError("i != 2"); } }; i = 2; new Thread(runnable).start(); } }
你提到的结论完全正确:这段代码里run()方法中的i必然是2。因为Java内存模型(JMM)明确规定:线程的start()调用先行发生于(happens-before)该线程内的所有操作。主线程里的i=2赋值操作,会被新启动的线程完全可见,不会触发断言错误。
那核心问题来了:如果不用start()这种标准方式触发线程逻辑,赋值操作的可见性还能得到保障吗?答案是几乎都不能,下面分几种常见情况拆解:
直接调用Runnable的run()方法
如果把new Thread(runnable).start()改成runnable.run(),这根本没有启动新线程——run()方法是在主线程里同步执行的,这种情况下自然不存在可见性问题,但这本质上不是多线程场景,和原问题的前提不符。
绕过start()直接调用Thread的run()方法
要是你写了Thread t = new Thread(runnable); t.run();,结果和上面一样:run()是在当前线程执行,没有真正启动新线程,同样不涉及多线程可见性问题,但这是错误的线程使用方式,完全违背了Thread类的设计意图。
使用非标准的线程任务触发机制(比如自定义调度)
如果是通过某种自定义方式让线程执行Runnable逻辑(比如提前初始化好线程池里的线程,之后手动触发它执行任务),这种情况下没有JMM的happens-before规则兜底。主线程的i=2赋值,对执行run()的线程来说完全可能不可见:
- 主线程的赋值可能只存在于自己的工作内存,还没刷回主内存;
- 执行线程可能已经缓存了
i的旧值(1),不会去主内存重新读取。
这时候你大概率会看到AssertionError抛出,因为线程看到的还是i的旧值。
手动保障可见性的替代方案
如果不能依赖start()的happens-before规则,你需要手动添加可见性保障手段:
- 把
i声明为volatile:强制所有线程直接从主内存读写i的值; - 用
synchronized块包裹赋值和读取操作:同步块的进入/退出会触发内存屏障,保证可见性; - 使用
java.util.concurrent包下的原子类(比如AtomicInteger):这类类的操作本身就具备可见性和原子性。
总结一下:只有通过Thread.start()启动新线程的场景,JMM才会自动保障启动前的操作对新线程可见;任何其他非标准的线程任务触发方式,都需要手动添加可见性保障,否则极大概率出现线程安全问题。
内容的提问来源于stack exchange,提问作者rom1v

