关于Java多线程编程学习笔记之volatile

Java内存模型

  java中的堆内存是用来存储实例化的对象,它在虚拟机启动的时候创建,在Java虚拟机规范中规定堆内存是所有对象实例以及数组都在堆内存中进行分配内存。堆内存是被所有的线程共享的内存区域,因此存在内存可见性的问题,但是局部变量,方法定义的参数则不会在线程之间共享,他们不存在内存可见性的问题,也不受Java内存模型的影响。Java内存模型定义了线程和主存之间的抽象关系。线程之间的共享变量存储在主存中,每个线程都有一个私有的本地内存。该本地内存中存储了该线程共享变量的副本。这里有一点需要注意的是本地内存是Java内存模型中一个抽象概念,它并不是真实存在的,它涵盖了缓存,写缓冲区,寄存器等区域。Java内存模型控制线程之间的通信,它决定一个线程对主存共享变量的写入何时对其他的线程可见。

  线程A和线程B之间通信必须经过下面两个步骤

  1. 线程A把线程A本地内存中更新过的变量刷新到主存中。
  2. 线程B从主存中读取线程A以及更新过的变量。

    《关于Java多线程编程学习笔记之volatile》 内存模型

举个例子

int temp=10;

  如果temp 是一个线程共享变量,那么假如线程A对temp变量修改为2,那么第一步是线程A对在线程A本地内存中的temp变量的缓存进行赋值操作,然后再写入到主存中,不是直接将数字2写入到主存去的。

原子性 可见性 有序性

  在多进程(线程)访问共享资源时,能够确保所有其他的进程(线程)都不在同一时间内访问相同的资源。原子操作(atomic operation)是不需要synchronized,所谓原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。通常所说的原子操作包括对非long和double型的数据类型进行赋值,以及返回这两者之外的数据类型。之所以要把它们排除在外是因为它们都比较大,而JVM的设计规范又没有要求读操作和赋值操作必须是原子操作(JVM可以试着去这么作,但并不保证)。
首先处理器会自动保证基本的内存操作的原子性。处理器保证从系统内存当中读取或者写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他处理器不能访问这个字节的内存地址。奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操作是原子的,但是复杂的内存操作处理器不能自动保证其原子性,比如跨总线宽度,跨多个缓存行,跨页表的访问。但是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性

  简单的来说就是对基本数据类型的读取和赋值操作是原子性操作,也即是说这些操作是不可以中断的,要不是执行完毕,要不就是不执行,不会出现执行一半,停止,中断这种情况。下面举个例子。

x=10;  //语句1
y=x;   //语句2
x++;   //语句3

上面三条语句其中只有语句1是原子性操作,其中语句2和语句3都不是原子性操作。为什么呢?因为虽然他们都是一条语句,但是其中语句2执行的操作首先是取出X的值,然后将x的值写入工作内存中赋值给y。这个两个操作如果单独拿出来都是原子性操作。语句3 包含了三个操作,读取X的值,然后对X的值进行+1,然后再向工作内存中写入新的值。

可见性

  可见性,是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到。当一个共享变量被volatile修饰时,它会保证修改的值立即被更新到主存,所以对其他线程是可见的。当有其他线程需要读取该值时,其他线程会去主存中读取新值。而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,并不会立即写入主存中,什么时候写入主存也是不确定的,当其它线程去读取该值的时候,这个时候主存中的值仍然是原来的值,这样就无法保证可见性。

有序性

  Java内存模型中允许编译器和处理器对指令进行重排序,虽然重排序过程不会影响单线程执行的正确性,但是会影响多线程并发执行的正确性,我们可以通过volatile来保证有序性。也可以通过synchronized来保证有序性。synchronized可以保证每个时刻只有一个线程执行同步代码。这相当于是让线程执行同步代码,从而保证有序性。当一个共享变量被volatile修饰之后,其就具备了两个含义,一个是线程修改了变量的值时,变量的新值对其他线程是立即可见的。换句话说,就是不同线程对这个变量进行操作时具有可见性。另一个含义是禁止使用指令重排序 什么是重排序呢?重排序通常是编译器或者运行时环境为了优化程序性能而采取的对指令进行重排序执行的一种手段。重排序分为两类:编译期重排序和运行期重排序,分别对应编译时和运行时环境。

  volatile关键字能禁止指令重排序,因此volatile能保证有序性。volatile关键字禁止指令重排序有两个含义:一个是当程序执行到volatile变量的操作时,在其前面的操作已经全部执行完毕,并且结果会对后面的操作可见,在其后面的操作还没有进行;在进行指令优化时,在volatile变量之前的语句不能在volatile变量后面执行;同样,在volatile变量之后的语句也不能在volatile变量前面执行。
我们平时在开发中经常会使用到单例模式,很多人喜欢用double check 模式

public class Instance {

    public static Instance instance;

    private Instance() {
    }

    public static Instance getInstance() {
        if (instance == null) {
            synchronized (Instance.class) {
                if (instance == null) {
                    instance = new Instance();
                }
            }
        }
        return instance;
    }
}

  我们这样写的单例模式其实是有问题的,并不能真正的实现单例模式,假如线程A首先进入getInstance()方法,发现instance 对象为空,那么首先获取锁,这个时候如果线程B也执行getInstance()方法,发现instance对象仍然为空,那么线程B将会尝试获取锁对象,发现锁被线程A持有,这个时候线程B进入阻塞状态。在线程A完成了初始化的时候,释放锁对象,这个时候线程B将会获取锁对象,前面我说过了Java中的内存模型,线程A初始化的instance对象首先是在线程A的本地内存中,这个时候如果线程B在进行第二次判断instance对象是否为空的时候发现线程B和主内存中的instance对象为空,那么线程B也会进行初始化操作。所以这样的double check 模式并不能保证单例模式。正确的写法因该是下面这种写法

public class Instance {
    public static volatile Instance instance;
    private Instance() {
    }
    
    public static Instance getInstance() {
        if (instance == null) {
            synchronized (Instance.class) {
                if (instance == null) {
                    instance = new Instance();
                }
            }
        }
        return instance;
    }
}

  通过给instance 变量 加上volatile声明过后,一旦线程A初始化成功Instance类的对象过后,会马上把对象从线程A本地内存中刷入主内存中,并且对线程B可见,这样就避免了线程B又初始化一次对象。

    原文作者:huangandroid
    原文地址: https://www.jianshu.com/p/c5da3aaabb28
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞