java同步之volatile解析

问题

(1)volatile是如何保证可见性的?

(2)volatile是如何禁止重排序的?

(3)volatile的实现原理?

(4)volatile的缺陷?

简介

volatile可以说是Java虚拟机提供的最轻量级的同步机制了,但是它并不容易被正确地理解,以至于很多人不习惯使用它,遇到多线程问题一律使用synchronized或其它锁来解决。

了解volatile的语义对理解多线程的特性具有很重要的意义,所以彤哥专门写了一篇文章来解释volatile的语义到底是什么。

语义一:可见性

前面介绍Java内存模型的时候,我们说过可见性是指当一个线程修改了共享变量的值,其它线程能立即感知到这种变化。

关于Java内存模型的讲解请参考【细谈java同步之JMM(Java Memory Model)】。

而普通变量无法做到立即感知这一点,变量的值在线程之间的传递均需要通过主内存来完成,比如,线程A修改了一个普通变量的值,然后向主内存回写,另外一条线程B只有在线程A的回写完成之后再从主内存中读取变量的值,才能够读取到新变量的值,也就是新变量才能对线程B可见。

在这期间可能会出现不一致的情况,比如:

(1)线程A并不是修改完成后立即回写;

(线路A修改了变量x的值为5,但是还没有回写,线程B从主内存读取到的还旧值0)

(2)线程B还在用着自己工作内存中的值,而并不是立即从主内存读取值;

(线程A回写了变量x的值为5到主内存中,但是线程B还没有读取主内存的值,依旧在使用旧值0在进行运算)

基于以上两种情况,所以,普通变量都无法做到立即感知这一点。

但是,volatile变量可以做到立即感知这一点,也就是volatile可以保证可见性。

java内存模型规定,volatile变量的每次修改都必须立即回写到主内存中,volatile变量的每次使用都必须从主内存刷新最新的值。

volatile的可见性可以通过下面的示例体现:

public class VolatileTest {
 // public static int finished = 0;
 public static volatile int finished = 0;

 private static void checkFinished() {
 while (finished == 0) {
  // do nothing
 }
 System.out.println("finished");
 }

 private static void finish() {
 finished = 1;
 }

 public static void main(String[] args) throws InterruptedException {
 // 起一个线程检测是否结束
 new Thread(() -> checkFinished()).start();

 Thread.sleep(100);

 // 主线程将finished标志置为1
 finish();

 System.out.println("main finished");

 }
}

在上面的代码中,针对finished变量,使用volatile修饰时这个程序可以正常结束,不使用volatile修饰时这个程序永远不会结束。

因为不使用volatile修饰时,checkFinished()所在的线程每次都是读取的它自己工作内存中的变量的值,这个值一直为0,所以一直都不会跳出while循环。

使用volatile修饰时,checkFinished()所在的线程每次都是从主内存中加载最新的值,当finished被主线程修改为1的时候,它会立即感知到,进而会跳出while循环。

语义二:禁止重排序

前面介绍Java内存模型的时候,我们说过Java中的有序性可以概括为一句话:如果在本线程中观察,所有的操作都是有序的;如果在另一个线程中观察,所有的操作都是无序的。

前半句是指线程内表现为串行的语义,后半句是指“指令重排序”现象和“工作内存和主内存同步延迟”现象。

普通变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获得正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致,因为一个线程的方法执行过程中无法感知到这点,这就是“线程内表现为串行的语义”。

比如,下面的代码:

// 两个操作在一个线程
int i = 0;
int j = 1;

上面两句话没有依赖关系,JVM在执行的时候为了充分利用CPU的处理能力,可能会先执行int j = 1;这句,也就是重排序了,但是在线程内是无法感知的。

看似没有什么影响,但是如果是在多线程环境下呢?

我们再看一个例子:

public class VolatileTest3 {
 private static Config config = null;
 private static volatile boolean initialized = false;

 public static void main(String[] args) {
 // 线程1负责初始化配置信息
 new Thread(() -> {
  config = new Config();
  config.name = "config";
  initialized = true;
 }).start();

 // 线程2检测到配置初始化完成后使用配置信息
 new Thread(() -> {
  while (!initialized) {
  LockSupport.parkNanos(TimeUnit.MILLISECONDS.toNanos(100));
  }

  // do sth with config
  String name = config.name;
 }).start();
 }
}

class Config {
 String name;
}

这个例子很简单,线程1负责初始化配置,线程2检测到配置初始化完毕,使用配置来干一些事。

在这个例子中,如果initialized不使用volatile来修饰,可能就会出现重排序,比如在初始化配置之前把initialized的值设置为了true,这样线程2读取到这个值为true了,就去使用配置了,这时候可能就会出现错误。

(此处这个例子只是用于说明重排序,实际运行时很难出现。)

通过这个例子,彤哥相信大家对“如果在本线程内观察,所有操作都是有序的;在另一个线程观察,所有操作都是无序的”有了更深刻的理解。

所以,重排序是站在另一个线程的视角的,因为在本线程中,是无法感知到重排序的影响的。

而volatile变量是禁止重排序的,它能保证程序实际运行是按代码顺序执行的。

实现:内存屏障

上面讲了volatile可以保证可见性和禁止重排序,那么它是怎么实现的呢?

答案就是,内存屏障。

内存屏障有两个作用:

(1)阻止屏障两侧的指令重排序;

(2)强制把写缓冲区/高速缓存中的数据回写到主内存,让缓存中相应的数据失效;

关于“内存屏障”的知识点,各路大神的观点也不完全一致,所以这里也就不展开讲述了,感兴趣的可以看看下面的文章:

(1)Doug Lea的《The JSR-133 Cookbook for Compiler Writers》

http://g.oswego.edu/dl/jmm/cookbook.html

Doug Lea 就是java并发包的作者,大牛!

(2)Martin Thompson的《Memory Barriers/Fences》

https://mechanical-sympathy.blogspot.com/2011/07/memory-barriersfences.html

Martin Thompson 专注于把性能提升到极致,专注于从硬件层面思考问题,比如如何避免伪共享等,大牛!

它的博客地址就是上面这个地址,里面有很多底层的知识,有兴趣的可以去看看。

(3)Dennis Byrne的《Memory Barriers and JVM Concurrency》

https://www.infoq.com/articles/memory_barriers_jvm_concurrency

这是InfoQ英文站上面的一篇文章,我觉得写的挺好的,基本上综合了上面的两种观点,并从汇编层面分析了内存屏障的实现。

目前国内市面上的关于内存屏障的讲解基本不会超过这三篇文章,包括相关书籍中的介绍。

我们还是来看一个例子来理解内存屏障的影响:

public class VolatileTest4 {
 // a不使用volatile修饰
 public static long a = 0;
 // 消除缓存行的影响
 public static long p1, p2, p3, p4, p5, p6, p7;
 // b使用volatile修饰
 public static volatile long b = 0;
 // 消除缓存行的影响
 public static long q1, q2, q3, q4, q5, q6, q7;
 // c不使用volatile修饰
 public static long c = 0;

 public static void main(String[] args) throws InterruptedException {
 new Thread(()->{
  while (a == 0) {
  long x = b;
  }
  System.out.println("a=" + a);
 }).start();

 new Thread(()->{
  while (c == 0) {
  long x = b;
  }
  System.out.println("c=" + c);
 }).start();

 Thread.sleep(100);

 a = 1;
 b = 1;
 c = 1;
 }
}

这段代码中,a和c不使用volatile修饰,b使用volatile修饰,而且我们在a/b、b/c之间各加入7个long字段消除伪共享的影响。

关于伪共享的相关知识,可以查看彤哥之前写的文章【杂谈 什么是伪共享(false sharing)?】。

在a和c的两个线程的while循环中我们获取一下b,你猜怎样?如果把long x = b;这行去掉呢?运行试试吧。

彤哥这里直接说结论了:volatile变量的影响范围不仅仅只包含它自己,它会对其上下的变量值的读写都有影响。

缺陷

上面我们介绍了volatile关键字的两大语义,那么,volatile关键字是不是就是万能的了呢?

当然不是,忘了我们内存模型那章说的一致性包括的三大特性了么?

一致性主要包含三大特性:原子性、可见性、有序性。

volatile关键字可以保证可见性和有序性,那么volatile能保证原子性么?

请看下面的例子:

public class VolatileTest5 {
 public static volatile int counter = 0;

 public static void increment() {
 counter++;
 }

 public static void main(String[] args) throws InterruptedException {
 CountDownLatch countDownLatch = new CountDownLatch(100);
 IntStream.range(0, 100).forEach(i->
  new Thread(()-> {
   IntStream.range(0, 1000).forEach(j->increment());
   countDownLatch.countDown();
  }).start());

 countDownLatch.await();

 System.out.println(counter);
 }
}

这段代码中,我们起了100个线程分别对counter自增1000次,一共应该是增加了100000,但是实际运行结果却永远不会达到100000。

让我们来看看increment()方法的字节码(IDEA下载相关插件可以查看):

0 getstatic #2 <com/coolcoding/code/synchronize/VolatileTest5.counter>
3 iconst_1
4 iadd
5 putstatic #2 <com/coolcoding/code/synchronize/VolatileTest5.counter>
8 return

可以看到counter++被分解成了四条指令:

(1)getstatic,获取counter当前的值并入栈

(2)iconst_1,入栈int类型的值1

(3)iadd,将栈顶的两个值相加

(4)putstatic,将相加的结果写回到counter中

由于counter是volatile修饰的,所以getstatic会从主内存刷新最新的值,putstatic也会把修改的值立即同步到主内存。

但是中间的两步iconst_1和iadd在执行的过程中,可能counter的值已经被修改了,这时并没有重新读取主内存中的最新值,所以volatile在counter++这个场景中并不能保证其原子性。

volatile关键字只能保证可见性和有序性,不能保证原子性,要解决原子性的问题,还是只能通过加锁或使用原子类的方式解决。

进而,我们得出volatile关键字使用的场景:

(1)运算的结果并不依赖于变量的当前值,或者能够确保只有单一的线程修改变量的值;

(2)变量不需要与其他状态变量共同参与不变约束。

说白了,就是volatile本身不保证原子性,那就要增加其它的约束条件来使其所在的场景本身就是原子的。

比如:

private volatile int a = 0;

// 线程A
a = 1;

// 线程B
if (a == 1) {
 // do sth
}

a = 1;这个赋值操作本身就是原子的,所以可以使用volatile来修饰。

总结

(1)volatile关键字可以保证可见性;

(2)volatile关键字可以保证有序性;

(3)volatile关键字不可以保证原子性;

(4)volatile关键字的底层主要是通过内存屏障来实现的;

(5)volatile关键字的使用场景必须是场景本身就是原子的;

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持我们。

(0)

相关推荐

  • 简单了解java volatile

    内存模型基本概念 计算机在执行程序时,每条指令都是在CPU中执行的,而执行指令过程中,势必涉及到数据的读取和写入.由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度.因此在CPU里面就有了高速缓存. 并发编程中的三个概念 1、原子性 即一个操作或者多个操作,要么全部执行并且执行的过程不会被

  • Java面试官最喜欢问的关键字之volatile详解

    前言 笔者去年面试过几家公司,基本上每家公司都会问到volatile,甚至有的公司每轮面试的时候都会问到.面试官这么喜欢问volatile就是因为这个关键字涉及到的知识点较多比如Java内存模型.内存屏障.happen-befor等知识,可以继续挖掘到系统指令.超线程等知识. Java内存模型(JMM) volatile是Java虚拟机提供的最轻量的同步机制,但很难被正确的理解与使用,通过学习Java内存模型对volatile专门定义的一些特殊访问规则,或许会对理解volatile有一定帮助.

  • Java并发编程-volatile可见性详解

    前言 要学习好Java的多线程,就一定得对volatile关键字的作用机制了熟于胸.最近博主看了大量关于volatile的相关博客,对其有了一点初步的理解和认识,下面通过自己的话叙述整理一遍. 有什么用? volatile主要对所修饰的变量提供两个功能 可见性 防止指令重排序 <br>本篇博客主要对volatile可见性进行探讨,以后发表关于指令重排序的博文. 什么是可见性? 把JAVA内存模型(JMM)展示得很详细了,简单概括一下 1.每个Thread有一个属于自己的工作内存(可以理解为每个

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

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

  • 详解java并发编程(2) --Synchronized与Volatile区别

    1 Synchronized 在多线程并发中synchronized一直是元老级别的角色.利用synchronized来实现同步具体有一下三种表现形式: 对于普通的同步方法,锁是当前实例对象. 对于静态同步方法,锁是当前类的class对象. 对于同步方法块,锁是synchronized括号里配置的对象. 当一个代码,方法或者类被synchronized修饰以后.当一个线程试图访问同步代码块的时候,它首先必须得到锁,退出或抛出异常的时候必须释放锁.那么这样做有什么好处呢? 它主要确保多个线程在同一

  • java同步之volatile解析

    问题 (1)volatile是如何保证可见性的? (2)volatile是如何禁止重排序的? (3)volatile的实现原理? (4)volatile的缺陷? 简介 volatile可以说是Java虚拟机提供的最轻量级的同步机制了,但是它并不容易被正确地理解,以至于很多人不习惯使用它,遇到多线程问题一律使用synchronized或其它锁来解决. 了解volatile的语义对理解多线程的特性具有很重要的意义,所以彤哥专门写了一篇文章来解释volatile的语义到底是什么. 语义一:可见性 前面

  • 详细解读java同步之synchronized解析

    问题 (1)synchronized的特性? (2)synchronized的实现原理? (3)synchronized是否可重入? (4)synchronized是否是公平锁? (5)synchronized的优化? (6)synchronized的五种使用方式? 简介 synchronized关键字是Java里面最基本的同步手段,它经过编译之后,会在同步块的前后分别生成 monitorenter 和 monitorexit 字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解

  • Java 并发编程:volatile的使用及其原理解析

    Java并发编程系列[未完]: •Java 并发编程:核心理论 •Java并发编程:Synchronized及其实现原理 •Java并发编程:Synchronized底层优化(轻量级锁.偏向锁) •Java 并发编程:线程间的协作(wait/notify/sleep/yield/join) •Java 并发编程:volatile的使用及其原理 一.volatile的作用 在<Java并发编程:核心理论>一文中,我们已经提到过可见性.有序性及原子性问题,通常情况下我们可以通过Synchroniz

  • Java同步关键字synchronize底层实现原理解析

    目录 1 字节码层实现 1.1 InterpreterRuntime::monitorenter 1.1.1 函数参数 JavaThread *thread 1.1.2 函数体 2 偏向锁 2.1 偏向锁的意义 2.2 偏向锁的获取 2.2.1 markOop mark = obj->mark() 2.2.2 判断mark是否为可偏向状态 2.2.3 判断mark中JavaThread的状态 2.2.4 通过CAS原子指令 2.2.5 如果执行CAS失败 2.3 偏向锁的撤销 2.4 轻量级锁

  • Java并发编程——volatile关键字

    一.volatile是什么 volatile是Java并发编程中重要的一个关键字,被比喻为"轻量级的synchronized",与synchronized不同的是,volatile只能修饰变量,无法修饰方法及代码块等. 下面是使用volatile关键字实现的单例模式: public class Singleton implements Serializable { private static volatile Singleton singleton; private Singleto

  • JAVA设计模式零基础解析之单例模式的八种方式

    目录 单例模式简介: 单例模式优点: 应用场景: 单例设计模式的八种方式: 1.饿汉式(静态常量) 2.饿汉式(静态代码块) 3.懒汉式(线程不安全) 4.懒汉式(线程安全,同步方法) 5.懒汉式(线程安全,同步代码块) 6.双重检查(推荐使用) 7.静态内部类(推荐使用) 8.枚举(推荐使用) 单例模式在JDK应用的源码分析 单例模式注意事项和细节说明 单例模式简介: 单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一.这种类型的设计模式属于创建型模式,它提供了

  • Java同步函数代码详解

    /* 同步函数 当函数中的代码全部放在了同步代码块中,那么这个函数就是同步函数 */ //同步函数的锁是this锁,this是一个引用,this指向的对象就是锁 //下面证明一下同步函数的锁就是this //创建两个线程,一个在同步代码块中执行,另一个在同步函数中执行 //同步代码块用的锁是obj,同步函数用的所是this //这就导致了两个线程存在两把锁,会出现上次所说的安全问题,即出现错误数据 //只有两个线程同时用一把锁,才能解决多线程的安全问题 class Ticket implemen

  • Java Thread多线程全面解析

    多线程是java中很重要的知识点,在此小编给大家总结Java Thread多线程,非常有用,希望大家可以掌握哦. 一.线程的生命周期及五种基本状态 关于Java中线程的生命周期,首先看一下下面这张较为经典的图: 上图中基本上囊括了Java中多线程各重要知识点.掌握了上图中的各知识点,Java中的多线程也就基本上掌握了.主要包括: Java线程具有五种基本状态 新建状态(New):当线程对象对创建后,即进入了新建状态,如:Thread t = new MyThread(); 就绪状态(Runnab

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

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

  • 详细分析java并发之volatile关键字

    Java面试中经常会涉及关于volatile的问题.本文梳理下volatile关键知识点. volatile字意为"易失性",在Java中用做修饰对象变量.它不是Java特有,在C,C++,C#等编程语言也存在,只是在其它编程语言中使用有所差异,但总体语义一致.比如使用volatile 能阻止编译器对变量的读写优化.简单说,如果一个变量被修饰为volatile,相当于告诉系统说我容易变化,编译器你不要随便优化(重排序,缓存)我. Happens-before 规范上,Java内存模型遵

随机推荐