为何修改后的SafePoint代码长期运行未抛出AssertionError?
嘿,这个问题挺有意思的,咱们一步步拆解来看,先搞清楚核心矛盾,再解释为什么你的测试没触发异常,最后聊聊这个修改后的类到底安不安全。
首先,先看修改后的SafePoint的本质
你把set改成私有,只在构造器里调用——这意味着每个SafePoint实例一旦创建完成,x和y就再也不会被修改了,属于「有效不可变(effectively immutable)」对象。但注意:它不是严格意义上的线程安全不可变类,因为x和y不是final,而且assertSanity方法直接读字段没加锁。
为什么你的测试代码没抛出异常?
你的测试场景有两个关键细节,刚好避开了会触发异常的情况:
1. 你创建的SafePoint实例x和y永远相等
你在循环里创建实例时用的是new SafePoint(finalI, finalI)——也就是说,每个实例的x和y从一开始就是同一个值。哪怕因为Java内存模型的构造器重排序,其他线程看到的是未初始化的默认值0,0 == 0也不会触发assertSanity里的异常。
要是你把创建代码改成new SafePoint(finalI, finalI + 1)(让x和y初始就不等),大概率会看到AssertionError——这时候线程可能会看到部分初始化的状态(比如x=23,y=0),这时候x != y就会触发异常。
2. get方法的synchronized保证了读取的正确性
get方法加了synchronized,根据Java内存模型的规则:
- 进入同步块时,线程会把主内存的最新字段值刷新到自己的工作内存,所以
get返回的x和y一定是该实例最终的正确值(也就是你创建时传入的相等值)。 - 所以后面的
if (xy[0] != xy[1])永远不会成立,自然不会抛出异常。
额外加分项:JIT优化帮了“倒忙”
JVM的即时编译器(JIT)可能会识别出SafePoint实例是有效不可变的(字段不会被修改),于是对assertSanity的读取做了优化——比如直接读主内存的最新值,或者把字段读取缓存成常量,这进一步降低了看到不一致状态的概率。
修改后的SafePoint到底线程安全吗?
结论:不是线程安全的,只是你的测试场景刚好没触发不安全的情况:
- 如果有多个线程同时访问同一个SafePoint实例的
assertSanity,可能会看到不一致的字段值(比如构造器重排序导致的部分初始化状态)——只是你的测试用例里创建的实例x和y始终相等,只有当看到x≠y的部分初始化状态才会抛异常,而你没这么写。 - 另外,静态变量
sp没有声明为volatile,线程对sp的赋值和读取之间没有happens-before关系,线程可能会看到sp的旧值,但这在你的测试里也不会导致异常,因为旧实例的x和y也是相等的。
如果要让修改后的SafePoint真正线程安全,有两个简单的修复方案:
- 把
x和y改成final字段,利用Java的初始化安全性,保证所有线程都能看到正确的字段值; - 给
assertSanity方法加上synchronized修饰,和get用同一把锁,保证字段读取的可见性。
内容的提问来源于stack exchange,提问作者katiex7

