JAVA JMM-volatile关键字的理解

volatile修饰的共享变量,就会具有以下两个特性:

1.保证内存可见性,也就是一个线程对共享变量修改,对于另一个线程立即可见。
主存:线程对共享变量的修改,会立即刷入主存
缓存:使其他缓存中该共享变量的引用失效,重新从主存中加载
2.禁止指令重排序。
防止JVM对非原子操作进行指令重排导致非预期结果

JMM-java内存模型-共享内存模型

在这里插入图片描述
虚拟机内存模型又叫JMM,就是每个线程有自己的工作内存,然后又一个主内存,线程工作的时候都是在自己的工作内存中拷贝一个主内存的副本;还说了JMM的happens before原则,程序顺序原则,锁原则,线程中断原则,传递性原则

JMM中的重排序
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。然而在程序最终执行之前,还要做一个内存的重排序。JMM指定了as-if-serial串行语义,也就是说,无论重新排序,程序的执行结果都不能更改,但是重排序可能会导致多线程程序出现内存可见性问题。
请看代码例子:

class ReorderEample {
        int a = 0;
        boolean flag = false;
        //写操作
        public void writer() {
            a = 1 ;  // (1)
            flag = true; //(2)
        }
        //读操作
        public void reader() {
            if (flag) {    //(3)
                int i = a * a;  //(4)
                // 处理逻辑
            }
        }
    }

flag变量是个标记,标识a是否已被写入。假设有两个线程A和B,A首先执行了写操作writer(),随后B接着执行读操作reader()方法。那么线程B在执行操作(4)时,是否能看到线程A在操作(1)时对共享变量a的写入?答案是未必能看到。因为在重排序时,A线程可能先标识了flag变量,再对a变量进行写入,但是在它们发生之间,B线程此时来读了,该程序的语义被破坏了。如下程序执行时序图:
在这里插入图片描述
volatile解决重排序问题

volatile遵循happens-before原则。请看如下代码:

class ReorderEample {
        int a = 0;
        volatile boolean flag = false;
        //写操作
        public void writer() {
            a = 1 ;  // (1)
            flag = true; //(2)
        }
        //读操作
        public void reader() {
            if (flag) {    //(3)
                int i = a * a;  //(4)
                // 处理逻辑
            }
        }
    }

假设线程A执行writer()方法之后,线程B执行reader()方法。根据happens-before原则,这个过程建立的happens-before关系分为3类:

根据程序次序规则,(1) happens-before (2) ; (3) happens-before (4)。

根据volatile规则,(2) happens-before (3).

根据happens-before的传递性规则,(1) happens-before (4)。
在这里插入图片描述
对于上一个例子来说,这个例子只对flag变量增加了volatile声明。A线程写入一个volatile变量后,B线程读同一个volatile变量。A线程在写volatile变量之前所有可见的共享变量,在B线程读同一个volatile变量后,立即对B线程可见。

happens-before规则知识点补充:

(1)程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作
(2)监视器锁规则:对一个线程的解锁,happens-before于随后对这个线程的加锁
(3)volatile变量规则:对一个volatile域的写,happens-before于后续对这个volatile域的读
(4)传递性:如果A happens-before B ,且 B happens-before C, 那么 A happens-before C
(5)start()规则:如果线程A执行操作ThreadB_start()(启动线程B) ,  那么A线程的ThreadB_start()happens-before 于B中的任意操作
(6)join()原则:如果A执行ThreadB.join()并且成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。
(7)interrupt()原则: 对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测是否有中断发生
(8)finalize()原则:一个对象的初始化完成先行发生于它的finalize()方法的开始。

synchronized的内存语义
synchronized内存语义与volatile内存语义类似,在Java并发编程机制中,锁除了让临界区互斥之外,还可以让释放锁的线程向获取同一个锁的线程发送消息。它的核心底层就是使用一个volatile声明的state变量来维护同步状态。

锁也遵循happens-before规则。请看如下代码:

 class MonitorExample{
        int a = 0;
        //写操作
        public synchronized void writer() { //(1)
            a ++;                           //(2)
        }                                   //(3)
        //读操作
        public synchronized void reader() { //(4)
            int i = a;                      //(5)
            //处理逻辑
        }                                   //(6)
    }

假设线程A执行writer()方法,随后线程B执行reader()方法。根据happens-before规则,这个过程包含的happens-before关系可以分为3类:

根据程序次序规则,(1) happens-before (2),(2) happens-before (3),(4) happens-before (5),(5) happens-before (6)。
2 .根据监视器锁规则,(3) happens-before (4)。

3 .根据传递性规则,(2) happens-before (5)。

其happens-before建立关系图如下:
在这里插入图片描述
线程A释放了锁之后,随后线程B获取同一个锁。因为 (2) happens-before (5),所以线程A在释放锁之前所有可见的共享变量在线程B获取同一个锁之后对于B线程都变得可见。

final的内存语义
在JMM中,通过内存屏障禁止编译器把final域的写重排序到构造函数之外。因此,在对象引用为任意线程可见之前,对象的final域已经被正确初始化(不为null的情况)了。
对于final域,编译器和处理器遵循两个重排序规则:

在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
初次读一个包含final域的对象的医用,与随后初次读这个final域,这两个操作之间不能重排序。
下面通过两个示例来说明这两个规则。

public class FinalExample {
    int i;                           //普通变量
    final int j;                     //final变量
    static FinalExample obj;
    public FinalExample(int j) {     //构造函数
        i = 1;                       //写普通域
        this.j = j;                  //写final域
    }
    
    public static void writer() {    //写线程A执行
        obj = new FinalExample(2);
    }
    
    public static void reader() {    //读线程B执行
        FinalExample object = obj;   //读引用对象
        int a = object.i;            //读普通域
        int b = object.j;            //读final域
    }
}

写普通域的操作被编译器重排序到了构造函数之外,读线程B错误地读取了普通变量i初始化之前的值。而写final域操作后,被写final域的重排序规则“限定”在了构造函数之内,读线程B正确地读取了final变量初始化的值。执行时序图如下
在这里插入图片描述
总结:
每个线程有自己的工作内存,工作内存中的数据并不会实时刷新回主内存,因此在并发情况下,有可能线程A已经修改了成员变量k的值,但是线程B并不能读取到线程A修改后的值,这是因为线程A的工作内存还没有被刷新回主内存,导致线程B无法读取到最新的值。

在工作内存中,每次使用volatile修饰的变量前都必须先从主内存刷新最新的值,这保证了当前线程能看见其他线程对volatile修饰的变量所做的修改后的值。

在工作内存中,每次修改volatile修饰的变量后都必须立刻同步回主内存中,这保证了其他线程可以看到自己对volatile修饰的变量所做的修改。

volatile修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同。

volatile保证可见性,不保证原子性,部分保证有序性(仅保证被volatile修饰的变量)。

指令重排序的目的是为了提高性能,指令重排序仅保证在单线程下不会改变最终的执行结果,但无法保证在多线程下的执行结果。

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止重排序。

发布了29 篇原创文章 · 获赞 0 · 访问量 1657

猜你喜欢

转载自blog.csdn.net/glamour2015/article/details/104217013
今日推荐