关于volatile关键字

volatile

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是
立即可见的。
2)禁止进行指令重排序。volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。

知识补充:

1.线程安全的两个方面:执行控制内存可见

执行控制的目的是控制代码执行(顺序)及是否可以并发执行。

内存可见控制的是线程执行结果在内存中对其它线程的可见性。根据Java内存模型的实现,线程在具体执行时,会先拷贝主存数据到线程本地(CPU缓存),操作完成后再把结果从线程本地刷到主存。

2.java内存模型中,多线程对主内存变量的读写情形描述:

在 java 的内存模型中每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问
某一个对象值的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线
程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了。

简单的说,多线程执行过程中都是操作的本地内存中共享变量的副本,只有到线程推退出前,才刷新到主内存中。

volatile变量的可见性

可见性:指的是线程访问变量是否是最新值。

局部变量不存在可见性问题,而共享内存就会有可见性问题,因为本地线程在创建的时候,会从主存中读取一个共享变量的副本,且修改也是修改副本,且并不是立即刷新到主存中去,那么其他线程并不会马上共享变量的修改。

解决共享变量可见性问题,需要用volatile关键字修饰。

可见性的特性总结为以下2点:

  • 对volatile变量的写会立即刷新到主存
  • 对volatile变量的读会读主存中的新值

为了能更深刻的理解volatile的语义,我们来看下面的时序图,回答这2个问题:

image-20190318151824712

问题1:t2时刻,如果线程A读取running变量,会读取到false,还是等待线程B执行完呢?

答案是会读到false,不会等待线程B执行完,因为volatile并没有锁的特性。

问题2:t4时刻,线程A是否一定能读取到线程B修改后的最新值

答案是肯定的,线程A会从重新从主存中读取running的最新值。

volatile变量的原子性

volatile变量的原子性指的是写操作,这里的原子性的特别总结为2点:

  • 对一个volatile变量的写操作,只有所有步骤完成,才能被其它线程读取到。
  • 多个线程对volatile变量的写操作本质上是有先后顺序的。也就是说并发写没有问题。

为了区分volatile变量和非volatile变量的区别,我们来举个例子:

//线程1初始化User
User user;
user = new User();
//线程2读取user
if(user!=null){
user.getName();
}

在多线程并发环境下,线程2读取到的user可能未初始化完成,具体来看User user = new User的语义:

  1. 分配对象的内存空间
  2. 初始化对象
  3. 设置user指向刚分配的内存地址

步骤2和步骤3可能会被重排序,流程变为

1->3->2

这样线程1在执行完第3步而还没来得及执行完第2步的时候,如果内存刷新到了主存,那么线程2将得到一个未初始化完成的对象。因此如果将user声明为volatile的,那么步骤2,3将不会被重排序。

下面我们来看一个具体案例,一个基于双重检查的懒加载的单例模式实现:

image-20190318152827001

这个单例模式看起来很完美,如果instance为空,则加锁,只有一个线程进入同步块完成对象的初始化,然后instance不为空,那么后续的所有线程获取instance都不用加锁,从而提升了性能。

但是我们刚才讲了对象赋值操作步骤可能会存在重排序,即当前线程的步骤4执行到一半,其它线程如果进来执行到步骤1,instance已经不为null(执行完了步骤3,没执行步骤2),因此将会读取到一个没有初始化完成的对象。

但如果将instance用volatile来修饰,就完全不一样了,对instance的写入操作将会变成一个原子操作,没有初始化完,就不会被刷新到主存中。

修改后的单例模式代码如下:

image-20190318153600790

#对volatile理解的误区

很多人会认为对volatile变量的所有操作都是原子性的,比如自增i++

其实是不对的。

i++操作分为3个步骤

  1. 读取i=0
  2. 计算i+1=1
  3. 重新赋值给i=1

那么可能存在2个线程同时读取到i=0,并计算出结果i=1然后赋值给i,那么其中一个i的值就会被覆盖,得不到预期结果i=2。

分析:

==volatile变量只能保证可见性和禁止指令重排,并不能保证操作的原子性**。

volatile和synchronized的区别

synchronized关键字和volatile关键字比较

  • volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些
  • 多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
  • volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
  • volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。
  • volatile标记的变量不会被编译器优化(禁止指令重排序优化,即执行顺序与程序顺序一致);
    synchronized标记的变量可以被编译器优化

   转载规则


《关于volatile关键字》 xuxinghua 采用 知识共享署名 4.0 国际许可协议 进行许可。
  目录
I I