多线程与并发之Volatile

在开始了解volatile之前,先来看两段代码:

         

第二段代码比第一段代码仅仅多出了一个volatile关键字,却产生了不一样的执行结果,为什么会这样,volatile做了什么?带着种种疑问,进行底层深挖!

一 计算机内存模型以及Java内存模型(JMM)

(1)计算机组成结构

根据上面的现象表明,问题的关键点在flag变量,flag是现象产生的自变量,而hello world是现象产生的因变量。而flag变量,保存在内存中,供程序进行读写操作,那么首先来了解计算机内存模型。

在这里插入图片描述

由于cpu指令速度远大于内存的存取速度,为了解决cpu与内存的速度不匹配,因此在cpu与内存之间,添加高速缓存(cpu速度 > 高速缓存 > 内存 > 磁盘)。如今我们用的计算机,大都是多核cpu,每核cpu有独立的高速缓存,并与其他CPU共享内存。

(2)JMM,线程与进程

根据Java内存模型规定,共享变量存在于主存中,局部变量存在于线程私有内存中,线程对变量的所有操作都必须在线程自己的私有内存中完成,而不能直接读写主存中的变量,线程中变量的值传递需要通过主存来完成。

基于以上描述,猜想:VolatileTest中的main方法和Add中的run方法由两个不同的线程A和B执行,当程序启动时,flag变量存在于主存中,在执行flag = true时,先将flag读到B线程的内存中,然后进行赋值操作,再同步到主存中,而线程A也会读取flag到自己的内存中,用来判断。这里可能是因为线程B执行完赋值操作,同步flag到主存之前,线程A内存中已经存在flag的副本,但是线程A一直没接收到flag修改为true的值。

二 可见性

什么叫可见性?

当一个线程修改了线程共享变量的值,其他线程能够立即得知这个修改的新值,则可见,否则,不可见。

那么根据(一)中的猜想,有什么办法如何能让线程A知道flag的新值呢?

方法一:加锁

当某一线程进入synchrsyonized代码块以后,线程获得锁,清空线程的私有内存,从主存中重新读取共享变量到线程的私有内存中成为副本,当修改完共享变量的值后会刷新到主存中,然后释放锁资源。

修改以上代码:

根据实验结果,synchronized可以解决可见性问题。

方法二:volatile修饰共享变量

根据文章开头的实验结果现象表明,volatile也可以解决可见性问题,但是volatile到底做了什么,解决了可见性问题呢?

被volatile修饰的共享变量,在被A和B线程读取到私有内存后,如果B线程修改了该共享变量并刷新到主存时,那么A线程会使自己私有内存中的变量副本失效,当A线程需要操作该变量时,需重新到主存中读取新的变量值。

比较两种方法: 发现volatile比加锁效率更高,因为加锁需清空线程私有内存的全部变量副本,但是我们操作的可能只是一个变量副本,当线程需要操作其他变量副本时,又得重新到主存读取变量。

三 缓存一致性协议(MESI)

根据(二)中可见性问题,我们知道了volatile修饰过的共享变量,当其中一个线程修改该变量,并刷新到缓存时,如果再其他线程中也存在该变量的副本,则需要通知其他线程让该变量副本失效,重新到缓存中读取。

对于现代计算机系统,在CPU与主存中间往往还有高速缓存,因此上面的变量修改,应当先刷新到高速缓存,然后再刷新到主存,就因为增加了高速缓存使得问题变得更加复杂,那么,其他线程(包括同一个CPU的其他线程和不同CPU的其他线程)是怎么得知该共享变量被修改的呢?

(1)嗅探

根据(一)中的计算机组成结构,cpu与主存之间,靠数据总线交换数据,那么多核CPU中,数据总线上的数据对每个CPU是可见的,那么每个CPU可以嗅探数据总线上的数据来检查自己保存的变量副本值是不是过期,如果CPU发现缓存行对应的主存地址被修改,就会将CPU的缓存行设置为无效,当其他线程需要操作该变量时,则需重新到主存读取变量。

(2)总线风暴

如果volatile修饰的共享变量较多,那么由于volatile的MESI缓存一致性协议,需不停嗅探变量值,势必会导致总线带宽到达峰值,至于volatile和加锁那种更好,需要结合具体的应用实际。

四 有序性

(1)指令重排

为了提高性能,编译器和处理器会对既定的代码进行指令重新排序,一般指令重排分为以下三种级别:

1. 编译器优化重排:编译器可以在不改变单线程程序语义的条件下,可以重新安排语句的执行顺序。

2. 指令并行重排:现代cpu多采用指令并行技术将多条指令叠加执行,生成指令流水(计算机组成原理),如果指令间不存在数据依赖,处理器可以改变语句对应机器指令的执行顺序。

3. 内存系统重排:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

(2)禁止指令重排

 as-if-serial语义:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。 为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被 编译器和处理器重排序。

那么volatile是怎么保证不被重排,达到有序的???

Java编译器优化指令顺序时,在适当的位置会插入内存屏障指令禁止特定类型的处理器重排序。

volatile写:

volatile读:

happens-before:前一个操作的结果可以被后续的操作获取。讲白点就是前面一个操作把变量a赋值为1,那后面一个操作肯定能知道a已经变成了1。

volatile变量规则:就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。

五 原子性

什么叫原子性?

就是一次操作,要么完全成功,要么完全失败。比如i++,就包含三步操作,第一步取i值,第二步i+1,第三步修改i的值。因此多线程对同一个变量进行i++操作没法保证结果正确,其读写过程并不能保证原子性。

如何解决原子性问题?

AtomicInteger或者加锁

六 volatile作用

(1)可见性:volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见,volatile属性不会被线程缓存,始终从主 存中读取。

(2)happens-before保证:volatile提供了happens-before保证,对volatile变量v的写入happens-before所有其他线程后续对v的读操作。

(3)禁止指令重排:volatile用于禁止指令重排序:可以解决单例双重检查对象初始化代码执行乱序问题。

对象实际上创建对象要进过如下几个步骤:

  • 分配内存空间。

  • 调用构造器,初始化实例。

  • 返回地址给引用

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值