Java多线程之volatile关键字及内存屏障实例解析

前面一篇文章在介绍Java内存模型的三大特性(原子性、可见性、有序性)时,在可见性和有序性中都提到了volatile关键字,那这篇文章就来介绍volatile关键字的内存语义以及实现其特性的内存屏障。

volatile是JVM提供的一种最轻量级的同步机制,因为Java内存模型为volatile定义特殊的访问规则,使其可以实现Java内存模型中的两大特性:可见性和有序性。正因为volatile关键字具有这两大特性,所以我们可以使用volatile关键字解决多线程中的某些同步问题。

volatile的可见性

volatile的可见性是指当一个变量被volatile修饰后,这个变量就对所有线程均可见。白话点就是说当一个线程修改了一个volatile修饰的变量后,其他线程可以立刻得知这个变量的修改,拿到最这个变量最新的值。

结合前一篇文章提到的Java内存模型中线程、工作内存、主内存的交互关系,我们对volatile的可见性也可以这么理解,定义为volatile修饰的变量,在线程对其进行写入操作时不会把值缓存在工作内存中,而是直接把修改后的值刷新回写到主内存,而当处理器监控到其他线程中该变量在主内存中的内存地址发生变化时,会让这些线程重新到主内存中拷贝这个变量的最新值到工作内存中,而不是继续使用工作内存中旧的缓存。

下面我列举一个利用volatile可见性解决多线程并发安全的示例:

public class VolatileDemo {
 //private static boolean isReady = false;
 private static volatile boolean isReady = false;
 static class ReadyThread extends Thread {
  public void run() {
   while (!isReady) {
   }
   System.out.println("ReadyThread finish");
  }
 }
 public static void main(String[] args) throws InterruptedException {
  new ReadyThread().start();
  Thread.sleep(1000);//sleep 1秒钟确保ReadyThread线程已经开始执行
  isReady = true;
 }
}

上面这段代码运行之后最终会在控制台打印出: ReadyThread finish ,而当你将变量isReady的volatile修饰符去掉之后再运行则会发现程序一直运行而不结束,而控制台也没有任何打印输出。

我们分析下这个程序:初始时isReady为false,所以ReadyThread线程启动开始执行后,它的while代码块因标志位isReady为false会进入死循环,当用volatile关键字修饰isReady时,main方法所在的线程将isReady修改为true之后,ReadyThread线程会立刻得知并获取这个最新的isReady值,紧接着while循环就会结束循环,所以最后打印出了相关文字。而当未用volatile修饰时,main方法所在的线程虽然修改了isReady变量,但ReadyThread线程并不知道这个修改,所以使用的还是之前的旧值,因此会一直死循环执行while语句。

volatile的有序性

有序性是指程序代码的执行是按照代码的实现顺序来按序执行的。

volatile的有序性特性则是指禁止JVM指令重排优化。

我们来看一个例子:

public class Singleton {
 private static Singleton instance = null;
 //private static volatile Singleton instance = null;
 private Singleton() { }

 public static Singleton getInstance() {
  //第一次判断
  if(instance == null) {
   synchronized (Singleton.class) {
    if(instance == null) {
     //初始化,并非原子操作
     instance = new Singleton();
    }
   }
  }
  return instance;
 }
}

上面的代码是一个很常见的单例模式实现方式,但是上述代码在多线程环境下是有问题的。为什么呢,问题出在instance对象的初始化上,因为 instance = new Singleton(); 这个初始化操作并不是原子的,在JVM上会对应下面的几条指令:

memory =allocate(); //1. 分配对象的内存空间
ctorInstance(memory); //2. 初始化对象
instance =memory;  //3. 设置instance指向刚分配的内存地址

上面三个指令中,步骤2依赖步骤1,但是步骤3不依赖步骤2,所以JVM可能针对他们进行指令重拍序优化,重排后的指令如下:

memory =allocate(); //1. 分配对象的内存空间
instance =memory;  //3. 设置instance指向刚分配的内存地址
ctorInstance(memory); //2. 初始化对象

这样优化之后,内存的初始化被放到了instance分配内存地址的后面,这样的话当线程1执行步骤3这段赋值指令后,刚好有另外一个线程2进入getInstance方法判断instance不为null,这个时候线程2拿到的instance对应的内存其实还未初始化,这个时候拿去使用就会导致出错。

所以我们在用这种方式实现单例模式时,会使用volatile关键字修饰instance变量,这是因为volatile关键字除了可以保证变量可见性之外,还具有防止指令重排序的作用。当用volatile修饰instance之后,JVM执行时就不会对上面提到的初始化指令进行重排序优化,这样也就不会出现多线程安全问题了。

volatile使用场景

volatile的可以在以下场景中使用:

当运算结果不依赖变量当前的值,或者能确保只有单一线程修改变量的值的时候,我们才可以对该变量使用volatile关键字
变量不需要与其他状态变量共同参与不变约束

volatile与原子性

volatile关键字能保证变量的可见性和代码的有序性,但是不能保证变量的原子性,下面我再举一个volatile与原子性的例子:

public class VolatileTest {
 public static volatile int count = 0;

 public static void increase() {
  count++;
 }

 public static void main(String[] args) {
  Thread[] threads = new Thread[20];
  for(int i = 0; i < threads.length; i++) {
   threads[i] = new Thread(() -> {
    for(int j = 0; j < 1000; j++) {
     increase();
    }
   });
   threads[i].start();
  }
  //等待所有累加线程结束
  while (Thread.activeCount() > 1) {
   Thread.yield();
  }
  System.out.println(count);
 }
}

上面这段代码创建了20个线程,每个线程对变量count进行1000次自增操作,如果这段代码并发正常的话,结果应该是20000,但实际运行过程中经常会出现小于20000的结果,因为count++这个自增操作不是原子操作。

上面的count++自增操作等价于count=count+1,所以JVM需要先读取count的值,然后在count的基础上给它加1,然后再将新的值重新赋值给count变量,所以这个自增总共需要三步。

上图中我将线程对count的自增操作画了个简单的流程,一个线程要对count进行自增时要先读取count的值,然后在当前count值的基础上进行count+1操作,最后将count的新值重新写回到count。

如果线程2在线程1读取count旧值写回count新值期间读取count的值,显然这个时候线程2读取的是count还未更新的旧值,这时两个线程是对同一个值进行了+1操作,这样这两个线程就没有对count实现累加效果,相反这些操作却又没有违反volatile的定义,所以这种情况下使用volatile依然会存在多线程并发安全的问题。

volatile与内存屏障

前面介绍了volatile的可见性和有序性,那JVM到底是如何为volatile关键字实现的这两大特性呢,Java内存模型其实是通过内存屏障(Memory Barrier)来实现的。

内存屏障其实也是一种JVM指令,Java内存模型的重排规则会要求Java编译器在生成JVM指令时插入特定的内存屏障指令,通过这些内存屏障指令来禁止特定的指令重排序。

另外内存屏障还具有一定的语义:内存屏障之前的所有写操作都要回写到主内存,内存屏障之后的所有读操作都能获得内存屏障之前的所有写操作的最新结果(实现了可见性)。因此重排序时,不允许把内存屏障之后的指令重排序到内存屏障之前。

下面的表是volatile有关的禁止指令重排的行为:

第一个操作 第二个操作:普通读写 第二个操作:volatile读 第二个操作:volatile写
普通读写 可以重排 可以重排 不可以重排
volatile读 不可以重排 不可以重排 不可以重排
volatile写 可以重排 不可以重排 不可以重排

从上面的表我们可以得出下面这些结论:

当第二个操作volatile写时,不论第一个操作是什么,都不能重排序。这个规则保证了volatile写之前的操作不会被重排到volatile写之后。

当第一个操作为volatile读时,不论第二个操作是什么,都不能重排。这个操作保证了volatile读之后的操作不会被重排到volatile读之前。

当第一个操作为volatile写,第二个操作为volatile读时,不能重排。

JVM中提供了四类内存屏障指令:

屏障类型 指令示例 说明
LoadLoad Load1; LoadLoad; Load2 保证load1的读取操作在load2及后续读取操作之前执行
StoreStore Store1; StoreStore; Store2 在store2及其后的写操作执行前,保证store1的写操作已刷新到主内存
LoadStore Load1; LoadStore; Store2 在stroe2及其后的写操作执行前,保证load1的读操作已读取结束
StoreLoad Store1; StoreLoad; Load2 保证store1的写操作已刷新到主内存之后,load2及其后的读操作才能执行

总结

volatile实现了Java内存模型中的可见性和有序性,它的这两大特性则是通过内存屏障来实现的,同时volatile无法保证原子性。

以上所述是小编给大家介绍的Java多线程之volatile关键字及内存屏障实例解析,希望对大家有所帮助,如果大家有任何疑问请给我留言,小编会及时回复大家的。在此也非常感谢大家对我们网站的支持!

(0)

相关推荐

  • java多线程编程之慎重使用volatile关键字

    volatile关键字相信了解Java多线程的读者都很清楚它的作用.volatile关键字用于声明简单类型变量,如int.float.boolean等数据类型.如果这些简单数据类型声明为volatile,对它们的操作就会变成原子级别的.但这有一定的限制.例如,下面的例子中的n就不是原子级别的: 复制代码 代码如下: package mythread; public class JoinThread extends Thread{public static volatile int n = 0;p

  • 学习Java多线程之volatile域

    前言 有时仅仅为了读写一个或者两个实例域就使用同步的话,显得开销过大,volatile关键字为实例域的同步访问提供了免锁的机制.如果声明一个域为volatile,那么编译器和虚拟机就知道该域是可能被另一个线程并发更新的.再讲到volatile关键字之前我们需要了解一下内存模型的相关概念以及并发编程中的三个特性:原子性,可见性和有序性. 1. java内存模型与原子性,可见性和有序性 Java内存模型规定所有的变量都是存在主存当中,每个线程都有自己的工作内存.线程对变量的所有操作都必须在工作内存中

  • 深入探讨Java多线程中的volatile变量

    volatile 变量提供了线程的可见性,并不能保证线程安全性和原子性. 什么是线程的可见性: 锁提供了两种主要特性:互斥(mutual exclusion) 和可见性(visibility).互斥即一次只允许一个线程持有某个特定的锁,因此可使用该特性实现对共享数据的协调访问协议,这样,一次就只有一个线程能够使用该共享数据.可见性要更加复杂一些,它必须确保释放锁之前对共享数据做出的更改对于随后获得该锁的另一个线程是可见的 -- 如果没有同步机制提供的这种可见性保证,线程看到的共享变量可能是修改前

  • java多线程中的volatile和synchronized用法分析

    本文实例分析了java多线程中的volatile和synchronized用法.分享给大家供大家参考.具体实现方法如下: 复制代码 代码如下: package com.chzhao; public class Volatiletest extends Thread { private static int count = 0; public void run() {         count++;     } public static void main(String[] args) {  

  • Java多线程并发编程 Volatile关键字

    volatile 关键字是一个神秘的关键字,也许在 J2EE 上的 JAVA 程序员会了解多一点,但在 Android 上的 JAVA 程序员大多不了解这个关键字.只要稍了解不当就好容易导致一些并发上的错误发生,例如好多人把 volatile 理解成变量的锁.(并不是) volatile 的特性: 具备可见性 保证不同线程对被 volatile 修饰的变量的可见性. 有一被 volatile 修饰的变量 i,在一个线程中修改了此变量 i,对于其他线程来说 i 的修改是立即可见的. 如: vola

  • Java多线程之volatile关键字及内存屏障实例解析

    前面一篇文章在介绍Java内存模型的三大特性(原子性.可见性.有序性)时,在可见性和有序性中都提到了volatile关键字,那这篇文章就来介绍volatile关键字的内存语义以及实现其特性的内存屏障. volatile是JVM提供的一种最轻量级的同步机制,因为Java内存模型为volatile定义特殊的访问规则,使其可以实现Java内存模型中的两大特性:可见性和有序性.正因为volatile关键字具有这两大特性,所以我们可以使用volatile关键字解决多线程中的某些同步问题. volatile

  • Java多线程之synchronized关键字的使用

    一.使用在非静态方法上 public synchronized void syzDemo(){ System.out.println(System.currentTimeMillis()); System.out.println("进入synchronized锁:syzDemo"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } 二.使用在静态方法上 publi

  • Java并发教程之volatile关键字详解

    引言 说到多线程,我觉得我们最重要的是要理解一个临界区概念. 举个例子,一个班上1个女孩子(临界区),49个男孩子(线程),男孩子的目标就是这一个女孩子,就是会有竞争关系(线程安全问题).推广到实际场景,例如对一个数相加或者相减等等情形,因为操作对象就只有一个,在多线程环境下,就会产生线程安全问题.理解临界区概念,我们对多线程问题可以有一个好意识. Jav内存模型(JMM) 谈到多线程就应该了解一下Java内存模型(JMM)的抽象示意图.下图: 线程A和线程B执行的是时候,会去读取共享变量(临界

  • 详解Java并发编程之volatile关键字

    目录 1.volatile是什么? 2.并发编程的三大特性 3.什么是指令重排序? 4.volatile有什么作用? 5.volatile可以保证原子性? 6.volatile 和 synchronized对比 总结 1.volatile是什么? 首先简单说一下,volatile是什么?volatile是Java中的一个关键字,也是一种同步机制.volatile为了保证变量的可见性,通过volatile修饰的变量具有共享性.修改了volatile修饰的变量,其它线程是可以读取到最新的值的 2.并

  • Java并发编程之Volatile变量详解分析

    目录 一.volatile变量的特性 1.1.保证可见性,不保证原子性 1.2.禁止指令重排 二.内存屏障 三.happens-before Volatile关键字是Java提供的一种轻量级的同步机制.Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量, 相比synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度. 但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其

  • Java并发编程:volatile关键字详细解析

    volatile这个关键字可能很多朋友都听说过,或许也都用过.在Java 5之前,它是一个备受争议的关键字,因为在程序中使用它往往会导致出人意料的结果.在Java 5之后,volatile关键字才得以重获生机. volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情.由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识,然后分析了volatile关键字的实现原理,最后给出了几个使用vola

  • Java多线程之Disruptor入门

    一.Disruptor简介 Disruptor目前是世界上最快的单机消息队列,由英国外汇交易公司LMAX开发,研发的初衷是解决内存队列的延迟问题(在性能测试中发现竟然与I/O操作处于同样的数量级).基于Disruptor开发的系统单线程能支撑每秒600万订单,2010年在QCon演讲后,获得了业界关注.2011年,企业应用软件专家Martin Fowler专门撰写长文介绍.同年它还获得了Oracle官方的Duke大奖.目前,包括Apache Storm.Camel.Log4j 2在内的很多知名项

  • Java多线程之Interrupt中断线程详解

    一.测试代码 https://gitee.com/zture/spring-test/blob/master/multithreading/src/test/java/cn/diswares/blog/InterruptTests.java 二.测试 为了方便理解简介中 interrupt 的概念, 写个 DEMO 测试一下 /** * 调用 interrupt 并不会影响线程正常运行 */ @Test public void testInvokeInterrupt() throws Inter

  • Java多线程之Future设计模式

    目录 Future -> 代表的是未来的一个凭据 AsynFuture -> Future具体实现类 FutureService -> 桥接Future和FutureTask FutureTask -> 将你的调用逻辑进行了隔离 Future -> 代表的是未来的一个凭据 public interface Future<T> { T get() throws InterruptedException; } AsynFuture -> Future具体实现类

随机推荐