死磕Java——volatile的理解

1、死磕Java——volatile的理解

1.1.JMM内存模型

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

JMM有关同步的规定:缓存

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

关于上述规定以下图解:安全

image-20190502153724126

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

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

1.2.Volatile关键字

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

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

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前缀的汇编指令主要有如下两个做用:this

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

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

参考:wiki.openjdk.java.net/display/Hot…spa

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

总结: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;
    }
}
复制代码
相关文章
相关标签/搜索