谈谈对Java中的volatile的理解

前言

volatile相关的知识其实自己一直都是有掌握的,能大概讲出一些知识,例如:它可以保证可见性;禁止指令重排。这两个特性张口就来,但要再往深了问,具体是如何实现这两个特性的,以及在什么场景下使用volatile,为什么不直接用synchronized这种深入和扩展相关的问题,就回答的不好了。因为volatile是面试必问的知识,所以这次准备把这部分知识也给啃掉。

系统处理效率与Java内存模型

在计算机中,每条程序指令都是在CPU中执行的,而CPU执行指令的数据都是临时存储在内存中的,但是CPU的执行速度远超内存的读取速度,如果所有的CPU指令都是通过内存来读取数据的话那么将大大的降低了系统的处理效率,所以现代计算机系统都不得不加入一层或多层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之间的缓冲。

将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后,在从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了。

虽然说增加了高速缓存提高了CPU的处理效率,但是也带来了新的问题 :

现代计算机都是多核CPU,一开始,内存中的变量A的值是1,第一个CPU读取了数据,第二个CPU也将数据读取到了自己的高速缓存当中,当第一个CPU对变量A进行加1操作时,变量A的值变成了2,然后将将变量A的值写回内存中,这时第二个CPU也对变量A进行加1操作时,由于第二个CPU中高速缓存中的值还是1,所以加1操作后的结果为2,然后第二个CPU又将变量A的值同步回内存中,这样就导致执行了两次加1操作后,变量A的值最终是2,而不是3。
这种被多个CPU访问的变量,通常称为共享变量。
而产生的上面的问题,就是引入了高速缓存后的,主内存和缓存内容不一致的问题。
因为每个处理器有自己的高速缓存,但是它们又共享同一块主内存,所以必然会出现主内存不知该以哪个高速缓存中的变量为准的情况。

上面这个缓存不一致的问题,我们先记下来,继续来看Java内存模型,其实Java内存模型描述的上面讲的计算机系统高速缓存和内存之间的关系类似。

Java内存模型描述了,各种变量的访问规则,以及将变量存储到内存和从内存读取变量的这种底层细节。

在Java内存模型中关注的变量都是共享变量(实例变量、类变量)。
所有的共享变量都是存储在主内存中的,但是每个线程在访问变量的时候也都会在自己的工作内存(处理器高速缓存)中保留一份共享变量的副本。

Java内存模型(Java Memory Model,简称JMM)规定:

线程对变量的所有操作(读,写)都必须在工作内存中进行,不能直接操作主内存中的数据。
不同线程之间 也不能直接访问对方工作内存中的变量,线程间的变量值传递必须通过主内存进行中转传递。
在JMM中工作内存和主内存的关系如下图:

Volatile的可见性(保证立即可见)

继续我们上面的缓存一致性的问题,这个问题,在Java内存模型中,就是可见性的问题,即一个线程修改了共享变量的值,对另一个线程来说是不是立即可见的。如果不是立即可见的,那么就会出现缓存一致性的问题,如果是立即可见的,那么另一个线程在进行操作的时候,拿到的变量值就是最新的。就可以解决可见性的问题。

那么怎么解决可见性问题呢?

  • 方案一:加锁

将共享变量加锁,无论是synchronized还是Lock都可以,加锁达到的目的是在同一时间内只能有一个线程能对共享变量进行操作,就是说,共享变量从读取到工作内存到更新值后,同步回主内存的过程中,其他线程是操作不了这个变量的。这样自然就解决了可见性的问题了,但是这样的效率比较低,操作不了共享变量的线程就只能阻塞。

  • 方案二:volatile修饰修饰共享变量

当一个共享变量被volatile修饰后,会保证每个线程将变量修改后的值立即同步回主内存中,当其他线程有需要读取变量时会读取到最新的变量值。

那么volatile做了些什么操作就能解决可见性的问题呢?

被volatile修饰的变量,在被线程操作时,会有这样的机制:

就是线程对变量操作时会从主内存中读取到自己的工作内存中,当线程对变量进行了修改后,那么其他已经读取了此变量的线程中的变量副本就会失效,这样其他线程在使用变量的时候,发现已经失效,那么就会去主内存中重新获取,这样获取到的就只最新的值了。

那么volatile这个关键字是如何实现这套机制的呢?

因为一台计算机有多台CPU,同一个变量,在多个CPU中缓存的值有可能不一样,那么以谁缓存的值为准呢?

既然大家都有自己的值,那么各个CPU间就产生了一种协议,来保证按照一定的规律为准,来确定共享变量的准确值,这样各个CPU在读写共享变量时都按照协议来操作。

这就是缓存一致性协议。

最著名的缓存一致性协议就是Intel的MESI了,说MESI时,先解释一下,缓存行:

缓存行(cache line):CPU高速缓存的中可以分配的最小存储单位,高速缓存中的变量都是存在缓存行中的。

MESI的核心思想就是,当CPU对变量进行写操作时发现,变量是共享变量,那么就会通知其他CPU中将该变量的缓存行设置为无效状态。当其他CPU在操作变量时发现此变量在的缓存行已经无效,那么就会去主内存中重新读取最新的变量。

那么其他CPU是如何发现变量被修改了的呢?

因为CPU和其他部件的进行通信是通过总线来进行的,所以每个CPU通过嗅探总线上的传播数据,来检查自己缓存的值是不是过期了,当处理器发现自己换成行对应的内存地址被修改后,就会将自己工作内存中的缓存行设置成无须状态,当CPU对此变量进行修改时会重新从系统主内存中读取变量。

Volatile的有序性(禁止指令重排)

一般来说,我们写程序的时候,都是要把先代码从上往下写,默认的认为程序是自顶向下顺序执行的,但是CPU为了提高效率,在保证最终结果准确的情况下,是会对指令进行重新排序的。就是说写在前的代码不一定先执行,在后面的也不一定晚执行。

举个例子:

int a = 5; // 代码1
int b = 8; // 代码2
a = a + 4;	// 代码3
int c = a + b;	// 代码4

上面四行代码的执行顺序有可能是

JMM在是允许指令重排序的,在保证最后结果正确的情况下,处理器可以尽情的发挥,提高执行效率。

当多个线程执行代码的时候重排序的情况就更为突出了,各个CPU为了提高自己的效率,有可能会产生竞争情况,这样就有可能导致最终执行的正确性。

所以为了保证在多个线程下最终执行的正确性,将变量用volatile进行修饰,这样就会达到禁止指令重排序的效果(其实也可以通过加锁,还有一些其他已知规则来实现禁止指令重排序,但是我们这里只讨论volatile的实现方式)。

那么volatile是如何实现指令重排序的呢?

答案是:内存屏障

内存屏障是一组CPU指令,用于实现对内存操作的顺序限制。
Java编译器,会在生成指令系列时,在适当的位置会插入内存屏障来禁止处理器对指令的重新排序。

volatile会在变量写操作的前后加入两个内存屏障,来保证前面的写指令和后面的读指令是有序的。

volatile在变量的读操作后面插入两个指令,禁止后面的读指令和写指令重排序。

有序性,不仅只有volatile能保证,其他的实现方式也能保证,但是如果每一种实现方式都要了解那对于开发人员来说就比较困难了。

所以从JDK5就出现了happen-before原则,也叫先行发生原则。
先行发生原则总结起来就是:如果一个操作A的产生的影响能被另一个操作B观察到,那么可以说,这个操作A先行发生与操作B。

这里所说的影响包括内存中的变量的修改,调用了方法,发送量消息等。

volatile中的先行发生原则是,对一个volatile变量的写操作,先行发生于后面任何地方对这个变量的读操作。

Volatile无法保证原子性

原子性,是指一个操作过程要么都成功,要么都失败,是一个独立的完整的。

就像上面说的,如果多个线程对一个变量进行累加,那么肯定得不到想要的结果,因为累加就不是一个原子操作。

要保证累加最终结果正确,要么对累加变量加锁,要么就用AotomicInteger这样的变量。

/**
 * 双重检查加锁式单例
 */
public class DoubleCheckLockSingleton implements Serializable{

  /**
   * 静态变量,用来存放实例。
   */
  private volatile static DoubleCheckLockSingleton doubleCheckLockSingleton = null;

  /**
   * 私有化构造方法,禁止外部创建实例。
   */
  private DoubleCheckLockSingleton(){}

  /**
   * 双重检查加锁的方式保证线程安全又能获得到唯一实例
   * @return
   */
  public static DoubleCheckLockSingleton getInstance(){
    //第一次检查实例是否已经存在,不存在则进入代码块
    if(null == doubleCheckLockSingleton){
      synchronized (DoubleCheckLockSingleton.class){
        //第二次检查
        if(null==doubleCheckLockSingleton){
          doubleCheckLockSingleton = new DoubleCheckLockSingleton();
        }
      }
    }

    return doubleCheckLockSingleton;
  }

}

为什么要进行双重检查呢?

当第一个线程走到第一次检查时发现对象为空,然后进入锁,第二次就检查时也为空,那么就去创建对象,但是这个时候又来了一个线程来到了第一次检查,发现为空,但是这个时候因为锁被占用,所以就只能阻塞等待,然后第一个线程创建对象成功了,由于对象是被volatile修饰的能够立即反馈到其他线程上,所以在第一个线程释放锁之后,第二个线程进入了锁,然后进行第二次检查时,发现对象已经被创建了,那么就不在创建对象了。从而保证的单例。

还有就是如果创建对象,步骤:

  • 分配内存空间。
  • 调用构造器,实例化。
  • 返回内存地址给引用。

如果这三个指令顺序被重排了,那么当多线程来获取对象的时候就会造成对象虽然实例化了,但是没有分配内存空间,会有空指针的风险。
所以加上了volatile的对象,也保证了在第二次检查时不会被已经在创建过程中的对象有被检测为空的风险。

总结一下

volatile其实可以看作是轻量级的synchronized,虽然说volatile不能保证原子性,但是如果在多线程下的操作本身就是原子性操作(例如赋值操作),那么使用volatile会由于synchronized

volatile可以适用于,某个标识flag,一旦被修改了就需要被其他线程立即可见的情况。也可以修饰作为触发器的变量,一旦变量被任何一个线程修改了,就去触发执行某个操作。

volatile的变量写操作happen-before,后面任何对此volatile变量的读操作。

到此这篇关于谈谈对Java中的volatile的理解的文章就介绍到这了,更多相关Java中的volatile内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • Java中Volatile关键字详解及代码示例

    一.基本概念 先补充一下概念:Java内存模型中的可见性.原子性和有序性. 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情.为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制. 可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的.也就是一个线程修改的结果.另一个线程马上就能看到.比如:用volatile修饰的变量,就会具有可见性.volatile修饰的

  • Java中的关键字volatile详解

    volatile关键字经常用来修饰变量.不过,volatile本身很容易被误用.本篇就介绍一下volatile的原理和使用方式. 在介绍volatile关键字原理前,我们首先要了解JVM运行时的内存分配逻辑. 对于成员变量i,它存储在堆内存中.每个线程在运行时都会有一个自己的线程栈,线程如果要访问类的成员变量i,会通过引用获取到堆中变量i实际的值10,然后把这个变量值拷贝到自己的栈内存中,作为一个变量副本,之后线程便不再会与堆中的变量有实际联系.每个线程都有一个自己的本地副本,相互隔离.线程访问

  • 详解Java面试官最爱问的volatile关键字

    本文向大家分享的主要内容是Java面试中一个常见的知识点:volatile关键字.本文详细介绍了volatile关键字的方方面面,希望大家在阅读过本文之后,能完美解决volatile关键字的相关问题.  在Java相关的岗位面试中,很多面试官都喜欢考察面试者对Java并发的了解程度,而以volatile关键字作为一个小的切入点,往往可以一问到底,把Java内存模型(JMM),Java并发编程的一些特性都牵扯出来,深入地话还可以考察JVM底层实现以及操作系统的相关知识. 下面我们以一次假想的面试过

  • 详解java如何正确使用volatile

    volatile关键字在java多线程中有着比较重要作用,volatile主要作用是可以保持变量在多线程中是实时可见的,是java中提供的最轻量的同步机制. 可见性 在Java的内存模型中所有的的变量(这里的变量是类全局变量,并不是局部变量,局部变量在方法内并没有线程安全的问题,因为变量随方法调用完成而销毁)都是存放在主内存中的,而每个线程有自己的工作内存,每次线程执行时,会从主内存获取变量的拷贝,对变量的操作都在线程的工作内存中进行,不同线程之间也不能共享工作内存,只能从主内存读取变量的拷贝.

  • Java开发中的volatile你必须要了解一下

    前言 上一篇文章说了 CAS 原理,其中说到了 Atomic* 类,他们实现原子操作的机制就依靠了 volatile 的内存可见性特性.如果还不了解 CAS 和 Atomic*,建议看一下我们说的 CAS 自旋锁是什么 并发的三个特性 首先说我们如果要使用 volatile 了,那肯定是在多线程并发的环境下.我们常说的并发场景下有三个重要特性:原子性.可见性.有序性.只有在满足了这三个特性,才能保证并发程序正确执行,否则就会出现各种各样的问题. 原子性,上篇文章说到的 CAS 和 Atomic*

  • java volatile关键字作用及使用场景详解

    1. volatile关键字的作用:保证了变量的可见性(visibility).被volatile关键字修饰的变量,如果值发生了变更,其他线程立马可见,避免出现脏读的现象.如以下代码片段,isShutDown被置为true后,doWork方法仍有执行.如用volatile修饰isShutDown变量,可避免此问题. public class VolatileTest3 { static class Work { boolean isShutDown = false; void shutdown(

  • Java中Volatile的作用实例解析

    Java 语言中的 volatile 变量可以被看作是一种 "程度较轻的 synchronized":与 synchronized 块相比,volatile 变量所需的编码较少,并且运行时开销也较少,但是它所能实现的功能也仅是 synchronized 的一部分. 锁 锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility). 互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用

  • 谈谈对Java中的volatile的理解

    前言 volatile相关的知识其实自己一直都是有掌握的,能大概讲出一些知识,例如:它可以保证可见性:禁止指令重排.这两个特性张口就来,但要再往深了问,具体是如何实现这两个特性的,以及在什么场景下使用volatile,为什么不直接用synchronized这种深入和扩展相关的问题,就回答的不好了.因为volatile是面试必问的知识,所以这次准备把这部分知识也给啃掉. 系统处理效率与Java内存模型 在计算机中,每条程序指令都是在CPU中执行的,而CPU执行指令的数据都是临时存储在内存中的,但是

  • 深入理解Java中的volatile关键字(总结篇)

    基本概念 -------------------------------------------------------------------------------- 先补充一下概念:Java 内存模型中的可见性.原子性和有序性. 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值,有时甚至是根本不可能的事情.为了确保多个线程之间对内存写入操作的可见性,必须使用同步机制. 可见性,是指线程之间的可见性,一个

  • 老生常谈Java中instanceof关键字的理解

    java 中的instanceof 运算符是用来在运行时指出对象是否是特定类的一个实例.instanceof通过返回一个布尔值来指出,这个对象是否是这个特定类或者是它的子类的一个实例. 用法: result = object instanceof class 参数: Result:布尔类型. Object:必选项.任意对象表达式. Class:必选项.任意已定义的对象类. 说明: 如果 object 是 class 的一个实例,则 instanceof 运算符返回 true.如果 object

  • java中join方法的理解与说明详解

    前言: java 中的 join() 方法在多线程中会涉及到,这个方法最初理解起来可能有点抽象,用一两次大概就懂了.简单说就是当前线程等待调用join方法的线程结束才能继续往下执行. 1. 举个例子 如下, MyRunnable 类是实现 Runnable 接口的多线程类,其run() 方法是一个计算,计算值存储在 result 字段,获取计算结果就必须等线程执行完之后调用 getResult() 获取 public class MyRunnable implements Runnable {

  • Java中ThreadLocal的一些理解

    前言 面试的时候被问到ThreadLocal的相关知识,没有回答好(奶奶的,现在感觉问啥都能被问倒),所以我决定先解决这几次面试中都遇到的高频问题,把这几个硬骨头都能理解的透彻的说出来了,感觉最起码不能总是一轮游. ThreadLocal介绍 ThreadLocal是JDK1.2开始就提供的一个用来存储线程本地变量的类.ThreadLocal中的变量是在每个线程中独立存在的,当多个线程访问ThreadLocal中的变量的时候,其实都是访问的自己当前线程的内存中的变量,从而保证的变量的线程安全.

  • Java中SPI的一些理解

    前言 最近在面试的时候被问到SPI了,没回答上来,主要也是自己的原因,把自己给带沟里去了,因为讲到了类加载器的双亲委派模型,后面就被问到了有哪些是破坏了双亲委派模型的场景,然后我就说到了SPI,JNDI,以及JDK9的模块化都破坏了双亲委派. 然后就被问,那你说说对Java中的SPI的理解吧.然后我就一脸懵逼了,之前只是知道它会破坏双亲委派,也知道是个怎么回事,但是并没有深入了解,那么这次我就好好的来总结一下这个知识吧. 什么是SPI SPI全称Service Provider Interfac

  • java 中的volatile关键字

    目录 1.volatile实现可见性的原理是什么? 2.演示volatile的可见性 1.volatile实现可见性的原理是什么? 有volatile变量修饰的共享变量进行写操作的时候汇编代码会多出一个Lock前缀指令. 在该指令下,多核处理器会引发两件事: 将当前处理器缓存行的数据写回系统内存 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效 这里需要简单了解CPU缓存一致性问题:多核处理器环境下,每个CPU都有自己的缓存行,缓存了内存中的数据,要维护多个CPU中缓存的数据一致性

  • 对Java中传值调用的理解分析

    本文实例分析了Java中的传值调用.分享给大家供大家参考.具体分析如下: Java以引用的方式操作对象实例 可以确认的是Java中操作对象的方式是以引用的方式操作对象.为了更深刻的了解这点我写了如下代码: 首先定义一个自定义类型 复制代码 代码如下: public class Person {            String name;            Person(String name){          this.name = name;      }  } 这里name默认是

  • 深入浅析Java中的volatile

    内存可见性 volatile是Java提供的一种轻量级的同步机制,在并发编程中,它也扮演着比较重要的角色.同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级,相比使用synchronized时引起的线程上下文切换所带来的庞大开销,倘若能恰当的合理的使用volatile,自然是美事一桩. 为了能比较清晰彻底的理解volatile,我们一步一步来分析.首先来看看如下代码 public class TestVolatile { boolean stat

  • 一文精通Java中的volatile关键字

    前言 在一些开源的框架的源码当中时不时都可以看到volatile这个关键字,最近特意学习一下volatile关键字的使用方法. volatile 关键字:当多个线程进行操作共享数据时,可以保证内存中的数据可见. 相较于 synchronized 是一种较为轻量级的同步策略. 缺点: 1. volatile 不具备"互斥性" 2. volatile 不能保证变量的"原子性" 很多资料中是这样介绍volatile关键字的: volatile是轻量级的synchroniz

随机推荐