【㉿Java并发】对并发熟悉吗?谈谈对volatile的使用及其原理

volatile的作用

通常情况下我们可以通过synchronized关键字来解决可见性、有序性及原子性问题,synchronized是一个比较重量级的操作,对系统的性能有比较大的影响,所以,如果有其他解决方案,我们通常都避免使用synchronized来解决问题。

volatile关键字就是Java中提供的另一种解决可见性和有序性问题的方案。对于原子性,需要强调一点:对volatile变量的单次读/写操作可以保证原子性的,如long和double类型变量,但是并不能保证i++这种操作的原子性,因为本质上i++是读、写两次操作。

volatile的使用

禁止重排序

在并发环境下的单例实现方式,我们通常可以采用双重检查加锁(DCL)的方式来实现。但是我们通常需要在单例实例变量singleton之间加上volatile关键字,这是为什么?要理解这个问题,先要了解对象的构造过程,实例化一个对象其实可以分为三个步骤:

  • 分配内存空间。
  • 初始化对象。
  • 将内存空间的地址赋值给对应的引用。

但是由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:

  • 分配内存空间。
  • 将内存空间的地址赋值给对应的引用。
  • 初始化对象

如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。因此,为了防止这个过程的重排序,我们需要将变量设置为volatile类型的变量。

实现可见性

可见性问题主要指一个线程修改了共享变量值,而另一个线程却看不到。引起可见性问题的主要原因是每个线程拥有自己的一个高速缓存区——线程工作内存。

volatile的原理

可见性实现

线程本身并不直接与主内存进行数据的交互,而是通过线程的工作内存来完成相应的操作。这也是导致线程间数据不可见的本质原因。因此要实现volatile变量的可见性,直接从这方面入手即可。对volatile变量的写操作与普通变量的主要区别有两点:

  • 修改volatile变量时会强制将修改后的值刷新的主内存中。
  • 修改volatile变量后会导致其他线程工作内存中对应的变量值失效。因此,再读取该变量值的时候就需要重新从读取主内存中的值。

有序性实现

happen-before规则通俗讲,就是如果a happen-before b,则a所做的任何操作对b是可见的。(这一点大家务必记住,因为happen-before这个词容易被误解为是时间的前后)。

JSR 133中定义了这些happen-before规则:

  • 同一个线程中的,前面的操作 happen-before 后续的操作。(即单线程内按代码顺序执行。但是,在不影响在单线程环境执行结果的前提下,编译器和处理器可以进行重排序,这是合法的。换句话说,这一是规则无法保证编译重排和指令重排)。
  • 监视器上的解锁操作 happen-before 其后续的加锁操作。(synchronized 规则
  • 对volatile变量的写操作 happen-before 后续的读操作。(volatile 规则
  • 线程的start() 方法 happen-before 该线程所有的后续操作。(线程启动规则
  • 线程所有的操作 happen-before 其他线程在该线程上调用 join 返回成功后的操作。
  • 如果 a happen-before b,b happen-before c,则a happen-before c(传递性)。

内存屏障

为了实现volatile可见性和happen-before的语义。JVM底层是通过一个叫做“内存屏障”的东西来完成。内存屏障,也叫做内存栅栏,是一组处理器指令,用于实现对内存操作的顺序限制。

 

参考:

 

posted @ 2023-03-18 15:42  残城碎梦  阅读(17)  评论(0编辑  收藏  举报