`

Java之 volatile 关键字原理详解

阅读更多
一、什么是 volatile ?

为了更好地了解Java中的volatile关键字,您将必须对Java内存模型中的变量发生的优化有所了解。假设您在代码中声明了一个名为 test 的变量。您会认为 test 变量将仅存储在RAM中,并且所有线程都将从那里读取测试变量的值。但是,为了使处理更快,处理器会将变量的值保存在其缓存中。在那种情况下,仅当高速缓存和内存之间发生同步时,对值的任何更改才会写回到主内存。

这将在多个线程正在读取或写入共享变量的地方引起问题。如果我们以在多个线程中使用的 test 变量为例,则可能出现以下情况:一个线程对仍存储在高速缓存中的 test 变量进行了更改,而另一个线程试图从主内存中读取测试变量的值 。这将导致内存和高速缓存数据不一致错误,因为不同的线程将读取/写入不同的测试变量值。



二、如何声明 volatile ?

将变量声明为 volatile 可确保始终从主存储器读取变量的值。 因此,在Java中将字段声明为volatile 可以提供线程可见性。从而确保每次对volatile字段进行都写操作,都会早于随后读取该字段的操作,即:(当前线程的)写操作对随后的(其它线程的)读操作都是可见的。

我们在上面看到的问题是因为 volatile 字段不会发生CPU缓存的值,因为可以保证线程1对volatile变量所做的更新始终对线程2可见。


三、 volatile 使用示例

Java中volatile关键字最常见的用法之一是声明为volatile的布尔状态标志,该标志指示事件的完成,以便另一个线程可以启动。

首先让我们看看如果在这种情况下不使用volatile会发生什么。

public class VolatileDemo {
    private static  boolean flag = false;
    public static void main(String[] args) {
        // Thread-1
        new Thread(new Runnable(){
            @Override
            public void run() {
                for (int i = 1; i <= 2000; i++){
                    System.out.println("value - " + i);
                }
                // changing status flag
                flag = true;
                System.out.println("status flag changed " + flag );
            }
            
        }).start();
        // Thread-2
        new Thread(new Runnable(){
            
            @Override
            public void run() {
                int i = 1;
                while (!flag){
                    i++;
                }
                System.out.println("Start other processing " + i);    
            }
            
        }).start();
    }
}


OUTPUT
....
....
value - 1997
value - 1998
value - 1999
value - 2000
status flag changed true


运行此代码后,您会看到第一个线程显示i直到2000的值,然后并更改状态标志。但是第二个线程不会打印消息“Start other processing ”,并且程序不会终止。 由于在while循环中的线程2中经常访问flag变量,因此编译器可以通过将flag的值放在缓存寄存器中来进行优化,然后它将继续测试循环条件(while(!flag)),而无需向主存储器中读取其中的值。

现在,如果您更改布尔变量标志并将其标记为volatile,这将确保一个线程对共享变量所做的更改对其他线程可见。

private static volatile boolean flag = false;


OUTPUT:结果正确
....
....
value - 1997
value - 1998
value - 1999
value - 2000
status flag changed true
Start other processing 68925258


四、 volatile 受变量声明的顺序的影响

当线程读取一个volatile变量时,它不仅会看到对volatile的最新更改,还会看到导致更改的代码的副作用。 这也称为(happens-before-extended-guarantee),由Java 5中的volatile 关键字提供。

例如,如果线程T1在更新volatile变量之前更改了其他变量,则线程T2也将获得那些在线程T1中更新volatile变量之前已更改的变量的更新变量。

这将我们带到可能在编译时发生的重新排序以优化代码的地步。 只要不改变语义,就可以对代码语句进行重新排序。

private int var1;
private int var2;
private volatile int var3;
public void calcValues(int var1, int var2, int var3){
    this.var1 = 1;
    this.var2 = 2;
    this.var3 = 3;
}


由于var3是 volatile 的,由于 happens-before extended guarantee,因此var1和var2的更新值也将被写入主内存,并且对其他线程可见。

如果将这些语句重新排序以进行优化怎么办。


this.var3 = 3;
this.var1 = 1;
this.var2 = 2;


现在,变量var1和var2的值在 volatile 变量var3更新后更新。 因此,这些变量var1和var2的更新值可能不可用于其他线程。

这就是为什么如果在更新其他变量之后对 volatile 变量进行读取或写入,则不允许重新排序的原因。


五、 volatile 只能保证 可见性,不能保证原子性

在只有一个线程正在写入变量而其他线程仅在读取的情况下(如在状态标志的情况下),volatile有助于正确查看变量值。 但是,如果许多线程正在读取和写入共享变量的值,那么volatile是不够的。 在那种情况下,由于竞争条件,线程可能仍会得到错误的值。

让我们用一个Java示例来说清楚,其中有一个SharedData类,其对象在线程之间共享。 在SharedData类中,计数器变量被标记为volatile。 创建了四个线程,它们使计数器递增,然后显示更新的值。 由于存在竞争条件,线程可能仍会获得错误的值。 请注意,您也可以在几次运行中获得正确的值。

public class VolatileDemo implements Runnable {
    SharedData obj = new SharedData();
    public static void main(String[] args) {
        VolatileDemo vd = new VolatileDemo();
        new Thread(vd).start();
        new Thread(vd).start();
        new Thread(vd).start();
        new Thread(vd).start();
    }
 
    @Override
    public void run() {
        obj.incrementCounter();
        System.out.println("Counter for Thread " + Thread.currentThread().getName() + 
                    " " + obj.getCounter());
    }    
}
 
class SharedData{
    public volatile int counter = 0;
    public int getCounter() {
        return counter;
    }
 
    public void incrementCounter() {
        ++counter;
    }
}



OUTPUT
Counter for Thread Thread-0 1
Counter for Thread Thread-3 4
Counter for Thread Thread-2 3
Counter for Thread Thread-1 3



六、 volatile 要点总结

在Java中volatile关键字只能与不带的方法和类变量使用。

标记为volatile的变量可确保不缓存该值,并且对volatile变量的更新始终在主内存中进行。

volatile 也保证了报表的重新排序不会发生这样的挥发性提供的之前发生延长的保证下volatile变量的更新之前更改其他变量也被写入主存储器和可见的其他线程。

volatile 确保仅可见性而不是原子性。

如果 final 变量也声明为 volatile,则是编译时错误。

使用 volatile 比使用 Lock 便宜。





























-
Refer: https://knpcode.com/java/multi-threading/volatile-keyword-in-java/
-
  • 大小: 6.6 KB
分享到:
评论

相关推荐

    java线程详解

    Java线程:volatile关键字 Java线程:新特征-线程池 一、固定大小的线程池 二、单任务线程池 三、可变尺寸的线程池 四、延迟连接池 五、单任务延迟连接池 六、自定义线程池 Java线程:新特征-有返回值的线程...

    java并发编程理论基础精讲

    本资源为您提供了关于 Java 并发编程理论基础的精讲,涵盖了多线程编程的核心概念、基本原理以及在 Java 中的应用。通过深入学习,您将建立坚实的并发编程基础,能够更好地理解和应对多线程编程中的挑战。 并发编程...

    各种java面试大全,包含nginx、mysql、redis、spring各种基础

    hashmap实现原理,java基础笔记,java基础面试全集,java里的volatile关键字详解,jvm垃圾回收,MySQL索引背后的数据结构及算法原理,MySQL性能优化的最佳21条经验,MySQL中的锁(表锁、行锁),Redis的优点和5种...

    Java-Interview:此项目为 Java 面试的汇总,多数是一些 Java 基础知识、底层原理、算法详解。也有上层应用设计,其中不乏一些大厂面试真题

    synchronize 关键字原理 多线程的三大核心 对锁的一些认知 ReentrantLock 实现原理 ConcurrentHashMap 的实现原理 线程池原理 深入理解线程通信 交替打印奇偶数 JVM Java 运行时内存划分 类加载机制 OOM 分析 垃圾...

    Java-Interview:https

    synchronize 关键字原理 多线程的三大核心 对锁的一些认知 ReentrantLock 实现原理 ConcurrentHashMap 的实现原理 线程池原理 深入理解线程通信 交替打印奇偶数 JVM Java 运行时内存划分 类加载机制 OOM 分析 垃圾...

    java-interview

    synchronize 关键字原理 多线程的三大核心 对锁的一些认知 ReentrantLock 实现原理 ConcurrentHashMap 的实现原理 线程池原理 深入理解线程通信 交替打印奇偶数 JVM Java 运行时内存划分 类加载机制 OOM 分析 垃圾...

    Java基础知识点总结.docx

    无论是工作学习,不断的总结是必不可少的。只有不断的总结,发现问题,弥补不足,才能长久的进步!!Java学习更是如此,知识点总结目录如下: ...volatile详解 337 Java 8新特性 347 Java 性能优化 362

    整理后java开发全套达内学习笔记(含练习)

    volatile (关键字) 不稳定的['vɒlәtail] while (关键字) 循环语句。 当...的时候 [hwail] ORACLE_SID=oral10g\ --变局部变量 export ORACLE_SID --变全局变量 unset ORACLE_SID --卸载环境变量 ORACLE_HOME=...

    93个netty高并发教学视频下载.txt

    83_AtomicIntegerFieldUpdater实例演练与volatile关键字分析;84_Netty引用计数注意事项与内存泄露检测方式;85_Netty编解码器剖析与入站出站处理器详解;86_Netty自定义编解码器与TCP粘包拆包问题;87_Netty编解码...

    精通并发与 netty 视频教程(2018)视频教程

    Netty引用计数的实现机制与自旋锁的使用技巧 82_Netty引用计数原子更新揭秘与AtomicIntegerFieldUpdater深度剖析 83_AtomicIntegerFieldUpdater实例演练与volatile关键字分析 84_Netty引用计数注意事项与内存泄露...

    JAVA核心知识点整理(有效)

    25 JAVA8 与元数据.................................................................................................................................25 2.4. 垃圾回收与算法 .................................

    精通并发与netty视频教程(2018)视频教程

    83_AtomicIntegerFieldUpdater实例演练与volatile关键字分析 84_Netty引用计数注意事项与内存泄露检测方式 85_Netty编解码器剖析与入站出站处理器详解 86_Netty自定义编解码器与TCP粘包拆包问题 87_Netty编解码器...

    java面试题,180多页,绝对良心制作,欢迎点评,涵盖各种知识点,排版优美,阅读舒心

    【JVM】JAVA编译原理和JVM原理 42 【JVM】Java内存模型 44 【JVM】jvm内存模型 45 主内存与工作内存 45 内存间交互操作 46 重排序 48 【JVM】内存泄漏 49 【JVM】java虚拟机的区域如何划分,每一个区的动能? 49 ...

    精通并发与netty 无加密视频

    第83讲:AtomicIntegerFieldUpdater实例演练与volatile关键字分析 第84讲:Netty引用计数注意事项与内存泄露检测方式 第85讲:Netty编解码器剖析与入站出站处理器详解 第86讲:Netty自定义编解码器与TCP粘包拆包...

    汪文君高并发编程实战视频资源下载.txt

    │ 高并发编程第二阶段10讲、volatile关键字深入详解.mp4 │ 高并发编程第二阶段11讲、volatile关键字总结.mp4 │ 高并发编程第二阶段12讲、观察者设计模式介绍.mp4 │ 高并发编程第二阶段13讲、使用观察者设计...

    汪文君高并发编程实战视频资源全集

    │ 高并发编程第二阶段10讲、volatile关键字深入详解.mp4 │ 高并发编程第二阶段11讲、volatile关键字总结.mp4 │ 高并发编程第二阶段12讲、观察者设计模式介绍.mp4 │ 高并发编程第二阶段13讲、使用观察者设计...

Global site tag (gtag.js) - Google Analytics