java多线程volatile内存语义解析

这篇文章主要介绍了java多线程volatile内存语义解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下

  volatile关键字是java虚拟机提供的最轻量级额的同步机制。由于volatile关键字与java内存模型相关,因此,我们在介绍volatile关键字之前,对java内存模型进行更多的补充(之前的博文也曾介绍过)。

  1. java内存模型(JMM)

  JMM是一种规范,主要用于定义共享变量的访问规则,目的是解决多个线程本地内存与共享内存的数据不一致、编译器处理器的指令重排序造成的各种线程安全问题,以保障多线程编程的原子性、可见性和有序性。

  JMM规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程中的工作内存中存储了该线程用到的变量的主内存的拷贝,各线程对变量的所有操作都必须在工作内存中进行,
线程之间的变量值的传递都必须通过主内存来进行。

  JMM定义了8中操作实现主内存与工作内存的交互协议:

  •     1)lock:作用于主内存,它把一个变量标识为一条线程的独占状态。
  •     2)unlock:作用于主内存,它把一个处于锁定状态的变量的释放出来。
  •     3)read:作用于主内存,它把一个变量的值从主内存传输到线程的工作内存中。
  •     4)load:作用于工作内存,它把从主内存中read到的值放入工作内存的变量副本中。
  •     5)use:作用于工作内存,它把一个变量的值从主内存传递给执行引擎
  •     6)assign:作用与工作内存,它把一个从执行引擎接收到的值赋值给工作内存的变量。
  •     7)store:作用于工作内存,把工作内存中一个变量的值传送到主内存。
  •     8)write:作用于主内存,它把store操作从工作内存中得到的值放入主内存中的变量中。

  这8中操作以及对着8中操作的规则的限制就能确定哪些内存访问在并发条件下是线程安全的,这种方式比较繁琐,jdk1.5之后提出了提出了happens-before规则来判断线程是否安全。

  可以这么理解,happens-before规则是JMM的核心.Happens-before就是用来确定两个操作的执行顺序。这两个操作可在同一线程中,也可以在两个线程中。

happens-before规定:如果一个操作happens-before另个一操作,那么第一个操作的结果对第二个操作可见(但这并不意味着处理器必须按照happens-before顺序执行,只要不改变执行结果,可任意优化)。happens-before规则已在前边博文中介绍,这里不再重复(http://www.cnblogs.com/gdy1993/p/9117331.html

  JMM内存规则仅仅是一种规则,规则的最终落实是通过java虚拟机、编译器以及处理器一同协作来落实的,而内存屏障是java虚拟机、编译器、处理器之间沟通的纽带。

而java原因封装了这些底层的具体实现与控制,提供了synchronized、lock和volatile等关键字的来保障多线程安全问题。

  2. volatile关键字

  (1)volatile对可见性的保证

  在介绍volatile关键字之前,先来看这样一段代码:

//线程1
    boolean stop = false;
    while(!stop) {
      doSomething();
    }
    //线程2
    stop = true;

  有两个线程:线程1和线程2,线程1在stop==false时,不停的执行doSomething()方法;线程2在执行到一定情况时,将stop设置为true,将线程1中断,很多人采用这种方式中断线程,但这并不是安全的。因为stop作为一个普通变量,线程2对其的修改,并不能立刻被线程1所感知,即线程1对stop的修改仅仅在自己的工作内存中,还没来的急写入主内存,线程2工作内存中的stop并未修改,可能导致线程无法中断,虽然这种可能性很小,但一旦发生,后果严重。

  而使用volatile变量修饰就能避免这个问题,这也是volatile第一个重要含义:

    volatile修饰的变量,能够保证不同线程对这个变量操作的可见性,即一个线程修改了这个变量的值,这个新值对于其他线程是立即可见的。

    volatile的对可见性保证的原理:

  对于volatile修饰的变量,当某个线程对其进行修改时,会强制将该值刷新到主内存,这就使得其他线程对该变量在各自工作内存中的缓存无效,因而在其他线程对该变量进行操作时,必须从主内存中重新加载

   (2)volatile对原子性的保障?

  首先来看这样一段代码(深入理解java虚拟机):

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

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

  public static final int THREAD_COUNT = 20;

  public static void main(String[] args) {
    Thread[] threads = new Thread[THREAD_COUNT];
    for (Thread t : threads) {
      t = new Thread(new Runnable() {

        @Override
        public void run() {
          for(int i = 0; i < 10000; i++) {
            increase();
          }
        }
      });
      t.start();
    }

    while(Thread.activeCount() > 1) {
      Thread.yield();
    }

    System.out.println(race);//race < 200000

  }
}

  race是volatile修饰的共享变量,创建20个线程对这个共享变量进行自增操作,每个线程自增的次数为10000次,如果volatile能够保证原子性的话,最终race的结果肯定是200000。但结果不然,每次程序运行race'的值总是小于200000,这也侧面证明了volatile并不能保证共享变量操作的原子性。原理如下:

  线程1读取了race的值,然后cp分配的时间片结束,线程2此时读取了共享变量的值,并对race进行自增操作,并将操作后的值刷新到主内存,此时线程1已经读取了race的值,因此保留的依然是原来的值,此时这个值已是旧值,对race进行自增操作后刷新到主内存,因此主内存中的值也是旧值。这也是volatile仅仅能保障读到的是相对新值的原因。

  (3)volatile对有序性的保障

  首先来看这样一段代码:

//线程1
    boolean initialized = false;
    context = loadContext();
    initialized = true;
    //线程2
    while(!initialized) {
      sleep();
    }
    doSomething(context);

  线程2在initialized变量为true时,使用context变量完成一些操作;线程1负责加载context,并在加载完成后将initialized变量设为true。但是,由于initialized只是一个普通变量,普通变量仅仅能够保证在该方法的执行过程中,所有依赖赋值结果的地方都能获得正确的值,而不能保证变量的赋值顺序与程序代码的执行顺序一致。因此就可能出现这样一种情况,当线程1将initialized变量设为true时,context依然没有加载完成,但线程2由于读到initialized为true,就可能执行了doSomething()方法,可能会产生非常奇怪的效果。

  而volatile的第二个语义就是禁止重排序: 

    写volatile变量的操作与该操作之前的任何读写操作都不会被重排序;

    读volatile变量操作与该操作之后的任何读写操作都不会重排序。

  (4) volatile的底层实现原理

  java语言底层是通过内存屏障来实现volatile语义的。

  对于volatile变量的写操作:

  ①java虚拟机会在该操作之前插入一个释放屏障(loadstore+storestore),释放屏障禁止了volatile变量的写操作与该操作之前的任何读写操作的重排序。

  ②java虚拟机会在该操作之后插入一个存储屏障(storeload),存储屏障使得对volatile变量的写操作能够同步到主内存。

  对于volatile变量的读操作:

  ③java虚拟机会在该操作之前插入一个loadload,使得每次对volatile变量的读取都从主内存中重新加载(刷新处理器缓存)

  ④java虚拟机会在该操作之后插入一个获得屏障(loadstore+loadload),使得volatile后的任何读写操作与该操作进行重排序。

  ①③保障可见性,②④保障有序性。

  (5)volatile关键字与happens-before的关系

  Happens-before规则中的volatile规则为:对于一个volatile域的写happens-before后续每一个针对该变量的读操作。

  写线程执行write(),然后读线程执行read()方法,图中每个箭头都代表一个happens-before关系,黑色箭头是根据程序顺序规则,蓝色箭头根据volatile规则,红色箭头是根据传递性推出的,即操作2happens-before操作3,即对volatile共享变量的更新操作排在后续读取操作之前,对volatile变量的修改对后续volatile变量的读取可见。

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

(0)

相关推荐

  • 详解Java线程编程中的volatile关键字的作用

    1.volatile关键字的两层语义 一旦一个共享变量(类的成员变量.类的静态成员变量)被volatile修饰之后,那么就具备了两层语义: 1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的. 2)禁止进行指令重排序. 先看一段代码,假如线程1先执行,线程2后执行: //线程1 boolean stop = false; while(!stop){ doSomething(); } //线程2 stop = true; 这段代码是很典型

  • Java线程之线程同步synchronized和volatile详解

    上篇通过一个简单的例子说明了线程安全与不安全,在例子中不安全的情况下输出的结果恰好是逐个递增的(其实是巧合,多运行几次,会产生不同的输出结果),为什么会产生这样的结果呢,因为建立的Count对象是线程共享的,一个线程改变了其成员变量num值,下一个线程正巧读到了修改后的num,所以会递增输出. 要说明线程同步问题首先要说明Java线程的两个特性,可见性和有序性.多个线程之间是不能直接传递数据交互的,它们之间的交互只能通过共享变量来实现.拿上篇博文中的例子来说明,在多个线程之间共享了Count类的

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

  • 学习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不能保证线程安全(实例讲解)

    今天打了打代码研究了一下java的volatile关键字到底能不能保证线程安全,经过实践,volatile是不能保证线程安全的,它只是保证了数据的可见性,不会再缓存,每个线程都是从主存中读到的数据,而不是从缓存中读取的数据,附上代码如下,当synchronized去掉的时候,每个线程的结果是乱的,加上的时候结果才是正确的. /** * * 类简要描述 * * <p> * 类详细描述 * </p> * * @author think * */ public class Volatil

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

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

  • java多线程volatile内存语义解析

    这篇文章主要介绍了java多线程volatile内存语义解析,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下 volatile关键字是java虚拟机提供的最轻量级额的同步机制.由于volatile关键字与java内存模型相关,因此,我们在介绍volatile关键字之前,对java内存模型进行更多的补充(之前的博文也曾介绍过). 1. java内存模型(JMM) JMM是一种规范,主要用于定义共享变量的访问规则,目的是解决多个线程本地内存与共享内存

  • Java多线程回调方法实例解析

    所谓回调,就是客户程序C调用服务程序S中的某个方法A,然后S又在某个时候反过来调用C中的某个方法B,对于C来说,这个B便叫做回调方法. 下面看一个实际例子来理解: 本示例设置一个提问者,一个回答者,而回答者需要回答提问者一个很深奥的问题时,这时需要很多时间去查找,提问者又开始做其他的事情, 等回答者找到答案后,再把答案告诉提问者. 一.提问者的类 涉及到长时间的思考,要sleep,要继承Thread package com.xykj.thread; public class XiaoZhang

  • Java对象创建内存案例解析

    Java对象创建内存图解析 1. 栈 Java栈的区域很小 , 特点是存取的速度特别快,栈存储的特点是, 先进后出,存储速度快的原因: 栈内存, 通过 栈指针'来创建空间与释放空间,指针向下移动, 会创建新的内存, 向上移动, 会释放这些内存.这种方式速度特别快 , 仅次于PC寄存器,但是这种移动的方式, 必须要明确移动的大小与范围 ,明确大小与范围是为了方便指针的移动 , 这是一个对于数据存储的限制, 存储的数据大小是固定的 , 影响了程序的灵活性. 所以我们把更大部分的数据 存储到了堆内存中

  • Java多线程volatile原理及用法解析

    首先volatile有两大功能: 保证线程可见性 禁止指令重排序 1.保证线程可见性 首先我们来看这样一个程序,其中不加volatile关键字运行的结果截然不同,加上volatile程序能够正常结束,不加则程序进入死循环: package com.designmodal.design.juc01; import java.util.concurrent.TimeUnit; /** * @author D-L * @Classname T001_volatile * @Version 1.0 *

  • Java多线程 volatile关键字详解

    volatile volatile是一种轻量同步机制.请看例子 MyThread25类 public class MyThread25 extends Thread{ private boolean isRunning = true; public boolean isRunning() { return isRunning; } public void setRunning(boolean isRunning) { this.isRunning = isRunning; } public vo

  • java多线程下载文件原理解析

    原理解析:利用RandomAccessFile在本地创建一个随机访问文件,文件大小和服务器要下载的文件大小相同.根据线程的数量(假设有三个线程),服务器的文件三等分,并把我们在本地创建的文件同样三等分,每个线程下载自己负责的部分,到相应的位置即可. 示例图: 示例demo import java.io.InputStream; import java.io.RandomAccessFile; import java.net.HttpURLConnection; import java.net.U

  • 深度解析Java中volatile的内存语义实现以及运用场景

    volatile内存语义的实现 下面,让我们来看看JMM如何实现volatile写/读的内存语义. 前文我们提到过重排序分为编译器重排序和处理器重排序.为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型.下面是JMM针对编译器制定的volatile重排序规则表: 举例来说,第三行最后一个单元格的意思是:在程序顺序中,当第一个操作为普通变量的读或写时,如果第二个操作为volatile写,则编译器不能重排序这两个操作. 从上表我们可以看出: 当第二个操作是volatile写时,

  • 深度理解Java中volatile的内存语义

    volatile可见性实验 举个栗子 我这里开了两个线程,后面的线程去修改volatile变量,前面的线程不断获取volatile变量, 结果是会一致卡在死循环,控制台没有任何输出 假如将flag让volatile来进行修饰 结果是:三秒后,就不会不断打印出信息出来 注意,Thread.sleep是会刷新线程内存的,所以不要使用Thread.sleep来分别让一个线程获取两次volatile变量 volatile的特性 volatile其实相当于对变量的单词读或写操作加了锁.做了同步 由于是加了

  • 详解Java volatile 内存屏障底层原理语义

    目录 一.volatile关键字介绍及底层原理 1.volatile的特性(内存语义) 2.volatile底层原理 二.volatile--可见性 三.volatile--无法保证原子性 四.volatile--禁止指令重排 1.指令重排 2.as-if-serial语义 五.volatile与内存屏障(Memory Barrier) 1.内存屏障(Memory Barrier) 2.volatile的内存语义实现 六.JMM对volatile的特殊规则定义 一.volatile关键字介绍及底

  • 并发编程之Java内存模型volatile的内存语义

    1.volatile的特性 理解volatile特性的一个好办法是把对volatile变量的单个读/写,看成是使用同一个锁对单个读/写操作做了同步. 代码示例: package com.lizba.p1; /** * <p> * volatile示例 * </p> * * @Author: Liziba * @Date: 2021/6/9 21:34 */ public class VolatileFeatureExample { /** 使用volatile声明64位的long型

随机推荐