多线程系列(四) -volatile关键字使用详解

这篇具有很好参考价值的文章主要介绍了多线程系列(四) -volatile关键字使用详解。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

一、简介

在上篇文章中,我们介绍到在多线程环境下,如果编程不当,可能会出现程序运行结果混乱的问题。

出现这个原因主要是,JMM 中主内存和线程工作内存的数据不一致,以及多个线程执行时无序,共同导致的结果。

多线程系列(四) -volatile关键字使用详解

同时也提到引入synchronized同步锁,可以保证线程同步,让多个线程依次排队执行被synchronized修饰的方法或者方法块,使程序的运行结果与预期一致。

不可否认,采用synchronized同步锁确实可以保证线程安全,但是它对服务性能的消耗也很大,synchronized是一个独占式的同步锁,比如当多个线程尝试获取锁时,其中一个线程获取到锁之后,未获取到锁的线程会不断的尝试获取锁,而不会发生中断,当冲突严重的时候,线程会直接进入阻塞状态,不能再干别的活。

为了实现线程之间更加方便的访问共享变量,Java 编程语言还提供了另一种同步机制:volatile域变量,在某些场景下使用它会更加方便。

一般来说,被volatile修饰的变量,可以保证所有线程看到这个变量都是同一个值,同时它不会引起线程上下文的切换和调度,相比synchronizedvolatile更加的轻量化。

比较官方的解释,volatile修饰变量有以下几个作用:

  • 1.保证变量的可见性,不保证原子性
    当用volatile修饰一个变量时,JMM 会把当前线程本地内存中的变量强制刷新到主内存中去,这个写操作也会导致其他线程中被volatile修饰的变量缓存无效,然后从主内存中获取最新的值

  • 2.禁止指令重排
    正常情况下,编译器和处理器为了优化程序执行性能会对指令序列进行重排序,当然是在不影响程序结果的前提下。volatile能够在一定程度上禁止 JVM 进行指令重排。

从概念上感觉比较难理解,下面我们结合几个例子,一起来看看它的具体应用。

二、volatile 使用详解

我们先看一个例子。

public class DataEntity {

    private boolean isRunning = true;

    public void addCount(){
        System.out.println("线程运行开始....");
        while (isRunning){ }
        System.out.println("线程运行结束....");
    }

    public boolean isRunning() {
        return isRunning;
    }

    public void setRunning(boolean running) {
        isRunning = running;
    }
}
public class MyThread extends Thread {

    private DataEntity entity;

    public MyThread(DataEntity entity) {
        this.entity = entity;
    }

    @Override
    public void run() {
        entity.addCount();
    }
}
public class MyThreadTest {

    public static void main(String[] args) throws InterruptedException {
        // 初始化数据实体
        DataEntity entity = new DataEntity();

        MyThread threadA = new MyThread(entity);
        threadA.start();

        // 主线程阻塞1秒
        Thread.sleep(1000);

        // 将运行状态设置为false
        entity.setRunning(false);
    }
}

运行结果如下:

多线程系列(四) -volatile关键字使用详解

从实际运行结果来看,程序进入死循环状态,虽然最后一行手动设置了entity.setRunning(false),但是没有起到任何的作用。

原因其实也很简单,虽然主线程mainisRunning变量设置为false,但是线程threadA 里面的isRunning变量还是true,两个线程看到的数据不一致。

假如在isRunning变量上,加一个volatile关键字,我们再来看看运行效果。

/**
 * 在 isRunning 变量上加一个 volatile 关键字
 */
private volatile boolean isRunning = true;

运行结果如下:

多线程系列(四) -volatile关键字使用详解

程序运行后自动结束。

说明当主线程mainisRunning变量设置为false时,线程threadA 里面的isRunning值也随着发生变化。

说明被volatile修饰的变量,在多线程环境下,可以保证所有线程看到这个变量都是同一个值。

三、volatile 不适用的场景

对于某些场景下,volatile可能并不适用,我们还是先看一个例子。

public class DataEntity {

    private volatile int count = 0;

    public void addCount(){
        for (int i = 0; i < 100000; i++) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}
public class MyThreadTest {

    public static void main(String[] args) throws InterruptedException {
        // 初始化数据实体
        DataEntity entity = new DataEntity();

        // 初始化5个线程计数器
        CountDownLatch latch = new CountDownLatch(5);

        // 采用多线程进行操作
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    entity.addCount();
                    //线程运行完毕减1
                    latch.countDown();
                }
            }).start();
        }

        // 等待以上线程执行完毕,再获取结果
        latch.await();
        System.out.println("result: " + entity.getCount());
    }
}

运行结果如下:

第一次运行:result: 340464
第二次运行:result: 318342
第三次运行:result: 305957

理论上使用 5 个线程分别执行了100000自增,我们预期的结果应该是5*100000=500000,从实际的运行结果可以看出,与预期不一致。

这是因为volatile的作用其实是有限的,它只能保证多个线程之间看到的共享变量值是最新的,但是无法保证多个线程操作共享变量时依次有序,无法保证原子性操作

上面的例子中count++不是一个原子性操作,在处理器看来,其实一共做了三个步骤的操作:读取数据对数据加 1回写数据,在多线程随机执行情况下,输出结果不能达到预期值。

如果想要实现与预期一致的结果,有以下三种方案可选。

方案一:采用synchronized同步锁

public class DataEntityC2 {

    private int count = 0;

    /**
     * 采用 synchronized 同步锁,可以实现多个线程执行方法时串行
     */
    public synchronized void addCount(){
        for (int i = 0; i < 100000; i++) {
            count++;
        }
    }

    public int getCount() {
        return count;
    }
}

方案二:采用Lock

public class DataEntityC2 {

    private int count = 0;

    private Lock lock = new ReentrantLock();

    /**
     * 采用 Lock 锁,可以实现多个线程执行方法时串行
     */
    public void addCount(){
        for (int i = 0; i < 100000; i++) {
            lock.lock();
            try {
                count++;
            } finally {
                lock.unlock();
            }
        }
    }

    public int getCount() {
        return count;
    }
}

方案三:采用JUC包中的原子操作类

public class DataEntity {

    private AtomicInteger inc = new AtomicInteger();

    /**
     * 采用原子操作类,原子操作类是通过CAS循环的方式来保证操作原子性
     */
    public void addCount(){
        for (int i = 0; i < 100000; i++) {
            inc.getAndIncrement();
        }
    }

    public int getCount() {
        return inc.get();
    }
}

以上三种方案,都可以实现程序的运行结果与预期一致!

四、volatile 的原理

通过以上的例子介绍,相信大家对volatile关键字的作用有了一些认识。

volatile修饰的变量,可以保证变量在内存中的可见性,但是无法保证原子性操作。

关于原子性、可见性和有序性的定义,这三个特性主要从多线程编程安全角度总结出来的一些基本要素,也是并发编程的三大核心基础,在上篇文章中有所提到过,这里不再重复讲了。

在 JVM 底层,volatile是通过采用“内存屏障”来实现内存可见性和禁止指令重排。观察不加入volatile和加入volatile关键字所生成的汇编代码发现,加入volatile关键字的代码会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障,可以提供以下 3 个功能。

  • 1.它确保指令重排序时,不会把后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面,禁止处理器对影响程序执行结果的指令进行重排
  • 2.它会强制将缓存的修改操作立刻写入主存,保证内存变量可见
  • 3.如果是写操作,它会导致其它 CPU 中对应的行缓存无效,目的是让其他线程中被volatile修饰的变量缓存无效,然后从主内存中获取最新的值

五、单例模式中的双重检锁为什么要加 volatile?

在上篇文章中,我们提到过单例设计模式中的双重校验锁实现。

public class Singleton {  

    private volatile static Singleton singleton;  
    
    private Singleton (){}  
    
    public static Singleton getSingleton() {  
        if (singleton == null) {  //第一行
            synchronized (Singleton.class) {  //第二行
                if (singleton == null) {  //第三行
                    singleton = new Singleton();  //第四行
                }  
            }  
        }  
        return singleton;  //第五行
    }  
}

synchronized可以保证原子性、可见性和有序性,为什么变量singleton还需要加volatile关键字呢?

之所以需要加volatile关键字的原因是:问题出在第一行代码不在同步代码块之类,可能出现这个对象地址不为空,但是内容为空

以初始化一个Singleton singleton = new Singleton();为例,JVM 会分三个步骤完成:

a. memory = allocate() //分配内存
b. ctorInstanc(memory) //初始化对象
c. instance = memory   //设置instance指向刚分配的地址

上面的代码在编译运行时可能会出现重排序,因为bc无逻辑关联,执行的顺序是a -> b -> c或者a -> c -> b,在多线程的环境下可能会出现问题。

分析过程如下:

  • 1.线程 A 执行到第四行代码时,线程 B 进来执行第一行代码
  • 2.假设线程 A 在执行过程中发生了指令重排序,先执行了ac,没有执行b
  • 3.由于线程 A 执行了c导致instance指向了一段地址,此时线程 B 检查singleton发现不为null,会直接跳转到第五行代码,返回一个未初始化的对象,导致程序会出现报错
  • 4.因此需要在singleton变量上加一个volatile关键字,当线程 A 执行完毕b操作之后,会变量强制刷新到主内存中,此时线程 B 也可以拿到最新的对象

这就是为啥双重检锁模式中,singleton变量为啥要加一个volatile关键字的原因。

采用双重检锁的方式,可以显著的提升并发查询的效率。

六、小结

本篇文章主要围绕volatile关键字的用途、使用方式和一些坑点,做了一个简单的知识总结,内容难免有所遗漏,欢迎网友留言指出!

七、参考

1、老鼠只爱大米 - Java volatile关键字总结文章来源地址https://www.toymoban.com/news/detail-832982.html

到了这里,关于多线程系列(四) -volatile关键字使用详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • volatile关键字原理的使用介绍和底层原理解析和使用实例

    volatile 的主要作用是保证可见性和有序性,禁止编译器优化。 保证可见性:当一个变量被声明为 volatile 之后,每次读取这个变量的值都会从主内存中读取,而不是从缓存中读取,这就保证了不同线程对这个变量操作的可见性。 有序性:volatile 保证了不同线程对一个 vo

    2024年02月02日
    浏览(27)
  • Java多线程系列——同步关键字

    目录 一、线程安全和数据不一致性 二、synchronized的作用 三、synchronized工作原理 四、锁的级别 五、synchronized的优点与缺点 六、实战应用 七、总结 在Java中, synchronized 是并发编程中的一个基本构建块,用于控制多个线程对共享资源的访问,以确保数据的一致性

    2024年02月21日
    浏览(32)
  • volatile关键字作用

    volatile是一个和多线程相关的,主要有一下2点作用(只保证可见性,不保证原子性) 防止指令重排(有序性) JVM在不改变程序执行结果的前提下,在编译时会对指令的顺序进行重新排序,而volatile则能够禁止指令的重新排序 能够确保线程内存中的对象对其他内存可

    2024年02月15日
    浏览(34)
  • 【C】volatile 关键字

    1)基本概念 const 是C语言的一个。 const 用于告诉编译器相应的变量可能会在程序的控制之外被修改,因此编译器不应该对其进行优化。 声明语法: 作用: 防止编译器优化,确保对变量的每次访问都是实际的读写操作,而不是使用缓存中的值。 用于表示可能会 被异步

    2024年01月22日
    浏览(36)
  • JAVA volatile 关键字

    volatile 是JAVA虚拟机提供的轻量级的同步机制,有三大特性 1、保证可见性  2、不保证原子性  3、禁止指令重排 JMM  JAVA内存模型本身是一种抽象的概念并不真实存在 它描述的是一组规则或规范,提供这组规范定义了程序中各个变量(包括实例变量、静态变量)的访问方式。

    2024年02月13日
    浏览(36)
  • [JAVAee]volatile关键字

    目录 1.volatile的特性 ①保持线程可见性 2.volatile注意事项及适用场景 ①注意事项 ②适用场景 volatile,译为\\\"易变的\\\". 对此我们就可以这样理解,对于被volatile修饰的变量的数值,是容易变化的. 在之前的线程安全文章中,我们有讲解过\\\"可见性\\\",对于线程间的这个特性可能会导致:线程

    2024年02月16日
    浏览(28)
  • C语言volatile关键字

    在C语言中, volatile 是一个类型修饰符,用于告诉编译器对象的值可能会在编译器无法检测到的情况下被改变。这通常发生在以下两种情况: 硬件的输入/输出操作,例如一个设备寄存器的读取或写入。 共享内存的并行程序,其中一个线程修改了一个内存位置,而另一个线程

    2024年02月07日
    浏览(38)
  • 【Java基础】volatile关键字

    关于作者:CSDN内容合伙人、技术专家, 从零开始做过日活千万级APP。 专注于分享各领域原创系列文章 ,擅长java后端、移动开发、人工智能等,希望大家多多支持。 我们继续总结学习Java基础知识,温故知新。 volatile 是一个Java,可以用来修饰变量,volatile也被称为轻

    2024年02月11日
    浏览(32)
  • volatile关键字(轻量级锁)

    目录 一、volatile出现背景 二、JMM概述 2.1、JMM的规定  三、volatile的特性 3.1、可见性  3.1.1、举例说明  3.1.2、总结 3.2、无法保证原子性 3.2.1、举例说明 3.2.2、分析 3.2.3、使用volatile对原子性测试  3.2.4、使用锁机制  3.2.5、总结 3.3、禁止指令重排序  四、volatile的内存语义 4

    2024年02月15日
    浏览(31)
  • 浅析Java中volatile关键字

            Java中的volatile用于修饰一个变量,当这个变量被多个线程共享时,这个变量的值如果发生更新,每个线程都能获取到最新的值。volatile在多线程环境下还会禁止指令重排序,确保变量的赋值操作按照代码的顺序执行。需要注意是它不能保证变量操作的

    2024年01月21日
    浏览(37)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包