如果需要保证多线程共享变量的可见性和有序性时,就用volatile来修饰变量。
多线程并发编程中主要围绕三个特征实现,可见性是其中一种。
可见性:
可见性是指多个线程访问同一个共享变量的时候,一个线程修改了这个变量的值,其他的线程立即可以看到修改后的值。
原子性:
原子性指一个操作或者一组操作要么全部执行,要么全部不执行。
有序性:
有序性指程序执行的顺序按照代码的先后顺序执行。
1、状态标志:我们在工程中经常使用的变量标识程序是否启动、初始化完成、是否停止等、
//新消息检查处理类
public class MessageLoopHandler {
//volatile变量作为状态标识
private volatile boolean Shutdown=false;
//线程A执行shutdown方法 Shutdown变量为true
public void Shutdown(){
Shutdown=true;
}
//线程B检查到shutdown为true,结束while循环
public void doWork(){
while (!Shutdown){
//应用没有停止时,函数循环检查是否有新消息
}
}
}
如果不用volatile修饰shutdown变量,A线程调用shutdown方法时,使shutdown变量变为了true,但是线程B可能不知道shutdown变量的变化,从而继续执行dowork方法。
2、懒汉单例模式的双重检测。
public class Singleton {
//单例引用使用volatile修饰
//1、禁止指令重排 2、保证变量可见
private static volatile Singleton singleton;
//构造函数私有化
private Singleton(){ }
public static Singleton getInstance(){
//1.第一次检测
if (singleton==null){
//2.在singleton类对象上加锁
synchronized (Singleton.class){
//3.第二次检测
if (singleton==null){
//4.实例化
singleton=new Singleton();
}
}
}
return singleton;
}
}
使用volatile修饰保证singleton的实例化能够对所有线程立即可见。
(1)为什么使用了volatile修饰了singleton引用还要用synchronized锁?
因为volatile只能保证共享变量singleton的可见性,不能保证其原子性,因为singleton = new Singleton();这个操作不是原子性的,可以分为三步:
步骤1:在堆中申请一块内存空间。
步骤2:初始化申请好的内控空间。
步骤3:将内存空间的地址复制给singleton.
因此,singleton = new Singleton();是一个有三步操作组成的复合操作,多线程环境下,线程A执行了第一步、第二步之后发生线程切换,B线程开始执行第一步、第二步、第三步(因为此时线程A还没完成赋值操作),所以为了保障这个三个步骤的不可中断,可以使用synchronized在这段代码上加锁。
(2)第一次检查singleton为空后,为什么内部还进行第二次检查?
线程A进行判空检查之后,开始执行synchronized代码时发生线程切换(线程切换可能发生在任何时候),B线程也进入判空检查,B线程检查singleton==null结果为true,也开始执行synchronized代码块,虽然synchronized会让两个线程串行执行,如果synchronized代码块内部不进行二次判空检查,singleton可能会初始二次。
(3)volatile除了内存可见性,还有别的作用吗?
volatile修饰的变量除了可见性,还可以防止指令重排序。
指令重排是编译器和处理器为了优化程序执行的性能而对程序进行重排的一种手段。现场就是CPU执行指令的顺序可能和代码中的顺序不一致。singleton = new Singleton();又三步操作组成,如果没有用volatile修饰,就可能发生指令重排,步骤3在步骤2之前执行,singleton引用的是还没有被初始化的内存空间,别的线程调用单例模式就会引发未被初始化的错误。
指令重排遵循的原则:
volatile可以实现内存的可见性和防止指令重排序,但是volatile不保证操作的原子性。volatile通过内存屏障来实现可见性和有序性的。
由于CPU的运算速度远远大于内存的读取速度,因此CPU大部分的计算时间都浪费在等待内存读取数据上,因此在现在计算机系统中通过在CPU和内存之间加一层读写速度界接近于CPU计算速度的高速缓存来做数据缓冲,这样CPU就不用直接从内存中读取速度,可以直接从缓存中读取数据,大大缓解了因内存速度太慢导致CPU饥饿的问题。CPU还有寄存器,一些计算的临时结果将放在寄存器中。
CPU访问主内存数据存在的两个局限性现象?
因为这连个局部现象的存在,使得缓存的存在能够很大程度上缓解CPU的饥饿问题。
CPU从缓冲中读取数据和从内存中读取数据除了速度的差异,本质的区别是什么?
缓存的本质上只要存储20%的常用数据和指令。因为研究人员发现程序80%的时间在运行20%的代码。
CPU缓存的分类?
现代计算机操作系统一般有3级缓存,一级缓存在CPU核心内,二级缓存表示是CPU每个核都有,三级缓存属于共享缓存。
数据如何在主内存、缓存、寄存器中流转的?
以i=i+1为例,当线程执行这条语句的时候,会先从主内存中读取i的值,然后复制一份到缓存中,CPU读取缓存数据(取数指令),进行i+2操作(中间数据放在寄存器),然后把结果写到缓存中。最后将缓存中的数据更新到主内存。
什么是缓存一致性问题?
以i=i+1为例,在单线程环境中,没有任何问题,但是在多线程环境中,就可能出现问题,AB两个线程,在不同的CPU上运行,因为每个CPU都有自己的缓存,A线程从内存中数据放到缓存中,此时B线程也从内存中读取数据,存入自己的缓存,A线程执行加1操作,i变成了1,B线程中缓存的I还是0,B线程也对i进行加1 操作,最后AB线程先后将缓存中的数据写到内存,正确的结果应该是2,但是实际是1 。(造成缓存一致性问题,不是多核CPU缓存不一致导致的,而是线程切换调度造成的)
CPU怎么解决缓存一致性的问题?
早期的CPU通过锁总线的方式解决(总线访问加Lock 锁)。因为CPU都是通过总线来读取主内存中的数据,因此对总线加锁的话,其他CPU访问主内存就会被阻塞,这样就防止了共享变量的竞争。但是锁总线对于CPU的消耗很大。后来就出现了缓存一致协议(如MESI协议),主要规范了CPU读写内存、管理缓存数据的一系列规范。
缓存一致协议(MESI)的思想?
MESI协议和volatile实现的内存可见性是什么关系?
volatile 和MESI 中间差了好几层抽象,中间会经历java编译器,java虚拟机和JIT,操作系统,CPU核心。volatile 是Java 中标识变量可见性的关键字
CPU 有X86(复杂指令集)、ARM(精简指令集)等体系架构,版本类型也有很多种,CPU 可能通过锁总线、MESI 协议实现多核心缓存的一致性。因为有硬件的差异以及编译器和处理器的指令重排优化的存在,所以Java 需要一种协议来规避硬件平台的差异,保障同一段代表在所有平台运行效果一致,这个协议叫做Java 内存模型(Java Memory Model)
java内存模型是什么?
JMM是java定义的一套协议,用来屏蔽各种硬件和操作系统的内存访问差异,让java程序在各个平台都能有一致的运行结果。具体内容指:所有的变量都存在主内存中,每个线程还有自己的工作内存,线程的工作内存保存了该线程需要用到的变量(主内存中的拷贝),线程对变量的操作都必须在工作内存中进行,不能直接读写主内存中的变量,不同的线程不能直接访问对方工作内存中的变量,线程间的通信需要在主内存中完成。
线程的工作内存是在主内存还是在缓存中?
JMM定义的工作内存是抽象的概念,它和实际的CPU内存架构是相互关联的,首先CPU的内部架构没有堆栈之分,这个是Java的JMM划分的,另外线程私有的工作内存可能是CPU寄存器、缓存和主内存。JMM中的主内存也是如此。
JMM内存模型规范?
线程本地内存和物理真实内存之间的关系是相互关联
JMM模型中多线程如何通过共享变量通信的?
线程间通信必须要经过主内存,例如线程AB之间的通信。
主内存和工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步到主内存之间的实现细节,java内存模型定义以下八种操作来完成的(单一操作都是原子的):
JMM内存模型是如何保障内存规范的?
并发编程实际主要围以下三个特征的实现展开的:可见性、有序性、原子性
1、可见性问题:如果对象obj 没有使用volatile 修饰,A 线程在将对象count读取到本地内存,从1修改为2,B 线程也把obj 读取到本地内存,因为A 线程的修改对B 线程不可见,这是从Java 内存模型层面看可见性问题(前面从物理内存结构分析的)。
2、有序性问题:重排序发生的地方有很多,编译器优化、CPU 因为指令流水批处理而重排序、内存因为缓存以及store buffer 而显得乱序执行。如下图所示:
3、原子性问题:例如多线程并发执行 i = i +1。 i 是共享变量,看完Java 内存模型,知道这个操作不是原子的,可以分为+1 操作和赋值操作。因此多线程并发访问时,可能发生线程切换,造成不是预期结果。
可见性 & 有序性 问题解决:
volatile 可以让共享变量实现可见性,同时禁止共享变量的指令重排,保障可见性。
原子性问题解决:
原子性主要通过JUC Atomic***包实现,如下图所示,内部使用CAS 指令实现原子性
volatile的实现原理
volatile可以实现内存的可见性和防止指令重排序。
通过内存屏障技术实现的。
为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障指令,内存屏障效果有:
禁止volatile 修饰变量指令的重排序
写入数据强制刷新到主存
读取数据强制从主存读取
volatile使用总结
volatile 是Java 提供的一种轻量级同步机制,可以保证共享变量的可见性和有序性(禁止指令重排),常用于
状态标志、双重检查的单例等场景。使用原则:
volatile的使用场景不是很多,使用时需要仔细考虑下是否适用volatile,注意满足上面的二个原则。
单个的共享变量的读/写(比如a=1)具有原子性,但是像num++
或者a=b+1;
这种复合操作,volatile无法保证其原子性;