系列文章目录
线程安全(一)java对象头分析以及锁状态
线程安全(二)java中的CAS机制
线程安全(三)实现方法sychronized与ReentrantLock(阻塞同步)
线程安全(四)Java内存模型与volatile关键字
线程安全(五)线程状态和线程创建
线程安全(六)线程池
线程安全(七)ThreadLocal和java的四种引用
线程安全(八)Semaphore
线程安全(九)CyclicBarrier
线程安全(十)AQS(AbstractQueuedSynchronizer)
0.前言
想要了解线程安全问题,必须先理解Java内存模型
1.Java内存模型
- JMM:Java Memory Model,屏蔽掉各种硬件和操作系统的内存访问差异。定义了各个变量的访问规则。
- 内存模型规定所有变量都储存在主内存中(Main Memory)。每条线程还有自己的工作内存(Working Memory),线程的工作内存保存了被该线程使用到的变量的主内存的副本拷贝,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写主内存的变量,不同的线程间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。
1.1线程、主内存、工作内存三者的交互关系
来源于《深入理解java虚拟机(第二版)》
2.原子性、可见性、有序性
多核CPU出现的会导致原子性、可见性、有序性的问题
储存器的结构,由上到下:
CPU的寄存器-》高速缓存-》内存-》磁盘
其中高速缓存又有一级二级三级缓存等,L1、L2为核内独享的,L3是在多核CPU之间共享的
指令重排出现场景
- 源代码
- 编译器优化重排序
- 指令集并行重排序
- 内存系统重排序
- 最终执行指令序列
2.1.硬件方面
2.2.JMM层面
附:
- 主内存与工作内存的交互,虚拟机实现保证以下操作是原子性的,不可再分的
- 先行发生(happens-before)原则
3.volatile
3.1.概念
当一个变量定义为volatile之后,它将具备以下两种特性:
- 可见性,即当一个线程修改了这个变量的值,其他线程是可以立刻得知的。区别于普通变量,普通变量的值在线程间传递需要通过主内存来完成。volatile变量在各个线程是一致的,但是,java里的运算并非原子操作,比如自增(i++)等
不符合以下两条规则的都需要通过加锁来完成
1.运算结果并不依赖变量的当前值,或者能够确保只有单一线程修改量的值。(i++ 需要依赖当前值)
2.变量不需要与其他状态变量共同参与不变约束 - 禁止指令重排序优化,普通变量仅仅会保证在改方法的执行过程中所有依赖复制结果的地方都能获取到正确的结果,而不能保证变量复制操作的顺序和程序代码中的顺序一致(As-If-SerialSemantics)
3.2.实现
- java源码(volatile int i)
- ByteCode字节码(ACC_VOLATLIE)
- JVM虚拟机规范(JVM内存屏障)
- Hotspot实现(汇编语音Lock)
- CPU级别(MESI/总线锁)
3.3.源码分析
字节码:ACC_VOLATILE
JVM:cache->is_volatile()
storeload内存屏障
inline void OrderAccess::fence() {
if (os::is_MP()) {//返回是否多处理器,如果是多处理器才有必要增加内存屏障
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
//__asm__ volatile 嵌入汇编指令
//lock 汇编指令,lock指令会锁住操作的缓存行,也就是缓存锁的实现
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else _
_asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
}
}
这里用lock指令作为内存屏障,
然后利用asm volatile(“” ::: “cc,memory”)作为编译器屏障
附:Jvm的四种内存屏障
inlinevoidOrderAccess :: loadload(){ acquire ();}
inlinevoidOrderAccess :: storestore(){ release ();}
inlinevoidOrderAccess :: loadstore(){ acquire ();}
inlinevoidOrderAccess :: storeload(){ fence ();}
避免volatile写与后面可能有的volatile读/写操作重排序,在每个volatile写的后面,或者在每个volatile读的前面插入一个StoreLoad屏障。
StoreStore - volatile写操作 - StoreLoad
LoadLoad - volatile读操作 - LoadStore
intel lock保证有序性
3.3 DCL单例为什需要volatile关键字
先了解对象创建过程
源码 ClassA a = new ClassA();
汇编码
0 new #1 <com/relife/object/RichMan> 申请内存空间(半初始化)
3 dup
4 lconst_1
5 invokespecial #2 <com/relife/object/RichMan.<init>> 调用构造方法
8 astore_1 堆内存和栈内存建立联系
9 return
5和8有可能会发生指令重排,所以有可能在对象未赋值时,被另外的线程访问。