Java并发关键字Volatile 详解

Java并发关键字Volatile 详解


  • 问题引出:

    1.Volatile是什么?

    2.Volatile有哪些特性?

    3.Volatile每个特性的底层实现原理是什么?

  • 相关内容补充:

  1. 缓存一致性协议:MESI

    ? 由于计算机储存设备(硬盘等)的读写速度和CPU的计算速度有着几个数量级别的差距,为了不让CPU停下来等待读写,在CPU和存储设备之间加了高速缓存,每个CPU都有自己的高速缓存,而且他们共享同一个主内存区域,当他们都要同步到主内存时,如果每个CPU缓存里的数据都不一样,这时应该以哪个数据为准呢?为了解决这一同步问题,需要各个处理器都遵循一定的协议,比如MSI,MOSI,MESI等,目前用的比较多的就是MESI协议。

? 注 :缓存一致性协议是在总线上实现的。

#### MESI是代表了缓存数据的四种状态,分别是Modified、Exclusive、Shared、Invalid:

? ①M(Modified):被修改的,处于这一状态的数据,只在本CPU中有缓存数据,而其他CPU中没 有。同时其状态相对于内存中的值来说,是已经被修改的,且没有更新到内存中。

? ②E(Exclusive):独占的,处于这一状态的数据,只有在本CPU中有缓存,且其数据没有修改, 即与内存中一致。

? ③S(Shared):共享的。处于这一状态的数据在多个CPU中都有缓存,且与内存一致。

? ④I(Invalid):要么已经不在缓存中,要么它的内容已经过时。为了达到缓存的目的,这种状态 的段将会被忽略。一旦缓存段被标记为失效,那效果就等同于它从来没被加载到缓存中。在 缓存行中有这四种状态的基础上,

#### 总结:每个处理器通过嗅探在总线上传递的数据来检查自己缓存的数据是否过期,当处理器发现自己缓存行数据对应的内存地址被修改,就会将当前缓存行里的数据设置为无效。当再次需要使用该数据的时候就会去主内存中重新读取数据。

  1. Java内存模型:JMM?

    ? Java内存模型规定了Java变量(实例字段,静态字段,构成数组对象的元素等,但不包括局部变量和方法参数)存储到内存和从内存中取出的的底层实现细节,这些变量都存储在主(Main Memory)中,(主内存只是虚拟机内存的一部分) 每个线程都有自己的工作内存(Working Memory),工作内存(实际上工作内存并不存在,他只是JMM抽象出来的一个概念)中保存着从主内存读取来的变量副本拷贝,线程对副本的操作(读取,修改,赋值)都要在工作内存中进行,而不能直接在主内存中进行,不同线程之间也不能访问彼此的工作内存。且线程之间变量值的传递需要经过主内存作为第三方中介。

?

  1. 内存间原子性交互操作:

    ①lock(锁定):作用于主内存上的变量,当一个变量被标识为Lock的时候,表示该变量是线程 独占状态,此时其他线程不可以对该变量进行操作。早前的缓存一致性协议就是这样,但是这 样会导致某个变量被一个线程占用,其他线程不可以对其进行访问,并发就变成了串行,效率 降低,在后来的缓存一致性协议中就抛弃了这种做法。

    ②unlock(解锁):同样是作用于主内存中的变量,使变量从锁定状态释放出来,其他线程才可 以对其操作。

    ③read(读取):读取主内存中的变量,传输到线程的工作内存,等待后续的load操作。

    ④load(加载):加载工作内存中的变量,把其放入工作内存的副本变量中。

    ⑤use(使用):把工作内存中的变量副本值传递给执行引擎,每当虚拟机遇到使用变量值字节码 的时候就会进行此操作。

    ⑥assign(赋值):把一个从执行引擎接收到的数据赋给工作内存的变量,即执行赋值操作。

    ⑦store(存储):把工作内存中经过赋值更新后的值传递到主内存中,为后续write做准备。

    ⑧write(写入):把store操作传递来的值写入主内存,替换之前的值,完成同步更新。

? 4.JMM并发的特性要求:

? ①可见性(Visibility):可见性要求是指当一个线程修改了共享变量的值以后,其他线程能够马 上得知这个修改。

? ②原子性(Atomicity):原子性是指对变量的操作(read,load,assign等上述交互操作)不 可分割不可被打断,每个操作都要完整的执行完成才可以有其他操作进来。且默认对基本数据类 型的访问和读写都是原子性的(64位的long型和double型会有可能被拆分成两个32位进行读写 操作,但是这种概率极低,可以忽略不计。)

? ③有序性:为了提升效率,编译器会对代码进行乱序优化,而CPU会乱序执行,但是这样的操作 会导致很严重的问题。为了解决这一问题,使用了内存屏障来防止乱序的发生。 这样按照顺序执 行就是有序性。



? 进入主题


  • Volatile是什么?

    Volatile是轻量级的synchronized锁,所谓轻量级,是因为synchronized使用时会引起线程的上下文切换,使得执行成本更高,效率更低,而Volatile不会有这些问题,效率更高。

  • Volatile特性:

    1.可见性 :Visibility

    ? ①定义:当一个线程修改了共享变量的值以后,其他线程能够马上得知这个修改。

    ? ②先看一个例子:

    package Test;
    
    public class VolatileTest {
        public static boolean flag = false;
        public static void main(String[] args) throws InterruptedException {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    System.out.println("主线程等待线程A修改数据~~~");
                    while (!flag){}
                    System.out.println("主线程发现数据被线程A修改~~~~");
                }
            }).start();
            Thread.sleep(300);
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    changeData();
                }
            }).start();
    
        }
    
        public static void changeData(){
             flag = true;
            System.out.println("线程A修改完成数据~~~~");
        }
    
    }
    // 执行结果:
    等待线程线程A修改数据~~~
    线程A修改完成数据~~~~
    

    ? 可以看出,当线程A修改完成数据后,另外一个线程应该要输出主线程发现数据被线程A修改~~~~,但是实际的运行情况是主线程一直处于等待状态。而如果把public static boolean flag = false;修改为public static volatile boolean flag = false;,也就是把变量用volatile修饰,此时的执行结果:

    等待线程线程A修改数据~~~
    线程A修改完成数据~~~~
    线程A修改数据完成~~~~

    很明显,线程A修改变量后,主线程也能感知到,使得数据具有可见性,这就是volatile的作用。

    ? ③volatile可见性底层实现原理:

    ? 对未加volatile修饰的变量修改时的底层汇编码:

? 对volatile修饰的变量修改时的底层汇编码:

? 由底层汇编可知,对volatile修饰的变量修改时,汇编指令前面会多一个lock前缀,这个lock 前缀将会导致下面两件事发生:

? (1)立即将修改过的数据回写到主内存中,刷新原来的数据。

在Pentium及Pentium之前的处理器中,带有lock前缀的指令在执行期间会锁住总线,使得其他处理器暂时无法通过总线访问内存。很显然,这会带来昂贵的开销。从Pentium 4,Intel Xeon及P6处理器开始,intel在原有总线锁的基础上做了一个很有意义的优化:如果要访问的内存区域(area of memory)在lock前缀指令执行期间已经在处理器内部的缓存中被锁定(即包含该内存区域的缓存行当前处于独占或以修改状态),并且该内存区域被完全包含在单个缓存行(cache line)中,那么处理器将直接执行该指令。由于在指令执行期间该缓存行会一直被锁定,其它处理器无法读/写该指令要访问的内存区域,因此能保证指令执行的原子性。这个操作过程叫做缓存锁定(cache locking),缓存锁定将大大降低lock前缀指令的执行开销,但是当多处理器之间的竞争程度很高或者指令访问的内存地址未对齐时,仍然会锁住总线。
ps:摘自https://blog.csdn.net/yu280265067/article/details/50986947

?

? (2)如果其他处理器缓存了这个被修改过的数据,那回写操作会使他们失效。

IA-32处理器和Intel64位处理器使用MESI(缓存一致性)维护内部缓存和其他处理器缓存的一致性,在多核处理器(多线程)中,处理器和线程使用嗅探技术检测各自缓存中的数据和总线上传递的数据是否一致,如果检测到有其他处理器或线程回写数据,且该数据是共享数据,那么就会强制使其他缓存了该数据的缓存中的数据失效。

2.有序性(禁止指令重排序):Odering

? (1)指令重排序:为了优化和性能,编译器和处理器经常会对指令做重排序,且分为三种。

? ①编译器重排序:在不改变单线程程序语义的前提下,重新安排代码执行顺序。

? ②指令级并行重排序:处理器采用指令级并行技术将多条指令重叠执行,如果数据不存在 依赖,可以改变机器指令执行。

? ③内存系统重排序:处理器使用缓存和读/写缓冲区,使得加载和存储看上去是乱序执行。

? 重排序顺序示意图

?

? 先看一个例子:

package Test;

public class NoReoder {
    private static int a = 0;
    private static boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(new Runnable() {
            @Override
            public void run() {
                write();
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                read();
            }
        }).start();
    }

    public static void write(){
        a = 30;
        flag = true;
        System.out.println("方法一结束~~");
    }

    public static void read(){
        if (flag ) {
            a = a + 10;
            System.out.println(a);
            System.out.println("方法二结束~~");
        }
    }
}

? 当线程A启动调用了write方法,线程B启动调用read方法时能不能知道a被write方法修改了呢?答案是:不一定!!!

由于write方法中:

a = 30;
flag = true;

这两个操作的数据没有依赖性,所以可能会被重排序为:

flag = true;
a = 30;

这样就会使得read方法先读到flag = true ,而 a 还没修改完,从而使计算结果出错。

为了解决这种问题,在JMM中设计了内存屏障技术:

简单来说,Volatile的有序性就是靠内存屏障来实现,就是把一些操作限制在某些操作之前或者之后,比如将Store操作限制在Load之前,这样就能让其他线程得到的数据是最新的或者需要先写入数据再让其他线程加载数据。


说在最后:

            相关参考,详见《Java并发编程的艺术》一书

? 本文仅是对个人学习中一些理解的记录,鉴于水平有限或多或少存在错漏或不严谨之处,欢迎各位大神批评指正。码字不易,欢迎转载转发但请标注出处。

? 希望病毒早点结束,再难的日子里也要坚持学习,新年快乐,最后愿工作在与病毒抗争最前线的医护人员平安打完这场仗,加油!!!!

原文地址:https://www.cnblogs.com/coding-996/p/12233175.html

时间: 2024-10-28 07:33:36

Java并发关键字Volatile 详解的相关文章

Java并发编程--Volatile详解

摘要 Volatile是Java提供的一种弱同步机制,当一个变量被声明成volatile类型后编译器不会将该变量的操作与其他内存操作进行重排序.在某些场景下使用volatile代替锁可以减少代码量和使代码更易阅读.   Volatile特性 1.可见性:当一条线程对volatile变量进行了修改操作时,其他线程能立即知道修改的值,即当读取一个volatile变量时总是返回最近一次写入的值 2.原子性:对于单个voatile变量其具有原子性(能保证long double类型的变量具有原子性),但对

Java 并发 关键字volatile

Java 并发 关键字volatile @author ixenos volatile只是保证了共享变量的可见性,不保证同步操作的原子性 同步块 和 volatile 关键字机制 synchronized  同步块大家都比较熟悉,通过 synchronized 关键字来实现,所有加上synchronized 和 块语句,在多线程访问的时候,同一时刻只能有一个线程能够用synchronized 修饰的方法 或者 代码块. volatile 用volatile修饰的变量,线程在每次刚使用变量的时候,

Java关键字synchronized详解

Java关键字synchronized详解 博客分类: Java综合 Java多线程thread互联网制造 synchronized 关键字,代表这个方法加锁,相当于不管哪一个线程A每次运行到这个方法时,都要检查有没有其它正在用这个方法的线程B(或者C D等),有的话要等正在使用这个方法的线程B(或者C D)运行完这个方法后再运行此线程A,没有的话,直接运行 它包括两种用法:synchronized 方法和 synchronized 块. 1. synchronized 方法: 通过在方法声明中

【夯实基础】java关键字synchronized 详解

尊重版权:http://www.cnblogs.com/GnagWang/archive/2011/02/27/1966606.html Java语言的关键字,当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码. 一.当两个并发线程访问同一个对象object中的这个synchronized(this)同步代码块时,一个时间内只能有一个线程得到执行.另一个线程必须等待当前线程执行完这个代码块以后才能执行该代码块. 二.然而,当一个线程访问object的一个sy

Java下static关键字用法详解

Java下static关键字用法详解 本文章介绍了java下static关键字的用法,大部分内容摘自原作者,在此学习并分享给大家. Static关键字可以修饰什么? 从以下测试可以看出, static 可以修饰: 1. 语句块 2. 成员变量(但是不能修饰局部变量) 3. 方法 4. 接口(内部接口) 5. 类(只能修饰在类中的类, 即静态内部类) 6. jdk 1.5 中新增的静态导入 那么static 修饰的表示什么呢? 当创建一个类时,就是在创建一个新类型,描述这个类的对象的外观和行为,除

黑马-----内存模型和volatile详解

黑马程序员:Java培训.Android培训.iOS培训..Net培训 JAVA线程-内存模型和volatile详解 一.单核内存模型 1.程序运行时,将临时数据存放到Cache中 2.将CPU计算所需要的数据从Cache中拷贝一份到H Cache中 3.CPU直接从H Cache中读取数据进行计算 4.CPU将计算的结果写入H Cache中 5.H Cache将最新的结果值涮入Cache中(何时写入不确定) 6.将Cache中结果数据写回程序(如果有需要,例如文件.数据库) 需要H Cache

Java内存模型(JMM)详解

在Java JVM系列文章中有朋友问为什么要JVM,Java虚拟机不是已经帮我们处理好了么?同样,学习Java内存模型也有同样的问题,为什么要学习Java内存模型.它们的答案是一致的:能够让我们更好的理解底层原理,写出更高效的代码. 就Java内存模型而言,它是深入了解Java并发编程的先决条件.对于后续多线程中的线程安全.同步异步处理等更是大有裨益. 硬件内存架构 在学习Java内存模型之前,先了解一下计算机硬件内存模型.我们多知道处理器与计算机存储设备运算速度有几个数量级的差别.总不能让处理

java中static作用详解

static表示“全局”或者“静态”的意思,用来修饰成员变量和成员方法,也可以形成静态static代码块,但是Java语言中没有全局变量的概念. 被static修饰的成员变量和成员方法独立于该类的任何对象.也就是说,它不依赖类特定的实例,被类的所有实例共享. 只要这个类被加载,Java虚拟机就能根据类名在运行时数据区的方法区内定找到他们.因此,static对象可以在它的任何对象创建之前访问,无需引用任何对象. 用public修饰的static成员变量和成员方法本质是全局变量和全局方法,当声明它类

ava下static关键字用法详解

Java下static关键字用法详解 本文章介绍了java下static关键字的用法,大部分内容摘自原作者,在此学习并分享给大家. Static关键字可以修饰什么? 从以下测试可以看出, static 可以修饰: 1. 语句块 2. 成员变量(但是不能修饰局部变量) 3. 方法 4. 接口(内部接口) 5. 类(只能修饰在类中的类, 即静态内部类) 6. jdk 1.5 中新增的静态导入 那么static 修饰的表示什么呢? 当创建一个类时,就是在创建一个新类型,描述这个类的对象的外观和行为,除