【原文】https://www.toutiao.com/i6591422029323305480/

前言

不管是在面试还是实际开发中 volatile 都是一个应该掌握的技能。

首先来看看为什么会出现这个关键字。

内存可见性

由于 Java 内存模型(JMM)规定,所有的变量都存放在主内存中,而每个线程都有着自己的工作内存(高速缓存)。

线程在工作时,需要将主内存中的数据拷贝到工作内存中。这样对数据的任何操作都是基于工作内存(效率提高),并且不能直接操作主内存以及其他线程工作内存中的数据,之后再将更新之后的数据刷新到主内存中。

这里所提到的主内存可以简单认为是堆内存,而工作内存则可以认为是栈内存

如下图所示:

所以在并发运行时可能会出现线程 B 所读取到的数据是线程 A 更新之前的数据。

显然这肯定是会出问题的,因此 volatile 的作用出现了:

当一个变量被 volatile 修饰时,任何线程对它的写操作都会立即刷新到主内存中,并且会强制让缓存了该变量的线程中的数据清空,必须从主内存重新读取最新数据。

volatile 修饰之后并不是让线程直接从主内存中获取数据,依然需要将变量拷贝到工作内存中。

内存可见性的应用

当我们需要在两个线程间依据主内存通信时,通信的那个变量就必须的用 volatile 来修饰:

public class Volatile implements Runnable{

private static volatile boolean flag = true ;

@Override

public void run() {

while (flag){

}

System.out.println(Thread.currentThread().getName() +"执行完毕");

}

public static void main(String[] args) throws InterruptedException {

Volatile aVolatile = new Volatile();

new Thread(aVolatile,"thread A").start();

System.out.println("main 线程正在运行") ;

Scanner sc = new Scanner(System.in);

while(sc.hasNext()){

String value = sc.next();

if(value.equals("1")){

new Thread(new Runnable() {

@Override

public void run() {

aVolatile.stopThread();

}

}).start();

break ;

}

}

System.out.println("主线程退出了!");

}

private void stopThread(){

flag = false ;

}

}

主线程在修改了标志位使得线程 A 立即停止,如果没有用 volatile 修饰,就有可能出现延迟。

但这里有个误区,这样的使用方式容易给人的感觉是:

对 volatile 修饰的变量进行并发操作是线程安全的。

这里要重点强调,volatile 并不能保证线程安全性!

如下程序:

public class VolatileInc implements Runnable{

private static volatile int count = 0 ; //使用 volatile 修饰基本数据内存不能保证原子性

//private static AtomicInteger count = new AtomicInteger() ;

@Override

public void run() {

for (int i=0;i<10000 ;i++){

count ++ ;

//count.incrementAndGet() ;

}

}

public static void main(String[] args) throws InterruptedException {

VolatileInc volatileInc = new VolatileInc() ;

Thread t1 = new Thread(volatileInc,"t1") ;

Thread t2 = new Thread(volatileInc,"t2") ;

t1.start();

//t1.join();

t2.start();

//t2.join();

for (int i=0;i<10000 ;i++){

count ++ ;

//count.incrementAndGet();

}

System.out.println("最终Count="+count);

}

}

当我们三个线程(t1,t2,main)同时对一个 int 进行累加时会发现最终的值都会小于 30000。

这是因为虽然 volatile 保证了内存可见性,每个线程拿到的值都是最新值,但 count ++ 这个操作并不是原子的,这里面涉及到获取值、自增、赋值的操作并不能同时完成。

  • 所以想到达到线程安全可以使这三个线程串行执行(其实就是单线程,没有发挥多线程的优势)。
  • 也可以使用 synchronize 或者是锁的方式来保证原子性。
  • 还可以用 Atomic 包中 AtomicInteger 来替换 int,它利用了 CAS 算法来保证了原子性。

指令重排

内存可见性只是 volatile 的其中一个语义,它还可以防止 JVM 进行指令重排优化。

举一个伪代码:

int a=10 ;//1

int b=20 ;//2

int c= a+b ;//3

一段特别简单的代码,理想情况下它的执行顺序是:1>2>3。但有可能经过 JVM 优化之后的执行顺序变为了 2>1>3。

可以发现不管 JVM 怎么优化,前提都是保证单线程中最终结果不变的情况下进行的。

可能这里还看不出有什么问题,那看下一段伪代码:

private static Map<String,String> value ;

private static volatile boolean flag = fasle ;

//以下方法发生在线程 A 中 初始化 Map

public void initMap(){

//耗时操作

value = getMapValue() ;//1

flag = true ;//2

}

//发生在线程 B中 等到 Map 初始化成功进行其他操作

public void doSomeThing(){

while(!flag){

sleep() ;

}

//dosomething

doSomeThing(value);

}

这里就能看出问题了,当 flag 没有被 volatile 修饰时,JVM 对 1 和 2 进行重排,导致 value 都还没有被初始化就有可能被线程 B 使用了。

所以加上 volatile 之后可以防止这样的重排优化,保证业务的正确性。

指令重排的的应用

一个经典的使用场景就是双重懒加载的单例模式了:

public class Singleton {

private static volatile Singleton singleton;

private Singleton() {

}

public static Singleton getInstance() {

if (singleton == null) {

synchronized (Singleton.class) {

if (singleton == null) {

//防止指令重排

singleton = new Singleton();

}

}

}

return singleton;

}

}

这里的 volatile 关键字主要是为了防止指令重排。

如果不用 ,singleton = new Singleton();,这段代码其实是分为三步:

  • 分配内存空间。(1)
  • 初始化对象。(2)
  • 将 singleton 对象指向分配的内存地址。(3)

加上 volatile 是为了让以上的三步操作顺序执行,反之有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错。

总结

volatile 在 Java 并发中用的很多,比如像 Atomic 包中的 value、以及 AbstractQueuedLongSynchronizer 中的 state 都是被定义为 volatile 来用于保证内存可见性。

将这块理解透彻对我们编写并发程序时可以提供很大帮助。

【转】Java学习---volatile 关键字的更多相关文章

  1. 【转】java中volatile关键字的含义

    java中volatile关键字的含义   在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言 ...

  2. 转:java中volatile关键字的含义

    转:java中volatile关键字的含义 在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言 ...

  3. Java中Volatile关键字详解 (转自郑州的文武)

    java中volatile关键字的含义:http://www.cnblogs.com/aigongsi/archive/2012/04/01/2429166.html 一.基本概念 先补充一下概念:J ...

  4. 【转】Java学习---Java中volatile关键字实现原理

    [原文]https://www.toutiao.com/i6592879392400081412/ 前言 我们知道volatile关键字的作用是保证变量在多线程之间的可见性,它是java.util.c ...

  5. [面试必备]深入理解Java的volatile关键字

    前言 在Java并发编程中,volatile关键字有着至关重要的作用,在面试中也常常会是必备的一个问题.本文将会介绍volatile关键字的作用以及其实现原理. volatile作用 volatile ...

  6. java中volatile关键字的含义

    在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言是支持多线程的,为了解决线程并发的问题,在语 ...

  7. Java transient volatile关键字(转)

    Volatile修饰的成员变量在每次被线程访问时,都强迫从主内存中重读该成员变量的值.而且,当成员变量发生变化时,强迫线程将变化值回写到主内存.这样在任何时刻,两个不同的线程总是看到某个成员变量的同一 ...

  8. Java中Volatile关键字详解

    一.基本概念 先补充一下概念:Java并发中的可见性与原子性 可见性: 可见性是一种复杂的属性,因为可见性中的错误总是会违背我们的直觉.通常,我们无法确保执行读操作的线程能适时地看到其他线程写入的值, ...

  9. java中volatile关键字的含义 (转载)

    在java线程并发处理中,有一个关键字volatile的使用目前存在很大的混淆,以为使用这个关键字,在进行多线程并发处理的时候就可以万事大吉. Java语言是支持多线程的,为了解决线程并发的问题,在语 ...

随机推荐

  1. 在Linux上进行内核参数调整

    在Solaris上,使用工具mdb就可以直接修改内核内存里的内容.而在Linux上,则通常使用命令sysctl(8)做类似的事情. 本文以Fedora为例,介绍如何在Linux上进行内核参数调整. 常 ...

  2. PTA (Advanced Level) 1009 Product of Polynomials

    1009 Product of Polynomials This time, you are supposed to find A×B where A and B are two polynomial ...

  3. 软件架构设计学习总结(4):大数据架构hadoop

    摘要:Admaster数据挖掘总监 随着互联网.移动互联网和物联网的发展,谁也无法否认,我们已经切实地迎来了一个海量数据的时代,数据调查公司IDC预计2011年的数据总量将达到1.8万亿GB,对这些海 ...

  4. IIS 共享目录读写报错 Access to the path:“\\192.168.0.1\1.txt”is denied解决方案

    这个是IIS权限的问题,主要修改了以下地方,如果两台电脑有相同的用户名和密码可以跳过第一步 1.找到共享目录的文件夹,属性=>共享,给电脑创建一个新用户,共享文件下添加新用户的读写权限,然后对应 ...

  5. UIKit框架类层次图

    学习UIKit应该首选了解UIKit类的层次图,从根类一层一层的拨.

  6. CSS 分类 选择器

      CSS:层叠样式表(英文全称:Cascading Style Sheets)         后缀名:css         标志  style         对网页中元素位置的排版进行像素级精 ...

  7. ADO.NET获取数据(DataSet)同时获取表的架构

    普通的ADO.NET获取DataSet的写法如下: using System.Configuration; using System.Data; using System.Data.SqlClient ...

  8. Power of Matrix(uva11149+矩阵快速幂)

    Power of Matrix Time Limit:3000MS     Memory Limit:0KB     64bit IO Format:%lld & %llu Submit St ...

  9. POJ3694(KB9-D 割边+LCA)

    Network Time Limit: 5000MS   Memory Limit: 65536K Total Submissions: 10371   Accepted: 3853 Descript ...

  10. JS中冒泡排序,选择排序,快速排序

        var arr = [1,4,2,9,7,6,5,4,7,5];     // 冒泡排序(通俗的说就是j 和 j+1打,谁赢了谁去后面)       for(var i = 1;i<ar ...