Java面试:【Volatile是什么】

JUC

  • Java.util.concurrent(并发)
  • Java.util.concurrent.atomic
  • Java.util.concurrent.locks
    并发针对资源,一起
    并行针对时间,一边一边

请谈谈你对volatile的理解

一、 volatile是Java虚拟机提供的轻量级的同步机制(synchronized)

主要有三大特性

  1. 保证可见性(变量更新后立刻通知其他线程)
  2. 不保证原子性(是不满足JMM的同步规范的)
  3. 禁止指令重排序(防止多线程下指令重排带来的影响,指令重排只会考虑单线程下的数据依赖关系

二、 volatile特性与JMM

  • JVM(Java虚拟机)
  • JMM(Java内存模型):是一种抽象概念,描述的是一组的规则和规范
    JMM对于同步的规定
  • 线程解锁前,必须把共享的变量的值刷新回主内存
  • 线程加锁前,必须读取主内存中的最新值到自己的工作内存
  • 加锁解锁是同一把锁
    1.可见性
    2.原子性
    3.有序性

由于JVM运行程序的实体是线程,而每一个线程创建时JVM 都会为其创建一个工作内存(有些地方也称为栈空间),工作内存就是每个线程的私有数据区域,而Java内存模型中规定所有的变量都存储在主内存,主内存是共享的内存区域,所有线程都可以访问,但是线程对变量的操作(读取和赋值等)都必须在工作内存中进行,首先要将变量从主内存拷贝到自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能够直接操作主内存中的变量,各线程的工作内存中存储着主内存中的变量副本拷贝,因此不同的线程之间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成。(协同工作维护同一组数据的原理)

可见性:某一个线程改动了数据之后,保证所有的线程都能第一时间知道自己的数据不是最新的

在变量前不加Volatile关键字时,线程做出的修改对于其他线程不可见
(就是简单方法,并不是加了sych...的同步方法)

原子性:不可分割、完整性、也即某一个线程正在做某一个具体业务时候,中间不可以被加塞或者被分割。需要整体完成,要么同时成功要么同时失败。

number++在多线程下不是线程安全的
n++在字节码中被拆分成三个指令:
执行getfield拿到原始的n
执行iadd进行加1操作(在各自的工作内存中)
执行putfield写,将结果值写回,在这个位置线程被挂起了,导致没有收到值更新的通知,导致出现了写覆盖
为何volatile不能保证原子性操作
如何解决原子性

  • 加synchronnized
  • 使用JUC下的AtomicInteger创建一个附带原子性的整形变量

addAndGet(int delta)
getAndAdd(int delta)
getAndIncrement()
incrementAndGet()

有序性
计算机在执行程序时,为了提高性能,编译器和处理器往往会对指令做重排,一般分为三种
源代码->编译器优化的重排->指令并行的重排->内存系统的重排->最终执行的指令

  • 处理器在进行 重排序的时候必须要考虑指令之间的数据依赖性

数据依赖性:需要先声明后使用的、需要先进行计算的

  • 多线程环境中线程交替执行,由于编译器优化重排的存在, 两个线程中使用的变量能否保证一致性是无法预测的。
指令重排1

单独线程中不存在数据依赖,但是在不同线程之间具有数据依赖
int a,b,x,y = 0;

线程1 线程2
x = a; y = b;
b = 1; a = 2;

x=0,y=0
如果编译器对这段代码进行了重拍优化后,可能会出现的情况

线程1 线程2
b = 1; a = 2;
x = a; y = b;

x=2,y=1
添加Volatile禁止编译器进行指令重排

指令重排2

某个变量作为条件,重排会导致以其为条件的子句执行位置不固定(子句中有可能含有数据依赖

禁止指令重排的总结

Volatile实现禁止指令重排优化,从而避免了多线程环境下程序出现乱序执行的现象
了解概念内存屏障(Memory Barrier)又称内存栅栏,是一个CPU指令,作用有两个

  • 保证特定操作的执行顺序
  • 保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)
    由于编译器和处理器都能执行指令重拍优化。如果在指令之间插入一条某种Memory Barrier,则会告诉编译器和CPU,不管是什么指令都不能和这条Memory Barrier指令重排序。
    也就是说:通过插入Memory Barrier禁止了对在Memory Barrier前后的指令执行重排序优化。
    Memory Barrier的另一个作用就是强制刷出各种CPU 的缓存数据,因此任何CPU 上出线程都能够读取到这些数据的最新版本

分别对应volatile的可见性和有序性

对Volatile变量进行写操作时 对Volatile变量进行读操作时
会在写操作之后加入一条store屏障指令,将工作内存中的共享变量值刷新回主内存 会在读操作之前加入一条load屏障指令,从主内存中读取共享变量

小结

  • 工作内存与主内存同步延迟现象导致的可见性问题:
    可以使用 synchronized或者volatile关键字解决,他们都可以使一个线程修改后立刻对其他线程可见。
  • 对于指令重排导致的可见性问题和有序性问题:
    可以利用volatile关键字解决,因为volatile的另外一个作用就是禁止重排序优化。

三、在哪些地方用过Volatile

单例模式DCL 代码

单例模式有六种

public class SingletonDemo() {
    private static SingletonDemo instance = null;
    private SingletonDemo(){
        System.out.println(Thread.currentThread().getName()+"\t我是构造方法")
    }
    /*
    *单机模式下没问题,多线程下会导致多个线程分别创建自己的单例
    *也即会多次调用SingletonDemo()构造方法
    *这里为getInstence()添加synchronized是可以解决问题的,但是会导致
    *整个方法都被锁住
    *
    */
    public static SingletonDemo getInstence() {
        if(instance == null){
            instance = new SingletonDemo();
        }
        return instance;
    }
    public static void main(String[] args) {
        for(int i = 1;i<=10;i++){
            new Thread(()->{
                SingletonDemo.getInstence();
            },String.valueof(i)).start();
        }
    }
}

高并发的情况下,推荐使用DCL模式的单例模式。

    /*
    *--->所以使用DCL模式的单例模式
    *DCL(Double check Lock 双端检锁机制)
    */
    public static SingletonDemo getInstence() {
        //加锁之前进行一次判断
        if(instance == null){
            //使用同步代码块
            synchronized(SingletonDemo.class){
                //在加锁之后,再进行一次判断
                if(instance == null){
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }

但是这种DCL(双端检锁)单例模式机制不一定线程安全,原因是有指令重排的存在,可以加入Volatile来禁止指令重排。

  • 原因是在某一个线程执行到第一次检测,读取到的instance不为null时,instance的引用对象可能没有完成初始化。
    instance = new SingletonDemo();可以被分为三步:
  1. 分配对象内存空间
  2. 初始化对象
  3. 设置instance指向刚分配的内存地址,此时instance != null (等号运算符比较的是内存地址)
    步骤2、3之间没有数据依赖,重排前后对于单线程中的结果不产生影响。
    指令重排指挥保证单线程下的一致性,并不会关心多线程的语义一致性
    可能会出现
    1(分配内存空间) -> 3(此时instance != null) -> 2(初始化对象)
    的情况,即先分配内存空间并绑定内存空间后,再初始化对象。
    也就是说,存在某一线程访问到instance不为null时,由于instance实例未必已经初始化完成,也就造成了线程不安全。
1 public static SingletonDemo getInstence() {
2     //加锁前判断
3     if(instance == null){
4         //同步代码块(锁住的区域)
5         synchronized(SingletonDemo.class){
6             //加锁后判断
7             if(instance == null){
8                instance = new SingletonDemo();
9             }
10        }
11    }
12    //一旦其他线程发生指令重排132的情况,在其执行到13时
13    //本条线程就会由于不满足if条件直接进入返回语句,从而产生线程不安全
14    return instance;
15}

在其他线程重排为132的情况下,当其他线程执行到13时,本线程就会因为IF条件不满足,从而直接跳转到14行的return语句,可是此时对象并未创建完成,所以会产生线程不安全

小结:多线程模式下的单例模式:
  1. 获取对象方法使用DCL模式
  2. 在单例对象前加volatile禁止指令重排。
看CAS底层原理时候发现JUC中大量使用了volatile

//TODO

你可能感兴趣的:(Java面试:【Volatile是什么】)