并发编程系列之深入理解volatile关键字
在学习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),每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作时,会重新从系统内存中把数据读到处理器缓存中。
原文地址: https://segmentfault.com/a/1190000020803466
本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
相关文章