共计 1795 个字符,预计需要花费 5 分钟才能阅读完成。
在学习 volatile 关键字之前先了解一下 Java 内存模型和 happen-before 原则。
Java 内存模型
内存模型的特性
线程 1 写:先写入本地内存,在同步到主内存。
线程 2 读:先读本地内存,不能存在或失效在读主内存。
这种内存结构是基于操作系统的逻辑虚拟出来的结构,并不是真实存在的,可以屏蔽各种硬件和操作系统的差异性,实现平台一致性。而且和 jvm 的运行时结构也没关联。主内存存储的是实例字段,静态字段和数据对象线程共享的数据。本地内存可以理解为主内存数据的拷贝。所有的线程只能操作本地内存,线程间的通信需要通过主内存。
原子性
Java 内存模型保证了 read、load、use、assign、store、write、lock 和 unlock 操作具有原子性,例如对一个 int 类型的变量执行 assign 赋值操作,这个操作就是原子性的。但是 Java 内存模型允许虚拟机将没有被 volatile 修饰的 64 位数据(long、double)的读写操作划分为两次 32 位的操作来进行,即 load、store、read 和 write 操作可以不具备原子性。
可见性
可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。Java 内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值来实现可见性的。
有序性
在本线程内观察,所有操作都是有序的。在一个线程观察另一个线程,所有操作都是无序的,无序是因为发生了指令重排序。在 Java 内存模型中,允许编译器和处理器对指令进行重排序,重排序过程不会影响到单线程程序的执行,但是会影响到多线程并发执行的正确性。
happen-before 原则
单一线程原则
在一个线程内,在程序前面的操作先行发生于后面的操作。
单线程内按代码顺序执行。但是,在不影响单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这个是合法的。话句话说,这一规则无法保证编译重排和指令重排。
管程锁定规则
一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。
volatile 变量规则
对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作。
线程启动规则
Thread 对象的 start() 方法调用先行发生于此线程的每一个动作。
线程加入规则
Thread 对象的结束先行发生于 join() 方法返回。
线程中断规则
对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断时间的发生,可以通过 interrupted() 方法检测到是否有中断发生。
对象终结规则
一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize() 方法的开始。
传递性
如果操作 A 先行发生于操作 B,操作 B 先行发生于操作 C,那么操作 A 先行发生于操作 C。
volatile 关键字
volatile 是一个类型修饰符。volatile 的作用是作为指令关键字
volatile 关键字的特性
- 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
- 禁止指令重排序。
- volatile 只能保证对单次读 / 写的原子性。
volatile 关键字的可见性
volatile 的内存可见性是基于内存屏障实现的。
在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM 为了保证在不同的编译器和 CPU 上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障告诉编译器和 CPU:不管什么指令都不能和这条 Memory Barrier 指令重排序。
- 1.lock 前缀指令在多核处理器下会引发两件事情:
1)将当前处理器缓存行的数据写回到系统内存
2)写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效 - 2. 为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2 或其他)后再进行操作,但操作完不知何时会写到内存。
- 3. 为了保证各个处理器的缓存是一致的,实现了缓存一致性协议(MESI),每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作时,会重新从系统内存中把数据读到处理器缓存中。