死磕Java——volatile的理解

一、死磕Java——volatile的理解

1.1.JMM内存模型

理解volatile的相关知识前,先简单的认识一下JMM(Java Memory Model),JMMjdk5引入的一种jvm的一种规范,本身是一种抽象的概念,并不真实存在,它屏蔽了各种硬件和操作系统的访问差异,它的目的是为了解决由于多线程通过共享数据进行通信时,存在的本地内存数据不一致、编译器会对代码进行指令重排等问题。

JMM有关同步的规定:

  • 线程解锁前,必须把共享变量的值刷新回主内存;
  • 线程加锁前,必须读取主内存的最新值到自己的工作内存中;
  • 加锁和解锁使用的是同一把锁;

关于上述规定如下图解:

image-20190502153724126

说明:当我们在程序中new一个user对象的时候,这个对象就存在我们的主内存中,当多个线程操作主内存的name变量的时候,会先将user对象中的name属性进行拷贝一份到自己线程的工作内存中,自己修改自己工作内存中的属性后,再将修改后的属性值刷新回主内存,这就会存在一些问题,例如,一个线程写完,还没有写回到主内存,另一个线程先修改后写入到主内存,就会存在数据的丢失或者脏数据。所以,JMM就存在如下规定:

  • 可见性
  • 原子性
  • 有序性

1.2.Volatile关键字

volatilejava虚拟机提供的一种轻量级的同步机制,比较与synchronized。我们知道的事volatile的三大特性:

  • 可见性
  • 不保证原子性
  • 禁止指令重排

1.2.1.Volatile如何保证可见性

可见性就是当多个线程操作主内存的共享数据的时候,当其中一个线程修改了数据写回主内存的时候,回立刻通知其他线程,这就是线程的可见性。先看一个简单的例子:

class MyDataDemo {
    int num = 0;

    public void updateNum() {
        this.num = 60;
    }
}

public class VolatileDemo {

    public static void main(String[] args) {

        MyDataDemo myData = new MyDataDemo();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            myData.updateNum();
            System.out.println("num的值:" + myData.num);
        }, "子线程").start();

        while (myData.num == 0) {}
        System.out.println("程序执行结束");
    }
}

这是一个简单的示例程序,存在一个两个线程,一个子线程修改主内存的共享数据num的值,main线程使用while时时检测自己是否是道主内存的num的值是否被改变,运行程序程序执行结束并不会被打印,同时,程序也不会停止。这就是线程之间的不可见问题,解决方法就是可以添加volatile关键字,修改如下:

volatile int num = 0;

1.2.2.Volatile保证可见性的原理

Java程序生成汇编代码的时候,我们可以看见,当我们对添加了volatile关键字修饰的变量时候,会多出一条Lock前缀的的指令。我们知道的是cpu不直接与主内存进行数据交换,中间存在一个高速缓存区域,通常是一级缓存、二级缓存和三级缓存,而添加了volatile关键字进行操作时候,生成的Lock前缀的汇编指令主要有以下两个作用:

  • 将当前处理器缓存行的数据写回系统内存;
  • 这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效;

Idea查看程序的汇编指令在VM启动参数配上-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly即可;

参考:https://wiki.openjdk.java.net/display/HotSpot/PrintAssembly

在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

总结:Volatile通过缓存一致性保证可见性。

1.2.3.Volatile不保证原子性

原子性:也可以说是保持数据的完整一致性,也就是说当某一个线程操作每一个业务的时候,不能被其他线程打断,不可以被分割操作,即整体一致性,要么同时成功,要么同时失败。

class MyDataDemo {
    volatile int num = 0;

    public void addNum() {
        num++;
    }
}
public class VolatileDemo {

    public static void main(String[] args) {
        MyDataDemo data = new MyDataDemo();
        for(int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j < 1000; j++) {
                    data.addNum();
                }
            }, "当前子线程为线程" + String.valueOf(i)).start();
        }
        // 等待所有线程执行结束
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("最终结果:" + data.num);
    }
}

上述代码就是在共享数据前添加了volatile关键字,当时,打印的最终结果几乎很难为20000,这就很充分的说明了volatile并不能保证数据的原子性,这里的num++操作,虽然只有一行代码,但是实际是三步操作,这也是为什么i++在多线程下是非线程安全的。

1.2.4.为什么Volatile不保证原子性

可以参考JMM模型的那一张图,就是主内存中存在一个num = 0,当其中一个线程将其修改为1,然后将其写回主内存的时候,就被挂起了,另外一个线程也将主内存的num = 0修改为1,然后写入后,之前的线程被唤醒,快速的写入主内存,覆盖了已经写入的1,造成了数据丢失操作,两次操作最终结果应该为2,但是为1,这就是为什么会造成数据丢失。再来看i++对应的字节码

image-20190502175617528

简单翻译一下字节码的操作:

  • aload_0:从局部变量表的相应位置装载一个对象引用到操作数栈的栈顶;
  • dup:复制栈顶元素;
  • getfield:先获得原始值;
  • iadd:进行+1操作;
  • putfield:再把累加后的值写回主内存操作;

1.2.5.解决Volatile不保证原子性的问题

使用AtomicInteger来保证原子性,有关AtomicInteger的详细知识,后面在死磕,官方文档截图如下:

image-20190502182016318

修改之前的不保证原子性的代码如下:

class MyDataDemo {
    AtomicInteger atomicInteger = new AtomicInteger();

    public void addAtomicInteger() {
        atomicInteger.getAndIncrement();
    }
}
public class VolatileDemo {

    public static void main(String[] args) {
        MyDataDemo data = new MyDataDemo();
        for(int i = 1; i <= 20; i++) {
            new Thread(() -> {
                for (int j = 1; j <= 1000; j++) {
                    data.addAtomicInteger();
                }
            }, "当前子线程为线程" + String.valueOf(i)).start();
        }
        // 等待所有线程执行结束
        while (Thread.activeCount() > 2) {
            Thread.yield();
        }
        System.out.println("最终结果:" + data.atomicInteger);
    }
}

1.2.6.Volatile的禁止指令重排序

首先,假如写了如下代码

carbon

在程序中,我们觉得是会依次顺序执行,但是在计算机在执行程序的时候,为了提高性能,编译器和和处理器通常会对指令进行指令重排序,可能执行顺序为:2—1—3—4,也可能是:1—3—2—4,一般分为下面三种:

image-20190502184808400

虽然处理器会对指令进行重排,但是同时也会遵守一些规则,例如上述代码不可能重排后将第四句代码第一个执行,所以,单线程下确保程序的最终执行结果和顺序执行结一致,这就是处理器在进行指令重排序时候必须考虑的就是指令之间的数据依赖性

但是,在多线程环境下,由于编译器重排的存在,两个线程使用的变量能否保证一致性无法确定,所以结果就无法一致。在看一个示例:

http://image.luokangyuan.com/2019-05-02-113323.png

在多线程环境下,第一种就是顺序执行init方法,先将num进行赋值操作,在执行update方法,结果:num为6,但是存在编译器重排,那么可能先执行falg = true;再执行num = 1;,最终num为5;

1.2.7.Volatile禁止指令重排序的原理

前面说到了volatile禁止指令重排优化,从而避免在多线程环境下出现结果错乱的现象。这是因为在volatile会在指令之间插入一条内存屏障指令,通过内存屏障指令告诉CPU和编译器不管什么指令,都不进行指令重新排序。也就说说通过插入的内存屏障禁止在内存屏障前后的指令执行指令重新排序优化

什么是内存屏障

内存屏障是一个CPU指令,他的作用有两个:

  • 保证特定操作的执行顺序;
  • 保证某些变量的内存可见性;

将上述代码修改为:

volatile int num = 0;

volatile boolean falg = false;

这样就保证执行init方法的时候一定是先执行num = 1;再执行falg = true;,就避免的了结果出错的现象。

1.3.Volatile的单例模式

public class SingletonDemo {

    private static volatile SingletonDemo instance = null;

    private SingletonDemo(){};

    public static SingletonDemo getInstance() {
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }
}
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354

推荐阅读更多精彩内容