谈一谈你对volatile的理解

谈谈你对volatile的理解

1 什么是volatile

​ 首先volatile是Java虚拟机提供的轻量级的同步机制,相当于于一个轻量级的锁,其有如下的特点:

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

2 volatile特点的解析

2.1 可见性

​ 说起可见性,我们就需要了解到一个概念:JMM模型

1 为什么存在JMM?

​ 在早期,计算机的cpu的计算速度和内存的存取速度是差不多,但是到了现在cpu的运算速度高于内存的存取速度好几个数量级,因此为了整体的运行速率,我们不得不加上一层读写速度尽可能接近处理器的运算速度的高速缓存,作为内存和cpu之间的缓冲。

  • 将需要运算的数据复制到高速缓存中,让cpu运算快速进行,无序等待内存的读写。

这种方式缓和了处理器和内存的速率的差异,但是带来了新的问题:缓存一致性。在实际过程中,每个处理器都有自己的高速缓存,他们又共享同一个主内存,模型图如下所示。

在这里插入图片描述

问题1:多个处理器的运算任务都涉及到同一块主内存区域的时候,将可能导致各自的缓存不一致,如何解决?

答案:MESI(Inter缓存一致性协议)

​ 当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从主内存重新读取。

问题2:cpu是如何发现数据失效?

答案:总线嗅探

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

缺点:可能会出现总线风暴

​ 由于Volatile的MESI缓存一致性协议,需要不断的进行总线嗅探和cas不断循环,无效交互会导致总线带宽达到峰值。所以不要大量使用Volatile,至于什么时候去使用Volatile什么时候使用锁,根据场景区分,接下来需要说到JMM模型。

​ JMM(Java内存模型Java Memory Model,简称JMM)本身是一种抽象的概念,并不真实存在,它描述的是一组规则或规范。通过规范定制了程序中各个变量(线程共享变量)的访问方式,以及如何将变量存储到内存中的细节问题。

首先是变量拷贝和存储的过程:

​ 由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称之为栈空间),工作内存是每个线程的私有数据区域,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域(方法区和堆),所有线程都可访问。

​ 但线程对变量的操作(读取,修改等)必须在工作内存中进行,因此首先要将变量从主内存拷贝到自己的工作内存,然后对变量进行操作,操作完成再将变量写回主内存,而不是直接操作主内存中的变量。

​ 因为不同的线程无法访问对方的工作内存,多线程间的通讯(传值) 必须通过主内存来完成,其简要访问过程如下图:
在这里插入图片描述

​ 这样的机制也导致了可见性问题的出现,因此我们需要解决可见性的问题。

2 可见性

​ 通过前面对JMM的介绍,我们知道各个线程对主内存中共享变量的操作都是各个线程各自拷贝变量到自己的工作内存操作后,再写回主内存中的。这就可能存在一个线程AAA修改了共享变量X的值还未写回主内存中时,另外一个线程BBB又对内存中的一个共享变量X进行操作,但此时A线程工作内存中的共享变量的值,对线程B来说是不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题。

既然出现了可见性的问题那么如何解决?

1)加锁

那么为什么加锁可以解决可见性的问题?

源自于jmm模型对于同步的规定:

  • 线程加锁前,必须读取主内存的最新值到自己的工作内存。

  • 线程解锁前,必须把共享变量的值刷新回主内存。

  • 加锁和解锁是同一把锁。

这样就导致获取不到锁的线程就会阻塞等待,所以变量的值永远就是最新的。

2)volatile修饰共享变量

​ 每个线程操作共享数据的时候会重新去主内存拉取共享变量的值到自己的工作内存,如果他操作了数据并且写回到主内存,其他已经读取了该变量的线程的变量副本就会失效了,需要从新去主内存去读取共享变量的值。

2.2 原子性

number++在多线程下是非线程安全的,如何保证原子性?

  • 加锁:ReentrainLock或者Synchronized
  • 原子类:AtomicInteger等原子类进行操作。

VolatileDemo:发现volatile无法保证原子性

package com.at;

/**
 * @author : code1997
 * @date :2020-09-2020/9/21 15:23
 */
class ShareResource{
    private volatile int num=0;

    public int getNum() {
        return num;
    }

    public void increase(){
        num++;
    }

}
public class VolatileDemo {
    public static void main(String[] args) throws InterruptedException {
        ShareResource shareResource = new ShareResource();
        for (int i = 1; i <= 20000; i++) {
            new Thread(()->{
                shareResource.increase();
            }).start();
        }
        //保证上面线程执行结束
        Thread.sleep(5000);
        System.out.println(shareResource.getNum());
    }
}

结果:并不等于20000

2.3 有序性

1 什么是指令重排?

​ 计算机在执行程序时,为了提高性能,编译器和处理器常常会做指令重排。一般分为以下三种。

在这里插入图片描述

  • 编译器优化的重排序:单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致。
  • 指令级并行重排序:处理器在进行重新排序是必须要考虑指令之间的数据依赖性。多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程使用的变量能否保持一致性是无法确定的,结果无法预测。
  • 内存系统的重排序:由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行的。

2 volatile如何保证不会被执行重排序?

​ 内存屏障的技术来保证指令不会发生重排的现象,当我们使用volatile关键字修饰变量的时候,编译器会在生成指令时的适当位置插入内存屏障来禁止指令的重排序。

3 你在哪些地方用到过volatile?

3.1 单例模式DCL代码

public class Hungry {
    //1.构造器私有化
    private Hungry(){
    }
    //2.属性私有化
    private static volatile Hungry hungry;

    //3.静态方法,获取对象
    public synchronized static Hungry getInstance(){
        if (hungry==null){
            //4.保证一致性
            synchronized(Hungry.class){
                if (hungry==null){
                    hungry=new Hungry();
                }
            }
        }
        return hungry;
    }
}

3.2 单例模式volatile分析

1 大家可能好奇为啥要双重检查?如果不用Volatile会怎么样?

`对象实际上创建对象要经过如下几个步骤:

  • 分配内存空间。
  • 调用构造器,初始化实例。
  • 返回地址给引用。

​ 如果在没有volatile修饰的情况下是可能发生指令重排序的,那有可能构造函数在对象初始化完成前就赋值完成了,在内存里面开辟了一片存储区域后直接返回内存的引用,这个时候还没真正的初始化完对象。但是别的线程去判断instance!=null,直接拿去用了,其实这个对象是个半成品,那就有空指针异常了。因此加上Volatile来禁止指令重排序。

2 可见性得到保证

​ 如果没有可见性,线程A在自己的内存初始化了对象,还没来得及写回主内存,B线程也这么做了,那就创建了多个对象,不是真正意义上的单例了。

3 volatile与synchronized的区别?

  • volatile只能修饰实例变量和类变量;而synchronized可以修饰方法,以及代码块。
  • volatile保证数据的可见性,但是不保证原子性(多线程进行写操作,不保证线程安全);而synchronized是一种排他(互斥)的机制。 volatile用于禁止指令重排序:可以解决单例双重检查对象初始化代码执行乱序问题。
  • volatile可以看做是轻量版的synchronized,volatile不保证原子性,但是如果是对一个共享变量进行多个线程的赋值的操作,而没有其他的操作,那么就可以用volatile来代替synchronized,因为赋值本身是有原子性的,而volatile又保证了可见性,所以就可以保证线程安全了。

4 总结

  1. volatile修饰符适用于以下场景:
    1. 某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag。
    2. 作为触发器,实现轻量级同步。
  2. volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁_上,所以说它是低成本的,效率较高。
  3. volatile只能作用于属性,我们用volatile修饰属性,这样编译器就不会对这个属性做指令重排序。
  4. volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见,volatile属性不会被线程缓存,始终从主内存中读取。
  5. volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。

锁和释放锁_上,所以说它是低成本的,效率较高。
3. volatile只能作用于属性,我们用volatile修饰属性,这样编译器就不会对这个属性做指令重排序。
4. volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见,volatile属性不会被线程缓存,始终从主内存中读取。
6. volatile可以在单例双重检查中实现可见性和禁止指令重排序,从而保证安全性。

参考文档:https://zhuanlan.zhihu.com/p/137193948

©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页