1. 不用volatile出现的问题
多个线程,如果要公用一个变量,有一个线程专门负责修改这个变量,另外的一个线程专门负责读取这个变量的值,这样会发生一个问题,就是说可能线程已经修改了变量的值,但是读取的那个线程感知不到,还是查看的old值
2. volatile保证可见性
volatile使用嗅探机制保证可见性,让一个线程修改变量值,其他线程要立刻感知到这个值的变化,多线程并发读写机制
3. 主存以及cpu的多级缓存模型
这样做的一个好处就是,cpu可以直接操作自己对应的那个高速缓冲,不需要直接频繁的跟主存通信,这样可以保证cpu计算的效率非常高。
4. 多线程并发运行时可能引发数据不一致问题
一个线程在修改主存的值,但是不会立刻刷新主存,这个时候,另一个线程每次都是从自己 缓冲中读old值,这个时候就会导致一个线程修改的值,别的线程没有办法发现修改,只是读自己缓冲中的值
5. 总线加锁机制和MESI缓存一致性协议的工作原理
解决多线程并发运行时可能引发数据不一致的问题,最早的时候,人家用的是总线加锁的机制,大概的一个意思就是,某个cpu如果要读一个数据,会通过一个总线,对数据加一个总的锁,那么其他cup就没法读或者写数据了,只有当这个cpu修改完了以后,其他cpu可以读到最新的数据
这种总线加锁机制,效率太差了,一旦说多个线程出现了对某个共享变量的访问后,就会导致说,可能串行化的问题,多个cpu多线程并发的时候,效率很差
MESI协议,缓存一致性协议
这个协议可以保证说,上边的那个多线程并发问题没有了,当一个线程改变自己本地缓存的值,然后强制刷回到主存中去,然后还有一个嗅探机制,只要这个线程改变了本地缓存的数据以后,它会发布一个消息,通知出去,其它的线程会去嗅探,嗅探到了自己本地缓冲的数据被别的cpu给修改了后,那么自己缓存中的那个值就会过期掉,然后从主存中取出最新修改过的值。这样就保证了数据的一致性
6. java内存模型以及多线程并发问题发生
java内存模型跟cpu缓存模型是类似的,基于cpu缓存模型来建立的,只不过java的内存模型是标准化的,可以屏蔽掉底层不同计算机的区别
线程的工作内存和主存
read(从主存中读取),load(将主存读取到的值写入工作内存),use(从工作内存读取数据来计算),assign(将计算好的值重新赋值到工作内存中去),store(将工作内存数据写入主存),write(将store过去的变量赋值给主存的变量)
并发编程中的三大特性:可见性、原子性、有序性
可见性:
原子性:对于一个i++的操作,只要是多个线程并发运行来执行这行代码,其实的话,他都是不保证原子性的,如果保证原子性的,第一个线程i++,i = 1;第二个线程,i++,i = 2
有序性:对于代码,同时还有一个问题是指令重排序,编译器和指令器,有的时候为了提高代码执行效率,会将指令重排序,
flag = false;
prepare();
flag = true;
while(!flag){
Thread.sleep(1000);
}
execute();
重排序之后,让flag = true先执行了,会导致线程2直接跳过while等待,执行某段代码,结果prepare()方法还没执行,资源还没准备好呢,此时就会导致代码逻辑出现异常。
7.volatile是如何保证可见性的
如何能够说,加了volatile以后就可以保证多线程的可见性呢?
有一点,只要flag变成了1,然后线程不是要将flag = 1写回工作内存吗?assign操作,此时如果这个flag变量是加了volatile关键字的话,那么此时会这样子,就是说一定会强制保证说assign之后,就立马执行store + write,刷回到主内存里去
保证只要工作内存一旦变为flag = 1,主内存立马变成flag = 1
此外,如果这个变量是加了volatile关键字的话,此时他就会让其他线程的工作内存中的这个flag变量的缓存,会过期
线程2如果再从工作内存里读取flag变量的值,发现他已经过期了,此时就会重新从主内存里来加载这个flag = 1的值
通过volatile关键字,可以实现的一个效果就是说,有一个线程修改了值,其他线程可以立马感知到这个值
8. volatile为什么无法保证原子性
多个工作线程同时将变量加载到工作内存来计算了,这个时候就会出现这种问题
java
9. 基于happens-before原则来看volatile保证有序性
java中有一个happens-before原则:
volatile变量写,再是读,必须保证是先写,再读,会加一个内存屏障
编译器、指令器可能对代码重排序,乱排,要守一定的规则,happens-before原则,只要符合happens-before的原则,那么就不能胡乱重排,如果不符合这些规则的话,那就可以自己排序
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
上面这8条原则的意思很显而易见,就是程序中的代码如果满足这个条件,就一定会按照这个规则来保证指令的顺序。
但是如果没满足上面的规则,那么就可能会出现指令重排,就这个意思。这8条原则是避免说出现乱七八糟扰乱秩序的指令重排,要求是这几个重要的场景下,比如是按照顺序来,但是8条规则之外,可以随意重排指令。
prepare();
volatile flag = true;
while(!flag){
sleep()
}
execute();
比如这个例子,如果用volatile来修饰flag变量,一定可以让prepare()指令在flag = true之前先执行,这就禁止了指令重排。因为volatile要求的是,volatile前面的代码一定不能指令重排到volatile变量操作后面,volatile后面的代码也不能指令重排到volatile前面。
10. volatile的底层实现原理:lock指令以及内存屏障
(1)lock指令
volatile底层,java内存模型、问题、voaltile是如何保证可见性的,缓存一致性协议,但是后面有人家追问了一句,说volatile了以后,具体是发送了什么指令,去实现了什么效果
对volatile修饰的变量,执行写操作的话,JVM会发送一条lock前缀指令给CPU,CPU在计算完之后会立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被别人修改
如果发现别人修改了某个缓存的数据,那么CPU就会将自己本地缓存的数据过期掉,然后这个CPU上执行的线程在读取那个变量的时候,就会从主内存重新加载最新的数据了
lock前缀指令 + MESI缓存一致性协议
(2)内存屏障:禁止重排序
volatille是如何保证有序性的?加了volatile的变量,可以保证前后的一些代码不会被指令重排,这个是如何做到的呢?
指令重排是怎么回事,volatile就不会指令重排
Load1:
int localVar = this.variable
Load2:
int localVar = this.variable2
LoadLoad屏障:Load1;LoadLoad;Load2,确保Load1数据的装载先于Load2后所有装载指令,他的意思,Load1对应的代码和Load2对应的代码,是不能指令重排的
Store1:
this.variable = 1
StoreStore屏障
Store2:
this.variable2 = 2
StoreStore屏障:Store1;StoreStore;Store2,确保Store1的数据一定刷回主存,对其他cpu可见,先于Store2以及后续指令
LoadStore屏障:Load1;LoadStore;Store2,确保Load1指令的数据装载,先于Store2以及后续指令
StoreLoad屏障:Store1;StoreLoad;Load2,确保Store1指令的数据一定刷回主存,对其他cpu可见,先于Load2以及后续指令的数据装载
volatile的作用是什么呢?
volatile variable = 1
this.variable = 2 => store操作
int localVariable = this.variable => load操作
对于volatile修改变量的读写操作,都会加入内存屏障
每个volatile写操作前面,加StoreStore屏障,禁止上面的普通写和他重排;每个volatile写操作后面,加StoreLoad屏障,禁止跟下面的volatile读/写重排
每个volatile读操作后面,加LoadLoad屏障,禁止下面的普通读和voaltile读重排;每个volatile读操作后面,加LoadStore屏障,禁止下面的普通写和volatile读重排
11. double_check单例模式实现缺陷以及volatile优化
private static volatile DoubleCheckSingleton instance;
public DoubleCheckSingleton(){
socket = new Object();
}
public static DoubleCheckSingleton getInstance(){
if (instance == null ){
synchronized (DoubleCheckSingleton.class){
if (instance == null){
instance = new DoubleCheckSingleton();
}
}
}
return instance;
}
12. volatile应用
下边的这种是否运行的标志位就会大量的用到volatile关键字,因为在我们系统关闭的时候,相当于是一个线程在写这个isRunning,然后有其他的线程在读这个isRunning,所以这个时候必须保证可见性
while(isRunning){
}
private volatile Long latestHeartbeatTime = System.currentTimeMillis();
微服务中最近的一次心跳时间,有别的线程在读这个最近一次的心跳时间,然后做一个心跳过期比较,这里就会有可见性的问题