18-深入理解volatile原理与使用

我们本节来了解一个新的解决线程安全性问题的一个方案,我们知道,通过synchronized可以完全的来解决线程中所遇到的安全性问题,那么,我们也知道,synchronized属于一个相对来讲比较重量级的锁,虽然在JDK6的时候已经增加了偏向锁以及轻量级锁,但是,在很多情况下,使用它依然会显得比较笨重,那么,我们本节就来了解一个新的解决线程安全性问题的一个方案,叫做volatile关键字,这么说吧,volatile称之为轻量级锁,被volatile修饰的变量在线程之间是可见的。什么意思呢?首先解释可见的,什么是可见的?就是说,一个线程修改了这个变量的值,在另外一个线程中能够读到这个修改后的值,这就是所谓的可见性,那么,我们之前所说的synchronized除了能让线程之间互斥以外,还有一个非常大的功能,就是保证变量的可见性。什么意思呢?可能大家还是不太理解

那么,咱来举一个例子。

我们使用synchronized来做一件事情,

那么,当多线程的情况下,比如说一个线程在setA,另一个线程在getA,的时候,其实很多情况下会出现线程安全性问题,比如说调用了setA()方法,我们本来是想先让它设置a的值,然后在读a的值,当前a的值是1,那么,一个线程先调用setA(),那么,这个线程还没有执行this.a = a;这段代码的时候,那么,另一个线程就已经调用了getA(),那么,这个时候拿到的a的值就是1,这跟我们所希望的,所预期的结果是不一致的,那么,我们想解决这个问题该怎么办呢?就得加synchronized,保证线程的安全。

那么,加了synchronized之后,我们会发现,当一个线程在进行setA的时候,那么,它先拿到了这把对象锁

然后去给a赋值

那么,当另外一个线程在getA的时候,我们知道,方法上的synchronized锁的是当前类的实例,也就是说,此时synchronized锁已经被第一个线程拿到了,第二个线程就需要等待setA方法执行完毕之后,第二个线程才能够执行getA方法,这样也就保证了a在线程之间的可见性。那么,在这里还是给大家写明白吧,一个main方法,两个线程。

保证可见性的前提是,多个线程必须拿的是同一把锁

 

那么,这种情况下,比如说我们在这里休息一会

休眠是个好事,能够帮助我们解决很多线程中正常运行不会出现,而又可能会出现的一些问题。

但是,我们所希望的结果是10。

再来一次

我们发现这两个结果是不一致的,那么,我们想保证这两个结果是一致的,那么,该怎么办呢?我们就只能加锁,

当然,这个例子举得并不是特别的好,有可能setA方法并不一定是先执行的,那么,如果setA方法不是先执行的话,就是说,这个两个线程是同时启动的,我们并不能保证setA方法先执行,如果是getA方法先执行,那么,拿到的结果肯定为1。

这是关于synchronized与可见性问题的一个解释。那么,它和volatile又有什么关系呢?我们发现,我们想实现对a变量让多个线程之间可见,我们使用get和set方法,加了synchronized锁,这种方式是比较麻烦,而且synchronized相对来讲还是比较重量级的锁,那么,非常简单,我们只要在这个变量a上加一个volatile修饰这个a变量,那么,这个a变量就可以对多个线程之间的修改就对多个线程之间可见,

 

在我们之前讲单例模式的时候就已经看到过volatile了,那么,这里我们来详细的说volatile。

我们要保证demo.a = 10;先执行,这样就不好测了,其实demo.a = 10;先执行的话,我们没有必要开一个线程,因为主线程也是一个线程

所以,这样就可以了。

拿到的结果是一致的。这就是volatile保证变量在多个线程之间可见,保证变量的一致性。

volatile的使用我们已经演示完了。那么,我们再来一个例子,volatile到底会用在哪些地方呢?其中有一个非常好用的场景,给大家模拟一下,比如说有两个线程在这里执行一个任务,

比如说,当第一个线程执行完毕之后,才会通知第二个线程来执行,当然了,线程之间的通信我们还没有说,这里我们就使用我们刚才学习的volatile来进行实现,我们这里定义一个boolean型的变量,

两个线程,一个线程执行,执行完毕之后,它会把run的值修改为true,那么,第二个线程就开始执行了,这就是一个关于volatile一个非常常见的一个场景。

这是第一个线程,

当它执行到第10次之后,就把run的值修改为true

那么,这个时候,第二个线程就要开始运行了,也就是说对run变量的修改是可见的,下面

在这里的任务就是先等待第一个线程任务执行完毕之后,它在执行,怎么等待呀?

while中不执行,这里也是一种所谓的自旋。那么,当run的值为true了之后,它就执行了

我们来运行,看一下效果。

发现,当线程一执行完毕之后,线程二就执行了,也就是说

这个地方是生效的,线程一修改了run的值之后,线程二马上就能够知道。

这是关于volatile的一个例子。

下面就来说一下volatile的底层实现。

如果我们了解汇编的话,我们可以把

创建对象实例的时候,我们发现,如果是加了volatile,和不加volatile相比其实在汇编中它多了一个lock指令,所以,也就是说lock指令在生效。那么,这个Lock指令是干什么的呢?Lock指令是在多处理器的系统上,将当前处理器缓存行的数据写回到系统内存,这是第一个,第二个,我们知道有多个处理器,写回到内存的操作会使在其他CPU缓存了该内存地址的数据失效,这也就保证了,就是说,当我们一个线程修改volatile修饰的变量的时候,另一个线程是可见的,

那么,来分析一下这两个实现的意义,第一个是,将当前处理器缓存行的内容写回到系统内存,也就是说什么意思呢?学过操作系统的朋友肯定都知道,我们说,我们的存储的几个等级,最低的从硬盘开始,然后往下是内存,然后是CPU的缓存,CPU在执行计算的过程中,程序运行肯定要把硬盘上的数据加载到内存,就比如说Java虚拟机的类加载,就是把硬盘上的也好,网络上的也好,等等这些字节码文件加载到内存中,也就是说,程序运行的过程中,把所需要的资源首先要加载到内存中,为什么呢?因为内存的读写速度要远远高于硬盘,那么,如果在硬盘中去进行直接的操作,那么,CPU的速度非常的块,而硬盘的速度是跟不上CPU的速度的,所以就会形成性能瓶颈,那么,也就是说,为了提高CPU的利用率,我们先把硬盘中的数据扔到内存中,但是呢,内存的读写速度还是远远低于CPU的速度,所以CPU又加了自己的缓存,那么这个缓存,它的读写速度要远远高于内存的读写速度,因此,为了提高我们程序的性能,CPU会缓存一些内容,也就是说,当我们在进行操作的时候,它会把我们所操作的这个变量缓存到它的缓存里面去,那么,如果我们没有加volatile关键字的变量,那么,它计算完之后,并不会立即的把这个缓存行的内容写回到内存中,缓存行是什么呢?缓存行是CPU缓存的最小单位。它就会存到缓存行里面,它并不会立刻将这个处理完的结果返回到内存里面去,它有可能就是说以后还会用,缓存的目的就是为了提高性能,因此,它并不会立刻写回到内存中去,而是在它的缓存中保留一段时间,那么,保留,我们说有多个处理器,那么,它保留了之后,它操作了,它变了,其他的CPU是不可见的,所以就会出现不可见的问题,那么,加了volatile之后,它就会直接写回到系统内存中。这是第一个。写回到系统内存中还不行,写回到系统内存中,别的CPU中可能也有缓存,所以,就来了第二个,这个写回到内存的操作会使在其他CPU里缓存了该内存地址的数据失效,就是说,它能让其他的CPU缓存的该数据失效,怎么做到的呢?这个其实就是为了保证数据一致性,这是一个数据一致性协议,那么,每个处理器通过嗅探在总线上传播的数据来检查自己的缓存值是否过期,那么也就是说,这个CPU指令在进行执行的时候,那么,被其他的CPU嗅探到之后,就会把它所缓存的这个变量的数据就失效了,失效了是什么意思呢?就是不在缓存中了,不在缓存中了怎么办?再去内存中再去读,那么,这样就保证了数据的一致性,但是我们发现,这个问题还是比较多的,如果,我们大量的使用volatile的话,那么,是不是这个处理器的缓存就失效了,也就是说,会大大的降低性能,也就是说,使用volatie如果是

像我们这种情况的话,简单使用,就是说,在适用场景中使用的话,它能够提高性能,比synchronized解决方案性能要高的多

但是,如果你在一个方法中大量的使用volatile修饰变量,那么,我们就会发现CPU的缓存就用不上了,那么,就是系统性能还是会有大量的降低,另外,我们知道,之前也提到过重排序的问题,通过这个volatile修饰完之后,它会降低大量的CPU的优化,因此,volatile还是对性能影响比较大,也就是说,我们在用的过程中,也要适量的去使用它,而不是说一味地去用,这就是关于volatile。

那么,我们下面来简单的比较一下volatile和synchronized。

我们既然用volatile可以保证数据的可见性了,那么,我们为什么还有synchronized关键字呢?我们为什么还要用synchronized呢?其实这个问题问的是非常不专业的,我们使用volatile修饰变量,只能保证这个变量的可见性,但是,并不能保证对这个变量所操作的原子性,那么,比如说

我们想getA的过程中,对a进行了++

那么,这个过程中,a++,有加,也有赋值,显然a++就不是一个原子性操作,那,既然a++不是一个原子性操作,那么,如果我们使用volatile关键字来修饰的话,volatile是没有任何意义的,所以,volatile保证不了非原子性操作的线程安全性,volatile只能在getA和setA这个过程中保证这个变量对其他线程可见,但是不能保证原子性操作,因此volatile的作用是非常有局限性的,volatile只能够保证被volatile修饰的这个变量的原子性操作在其他的线程中可见,那么,非原子性操作volatile是保证不了的,也就是说,我们可以使用synchronized来完全替代volatile,但是,volatile并不能取代synchronized。Synchronized能取代volatile,为什么还会有volatile呢?也非常简单,因为volatile实现更轻量化,如果我们没有所谓的非原子性操作,仅仅就是get和set的话,那么就可以使用volatile会更简洁一些,这就是关于volatile的一些内容。关于volatile就说到这里。

猜你喜欢

转载自blog.csdn.net/G_66_hero/article/details/85883019
今日推荐