volatile
- 作用: 被 volatile 修饰的变量值在变化时,会被所有线程感知到.保证了多线程模型的可见性;
- 应用: 大量应用于 JDK 已有类型如 concurent, atomic 包下;
- 原理:
- 编译层面: 内存屏障防止指令重排序, 保证有序性;
- 执行层面: 被 volatile 修饰的变量在汇编语言执行时,会在语句前缀加上 lock ,从而依托多核 CPU 的缓存一致性协议 MESI ( 修改 , 互斥 , 共享 , 无效) ,当 总线 中的值修改时, 多个 CPU 可以同时立刻嗅探到修改操作( 总线嗅探机制 ), 并在缓存行变化成共享状态前将修改后的值记录回来,从而立刻感知到被 volatile 修改后的值, 保证可见性;
- 失效场景: var++; // 自增操作实际如下
int temp = var; var = var + 1;
- 可用场景: boolean 类型
- 性能对比: 优于 lock,synchronized
指令重排序
- 重排序场景: 编译期重排序, 处理器重排序, 内存系统重排序
- 什么时候不会重排序优化:
- as-if-serial:
- 单线程程序指令间操作数据存在依赖关系,可能会导致重排序,数据间无依赖关系则不会影响执行结果
- happens-before: 开始于 JDK5 ,JSR133内存模型
- 语义串行: 同 as-if-serial;
- 锁: 加锁先于解锁;
- volatile: 编译期内存屏障保证不重排, 执行时 MESI 保证可见;
- 线程启动: start() 先于线程内代码
- 线程中断: 先 interrupt 后被检测到线程中断
- 线程终止: join() 后于线程内代码
- 对象终结: 构造方法先于终结方法 finalize
- 传递性: 语义的串行具有传递性, a 得到 b, b 得到 c, 则 a 必然先于 c;
- as-if-serial:
- 如何避免出现意料之外的指令重排序
- volatile: 保证修改立即可见, 避免更新丢失
- lock加锁: 保证线程安全
- 多线程执行时,若线程间数据有有依赖,需要正确使用 volatile 或 lock 锁保证程序逻辑按照设计初衷执行;
对象半初始化问题
- 对象如何初始化: ①类加载检查②开辟空间(一块未赋值的地址)③赋默认初始值④设置对象头⑤C++实现的 init 方法为对象赋值
- 对象头(基于HotSpot虚拟机):
- ObjectHeader (64位对象头)
- 锁状态
- 无锁
- 偏向锁
- 轻量级锁
- 重量级锁
- GC 标志
- 类元数据指针 - 可知对象从哪个类得来
- 对象的哈希码
- 哪个线程持有锁 - 线程 id
- 锁状态
- InstanceData 实例信息
- PaddingData 对齐补充
- ObjectHeader (64位对象头)
- 对象头(基于HotSpot虚拟机):
- 为何会对象半初始化:
- 指令重排序 - 当前线程可以先返回拿到地址的对象, 而该对象还未被 init 值,即还未真实赋值
- 什么时候重排序 - 超高并发访问同一对象的加载
- 怎么解决:
- 是不是我连对象都不能 new 了? 除非超高并发访问同一对象的初始化时可能会导致半初始化,否则不会
- 懒加载对象双检索初始化单例, 最好给对象添加 volatile 修饰,保证对象的初始化不会被重排;