JDK 并发编程<volatile>关键字

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;
//线程1:
prepare();   // 准备资源
flag = true;         
//线程2:
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条规则之外,可以随意重排指令。

//线程1:
prepare();   // 准备资源
volatile flag = true; 

//线程2:
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前缀指令给CPUCPU在计算完之后会立即将这个值写回主内存,同时因为有MESI缓存一致性协议,所以各个CPU都会对总线进行嗅探,自己本地缓存中的数据是否被别人修改

如果发现别人修改了某个缓存的数据,那么CPU就会将自己本地缓存的数据过期掉,然后这个CPU上执行的线程在读取那个变量的时候,就会从主内存重新加载最新的数据了

lock前缀指令 + MESI缓存一致性协议

(2)内存屏障:禁止重排序

volatille是如何保证有序性的?加了volatile的变量,可以保证前后的一些代码不会被指令重排,这个是如何做到的呢?

指令重排是怎么回事,volatile就不会指令重排

Load1int localVar = this.variable

Load2int localVar = this.variable2

LoadLoad屏障:Load1LoadLoadLoad2,确保Load1数据的装载先于Load2后所有装载指令,他的意思,Load1对应的代码和Load2对应的代码,是不能指令重排的

Store1this.variable = 1

StoreStore屏障

Store2this.variable2 = 2

StoreStore屏障:Store1StoreStoreStore2,确保Store1的数据一定刷回主存,对其他cpu可见,先于Store2以及后续指令

LoadStore屏障:Load1LoadStoreStore2,确保Load1指令的数据装载,先于Store2以及后续指令

StoreLoad屏障:Store1StoreLoadLoad2,确保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优化

//如果没有volatile的时候,那么会出现内存可见性的问题
//这里其实主要不是保证可见性的volatile这里是防止指令重排,因为我们下边那个synchronize是可以保证可见性的
//synchronize是可以保证原子性和可见性的
private static volatile DoubleCheckSingleton instance;

public DoubleCheckSingleton(){
    socket = new Object();
}

public static DoubleCheckSingleton getInstance(){
    //第二个线程已经不是null了
    if (instance == null ){
        synchronized (DoubleCheckSingleton.class){
            if (instance == null){
                instance = new DoubleCheckSingleton();
                // 对象初始化的过程其实是分为几个步奏的:初始化一块内存空间,指针指向内存空间,给对象里的变量进行初始化
                // 有可能回出现一种情况,就是指令重排,可能会导致说
                // DoubleCheckSingleton对象里面的socket对象还没在构造函数里初始化,还是null;
                // 但是这个对象对应的内存空间和地址已经分配好了;指针指过去了,此时DoubleCheckSingleton自己
                // 此时还没有释放锁
                // 但是此时DoubleCheckSingleton对象不是null了,但是socket是null
            }
        }
    }
    return instance;
}

12. volatile应用

下边的这种是否运行的标志位就会大量的用到volatile关键字,因为在我们系统关闭的时候,相当于是一个线程在写这个isRunning,然后有其他的线程在读这个isRunning,所以这个时候必须保证可见性
while(isRunning){
}

/**
 * 最近一次心跳时间
 */
private volatile Long latestHeartbeatTime = System.currentTimeMillis();
微服务中最近的一次心跳时间,有别的线程在读这个最近一次的心跳时间,然后做一个心跳过期比较,这里就会有可见性的问题
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值