资讯专栏INFORMATION COLUMN

你应该知道的 volatile 关键字

calx / 220人阅读

摘要:线程在工作时,需要将主内存中的数据拷贝到工作内存中。内存可见性的应用当我们需要在两个线程间依据主内存通信时,通信的那个变量就必须的用来修饰正在运行。。。

前言

不管是在面试还是实际开发中 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() + "正在运行。。。");
        }
        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 线程正在运行") ;

        TimeUnit.MILLISECONDS.sleep(100) ;

        aVolatile.stopThread();

    }

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

总结

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

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

号外

最近在总结一些 Java 相关的知识点,感兴趣的朋友可以一起维护。

地址: https://github.com/crossoverJie/Java-Interview

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/68710.html

相关文章

  • JAVA多线程机制解析-volatile&synchronized

    摘要:当一个线程持有重量级锁时,另外一个线程就会被直接踢到同步队列中等待。 java代码先编译成字节码,字节码最后编译成cpu指令,因此Java的多线程实现最终依赖于jvm和cpu的实现 synchronized和volatile 我们先来讨论一下volatile关键字的作用以及实现机制,每个线程看到的用volatile修饰的变量的值都是最新的,更深入的解释就涉及到Java的内存模型了,我们...

    dendoink 评论0 收藏0
  • 如何使用 volatile, synchronized, final 进行线程间通信

    摘要:如线程执行后,线程执行,相当于线程向线程发送了消息。我们可以利用这种互斥性来进行线程间通信。 你是否真正理解并会用volatile, synchronized, final进行线程间通信呢,如果你不能回答下面的几个问题,那就说明你并没有真正的理解: 对volatile变量的操作一定具有原子性吗? synchronized所谓的加锁,锁住的是什么? final定义的变量不变的到底是什么...

    keithxiaoy 评论0 收藏0
  • volatile 键字深入分析及AtomicInteger使用

    摘要:我们使用命令查看字节码会发现在虚拟机中这个自增运算使用了条指令。其实这么说也不是最严谨的,因为即使经过编译后的字节码只使用了一条指令进行运算也不代表这条指令就是原子操作。 volatile的语义:1、保证被volatile修饰的变量对所有其他的线程的可见性。2、使用volatile修饰的变量禁止指令重排优化。看代码: public class InheritThreadClass ex...

    raoyi 评论0 收藏0
  • 重拾 Java 基础

    摘要:阿里开始招实习,同学问我要不要去申请阿里的实习,我说不去,个人对阿里的印象不好。记得去年阿里给我发了邮件,我很认真地回复,然后他不理我了。 引言 最近好久没有遇到技术瓶颈了,思考得自然少了,每天都是重复性的工作。 阿里开始招实习,同学问我要不要去申请阿里的实习,我说不去,个人对阿里的印象不好。 记得去年阿里给我发了邮件,我很认真地回复,然后他不理我了。(最起码的尊重都没有,就算我菜你起...

    ideaa 评论0 收藏0
  • 并发编程艺术

    摘要:假设不发生编译器重排和指令重排,线程修改了的值,但是修改以后,的值可能还没有写回到主存中,那么线程得到就是很自然的事了。同理,线程对于的赋值操作也可能没有及时刷新到主存中。线程的最后操作与线程发现线程已经结束同步。 很久没更新文章了,对隔三差五过来刷更新的读者说声抱歉。 关于 Java 并发也算是写了好几篇文章了,本文将介绍一些比较基础的内容,注意,阅读本文需要一定的并发基础。 本文的...

    curlyCheng 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<