重排问题根源回顾:
class Data{ int x; }
static Data data;
void create(){
// 风险代码
data = new Data();
data.x = 10;
}
new Data() 底层分 3 步:
- 堆分配内存,x 默认 0
- 执行构造方法初始化对象
- 将对象引用赋值给静态变量
data
CPU / 编译器指令重排后执行顺序:1 → 3 → 2线程 A 刚执行完 data = 新对象引用,还没给 x 赋值 10;此时线程 B 读取 data 不为 null,直接访问 data.x,拿到默认值 0,数据错乱。
四种修复方案,按推荐优先级排序
方案 1:给静态引用 data 加 volatile(最优,最简)
volatile 会在写引用操作前后插入内存屏障,禁止「对象引用赋值」和「对象内部字段赋值」发生重排。
class Data{ int x; }
// 核心修改:static 引用加 volatile
static volatile Data data;
void create(){
data = new Data();
data.x = 10;
}
原理
volatile 写屏障规则:所有在 volatile 写之前的读写操作,不能重排到 volatile 写之后。data.x = 10 属于 volatile 写(data = xxx)前面的普通写,一定会先执行完成,再把对象引用赋值给 data,杜绝半初始化对象溢出。
方案 2:把对象构造 + 赋值封装到局部变量,最后一次性赋值静态变量(无 volatile,纯代码规避)
先完整构造、填充对象,最后再把局部引用赋值给静态变量,中间不存在重排漏洞:
class Data{ int x; }
static Data data;
void create(){
// 全部操作作用于局部变量,局部变量线程私有,不存在多线程可见性问题
Data temp = new Data();
temp.x = 10;
// 最后一步才赋值给静态共享变量,不存在半初始化
data = temp;
}
优点
不需要 volatile,无内存屏障性能损耗;
原理
所有对象初始化、字段赋值都在局部变量完成,最后一步才暴露给多线程共享,重排无法拆分两段逻辑。生产最常用、性能最优方案,强烈推荐。
方案 3:使用 synchronized 同步锁包裹完整创建逻辑
临界区内完整初始化对象,锁的内存屏障阻止内部指令与外部重排:
class Data{ int x; }
static Data data;
void create(){
synchronized (Data.class) {
Data temp = new Data();
temp.x = 10;
data = temp;
}
}
缺点:加锁存在竞争开销,并发量大时性能较差,仅低并发场景使用。
方案 4:构造函数内部完成所有字段赋值,不对外暴露分步赋值
把 x=10 放入构造器,一行完成对象创建,消除分步赋值的重排空间:
class Data{
int x;
// 全参构造,创建时直接赋值
public Data(int x) {
this.x = x;
}
}
static Data data;
void create(){
// 一步完成分配+初始化+引用赋值,无中间可重排步骤
data = new Data(10);
}
适用场景
对象字段固定,可通过构造器一次性完成初始化;如果需要动态多步骤赋值则不适用。
方案对比选型
表格
| 方案 | 性能 | 代码改动 | 适用场景 |
|---|---|---|---|
| 局部变量中转(方案 2) | 最高,无屏障 / 锁 | 极小改动 | 通用所有场景,首选 |
| static volatile(方案 1) | 轻微损耗(内存屏障) | 一行注解 | 代码简洁、对象无法封装构造器 |
| synchronized 锁(方案 3) | 差,有竞争阻塞 | 改动大 | 极低并发、临时兜底 |
| 构造器全量初始化(方案 4) | 最高 | 需要改造实体构造 | 字段固定、可一次性初始化 |
补充拓展:错误修复示范(无效写法)
只给 x 加 volatile 没用!
class Data{
volatile int x; // 错误,解决不了引用重排
}
static Data data;
void create(){
data = new Data();
data.x = 10;
}
原因:重排发生在 data 引用赋值这一步,和字段 x 是否 volatile 无关,线程 B 拿到残缺对象的根源是引用提前发布。
4421

被折叠的 条评论
为什么被折叠?



