资讯专栏INFORMATION COLUMN

Java 多线程(6):volatile 关键字的使用

paulquei / 3209人阅读

摘要:所以多线程条件下使用关键字的前提是对变量的写操作不依赖于变量的当前值,而赋值操作很明显满足这一前提。在多线程环境下,正确使用关键字可以比直接使用更加高效而且代码简洁,但是使用关键字也更容易出错。

volatile 作为 Java 语言的一个关键字,被看作是轻量级的 synchronized(锁)。虽然 volatile 只具有synchronized 的部分功能,但是一般使用 volatile 会比使用 synchronized 更有效率。在编写多线程程序的时候,volatile 修饰的变量能够:

保证内存 可见性

防止指令 重排序

保证对 64 位变量 读写的原子性

一. 保证内存可见性

JVM 中,每个线程都拥有自己栈内存,用来保存当前线程运行过程中的变量数据;然后多个线程之间共享堆内存(也称主存)。当线程需要访问一个变量时,首先将其从堆内存中复制到自己的栈内存作为副本,然后线程每次对该变量的操作,都将是对栈中的副本进行操作 —— 在某些时刻(比如退出 synchronized 块或线程结束),线程会将栈中副本的值写回到主存,此时主存中的变量才会被替换为副本的值。这样自然就带来一个问题,即如果两个线程共享一个变量,线程A 改变了变量的值,但是 线程B 可能无法立即发现。比如下面这个经典的例子:

public class ConcurrentTest {

    private static boolean running = true;

    public static class AnotherThread extends Thread {

        @Override
        public void run() {
            System.out.println("AnotherThread is running");

            while (running) { }

            System.out.println("AnotherThread is stoped");
        }

    }

    public static void main(String[] args) throws Exception {
        new AnotherThread ().start();

        Thread.sleep(1000);
        running = false;  // 1 秒之后想停止 AnotherThread 
    }
}

上面这段代码一般情况下都会死锁,就是因为在 main 方法(主线程)中对 running 做的修改,并不能立马对 AnotherThread 可见。

如果将 running 加上修饰符 volatile,那么便可以获取实际希望的结果,因为此时主线程中设置 runningfalse 之后,AnotherThread 可以立马发现 running 的值发生了改变:

对于 volatile 修饰的变量,JVM 可以保证:

每次对该变量的写操作,都将立即同步到主存;

每次对该变量的读操作,都将从主存读取,而不是线程栈

二. 防止指令重排序

如果一个操作不是原子操作,那么 JVM 便可能会对该操作涉及的指令进行 重排序。重排序即在不改变程序语义的前提下,通过调整指令的执行顺序,尽可能达到提高运行效率的目的。

对于单例模式,为了达到延时初始化,并且可以在多线程环境下使用,我们可以直接使用 synchronized 关键字:

public class Singleton {

    public static Singleton instance = null;

    private Singleton() { }

    public synchronized static Singleton getSingleton() {
        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }
}

这样做的缺陷也很明显,那就是 instance 初始化完毕之后,以后每次获取 instance 仍然需要进行加锁操作,是个很大的效率浪费。

于是出现了一种经典写法叫 “双重检测锁”:

public class Singleton {

    public static Singleton instance = null;

    private Singleton() { }

    public static Singleton getSingleton() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }

        return instance;
    }
}

但是这样的写法同样会存在问题,因为 instance = new Singleton() 并非原子操作,其大概可以等同于执行:

分配一个 Singleton 对应的内存

初始化这个 Singleton 对应的内存

instance 指向对应的内存的地址

其中,2 依赖于 1,但是 3 并不依赖于 2 —— 所以,存在 JVM 将这三条语句重排序为 1->3->2 的可能,即变为:

a. 分配一个 Singleton 对应的内存
b.instance 指向对应的内存的地址
c. 初始化这个 Singleton 对应的内存

此时如果 线程A 执行完 b,那么此时的 instance 指向的内存并不为 null,然而这块内存却还没有被初始化。当 线程B 此时判断第一个 if (instance == null) 时发现 instance 并不为 null,便会将此时的 instance 返回 —— 但 Singleton 的初始化可能并未完成,此时 线程B 使用 instance 便可能会出现错误。

在 JDK 1.5 之后,增强了 volatile 的语义,严格限制 JVM (编译器、处理器)不能对 volatile 修饰的变量涉及的操作指令进行重排序。

所以为了避免对 instance 变量涉及的操作进行重排序,保证 “双重检测锁” 的正确性,我们可以将 instance 使用 volatile 修饰:

public class Singleton {

    /* 使用 volatile 修饰 */
    public static volatile Singleton instance = null;

    private Singleton() { }

    public static Singleton getSingleton() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }

        return instance;
    }
}
三. 保证对 64 位变量读写的原子性

JVM 可以保证对 32位 数据读写的原子性,但是对于 longdouble 这样 64位 的数据的读写,会将其分为 高32位 和 低32位 分两次读写。所以对于longdouble 的读写并不是原子性的,这样在并发程序中共享 longdouble 变量就可能会出现问题,于是 JVM 提供了 volatile 关键字来解决这个问题:

使用 volatile 修饰的 longdouble 变量,JVM 可以保证对其读写的原子性。

但值得注意的是,此处的 “写” 仅指对 64位 的变量进行直接赋值。而对于 i++ 这个语句,事实上涉及了 读取-修改-写入 三个操作:

读取变量到栈中某个位置

对栈中该位置的值进行自增

将自增后的值写回到变量对应的存储位置

因此哪怕变量 i 使用 volatile 修饰,也并不能使涉及上面三个操作的 i++ 具有原子性。所以多线程条件下使用 volatile 关键字的前提是:对变量的写操作不依赖于变量的当前值,而赋值操作很明显满足这一前提。

在多线程环境下,正确使用 volatile 关键字可以比直接使用 synchronized 更加高效而且代码简洁,但是使用 volatile 关键字也更容易出错。所以,除非十分清楚 volatile 的使用场景,否则还是应该选择更加具有保障性的 synchronized

Brian Goetz 大大写过一篇 “volatile 变量使用指南”,有兴趣的读者可以参阅:Java 理论与实践: 正确使用 Volatile 变量

volatile 变量的底层实现原理,有兴趣的读者可以参阅:

http://www.infoq.com/cn/artic...

http://www.cnblogs.com/paddix...

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

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

相关文章

  • Java线程学习(三)volatile键字

    摘要:三关键字能保证原子性吗并发编程艺术这本书上说保证但是在自增操作非原子操作上不保证,多线程编程核心艺术这本书说不保证。多线程访问关键字不会发生阻塞,而关键字可能会发生阻塞关键字能保证数据的可见性,但不能保证数据的原子性。 系列文章传送门: Java多线程学习(一)Java多线程入门 Java多线程学习(二)synchronized关键字(1) java多线程学习(二)synchroniz...

    tain335 评论0 收藏0
  • 慕课网_《细说Java线程之内存可见性》学习总结

    时间:2017年07月09日星期日说明:本文部分内容均来自慕课网。@慕课网:http://www.imooc.com教学源码:无学习源码:https://github.com/zccodere/s... 第一章:课程简介 1-1 课程简介 课程目标和学习内容 共享变量在线程间的可见性 synchronized实现可见性 volatile实现可见性 指令重排序 as-if-seria...

    wupengyu 评论0 收藏0
  • JAVA并发编程之-Volatile键字及内存可见性

    摘要:的缺点频繁刷新主内存中变量,可能会造成性能瓶颈不具备操作的原子性,不适合在对该变量的写操作依赖于变量本身自己。 作者:毕来生微信:878799579 1. 什么是JUC? JUC全称 java.util.concurrent 是在并发编程中很常用的实用工具类 2.Volatile关键字 1、如果一个变量被volatile关键字修饰,那么这个变量对所有线程都是可见的。2、如果某条线程修...

    xcold 评论0 收藏0
  • 深入理解volatile类型——从Java虚拟机内存模型角度

    摘要:本文从内存模型角度,探讨的实现原理。通过共享内存或者消息通知这两种方法,可以实现通信或同步。基于共享内存的线程通信是隐式的,线程同步是显式的而基于消息通知的线程通信是显式的,线程同步是隐式的。锁规则锁的解锁,于于锁的获取或加锁。 一、前言 在java多线程编程中,volatile可以用来定义轻量级的共享变量,它比synchronized的使用成本更低,因为它不会引起线程上下文的切换和调...

    mushang 评论0 收藏0
  • BATJ都爱问线程面试题

    摘要:今天给大家总结一下,面试中出镜率很高的几个多线程面试题,希望对大家学习和面试都能有所帮助。指令重排在单线程环境下不会出先问题,但是在多线程环境下会导致一个线程获得还没有初始化的实例。使用可以禁止的指令重排,保证在多线程环境下也能正常运行。 下面最近发的一些并发编程的文章汇总,通过阅读这些文章大家再看大厂面试中的并发编程问题就没有那么头疼了。今天给大家总结一下,面试中出镜率很高的几个多线...

    高胜山 评论0 收藏0

发表评论

0条评论

paulquei

|高级讲师

TA的文章

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