Volatile的简单理解

1 谈谈对 Volatile 的理解

volatile 应用于多线程环境下;
volatile 是JVM提供的轻量级的同步机制;
volatile 修饰的变量 保证可见性、不保证原子性、禁止指令重排

  • 可见性:多个线程操作同一个公共资源时,其中一个线程修改了这个资源,其他线程可以第一时间就知道修改信息。
  • 原子性:不可分割,即某个线程在做某个具体任务时,中间不可以被加塞或者被分割。整体要么都成功要么都失败
  • 指令重排:多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致是无法确定的,结果无法预测

一个验证可见性的 Demo

class Demo {
    public static void main(String[] args) {
		//资源类
        Date date = new Date();
		
        new Thread(() ->{
            System.out.println(Thread.currentThread().getName() + "线程开始执行");
            
            // 线程睡眠3秒
            try {
                TimeUnit.SECONDS.sleep(3);
                date.setNumber();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },"A").start();

        //模拟线程B:一直在这里等待循环,直到 number 的值不等于零
        while (date.number == 0){

        }

        //只要变量的值被修改,就会执行下面的语句
        System.out.println(Thread.currentThread().getName() + "执行结束");
    }
}

class Date{
    //volatile 保证可见性
    volatile int number;

    public void setNumber(){
        number = 60;
    }
}

过程解读

  1. 线程 a 从主内存读取 共享变量 到对应的工作内存
  2. 对共享变量进行更改
  3. 线程 b 读取共享变量的值到对应的工作内存
  4. 线程 a 将修改后的值刷新到主内存,失效其他线程对 共享变量的副本
  5. 线程 b 对共享变量进行操作时,发现已经失效,重新从主内存读取最新值,放入到对应工作内存。

一个验证不保证原子性的 Demo

public class Demo2 {
    public static void main(String[] args) {

        Date2 date2 = new Date2();

        //开启20个线程
        for(int i = 0;i < 20;i++){
            new Thread(() -> {
                //每个线程执行1000次++操作
                for (int j = 0;j < 1000;j++){
                    date2.setNumberPlus();
                }
            },String.valueOf(i)).start();
        }

        //让20个线程全部执行完
        while (Thread.activeCount() > 2){ //main + GC
            //礼让线程
            Thread.yield();
        }

        //查看最终结果
        System.out.println(date2.number);
    }
}

class Date2{
    volatile int number;

    public void setNumberPlus(){
        //让其自增
        number++;
    }
}

过程解读

  1. 假设现在共享变量值为10,线程A 从主内存中读取数值到自己的工作内存,还没有来得及自增,CPU 调度切换到了线程B;
  2. 此时线程B 读取主内存中的数值,仍然是10,完成自增后,还来得及写回主内存,CPU 调度又切换回线程A ,此时线程A自增;
  3. 线程A 写回主内存值为11
  4. 线程B 写回主内存值为11
  5. 此时的结果就是2个线程只进行了1次修改

如何才能保证原子性

1、使用synchronized,不建议使用
2、使用AtomicInteger代替int/Integer,同时方法也相应的改变

public class Demo3 {
    public static void main(String[] args) {

        Date3 date3 = new Date3();

        //开启20个线程
        for(int i = 0;i < 20;i++){
            new Thread(() -> {
                //每个线程执行1000次++操作
                for (int j = 0;j < 1000;j++){
                    date3.setAtomic();
                }
            },String.valueOf(i)).start();
        }

        //让20个线程全部执行完
        while (Thread.activeCount() > 2){ //主线程 + GC
            Thread.yield();//礼让线程
        }

        //查看最终结果
        System.out.println(date3.number); 
    }
}

class Date3{
	//创建一个原子 Integer 包装类,默认为0
    AtomicInteger number = new AtomicInteger();

    public void setAtomic(){
        //相当于 atomicInter ++
        number.getAndIncrement();
    }
}

什么是指令重排

为了提高性能,JVM在执行代码时会经过以下过程

单线程环境里保证最终执行结果和代码顺序的结果一致。

处理器在进行指令重排时,要考虑到指令之间数据的依赖关系
在多线程环境中,由于编译器优化重排,两个线程在使用的变量能否保住一致性是无法确定的,结果无法预测 。

2 Volatile 的使用场景举例

单例模式中的DCL(双端检查机制)

public class Singleton6 {
    //2.提供静态变量保存实例对象
    private volatile static Singleton6 INSTANCE;

    //1.私有化构造器
    private Singleton6(){}

    //3.提供获取对象的方法
    public static  Singleton6 getInstance(){
        //第一重检查:针对很多个线程同时想要创建对象的情况
        if(INSTANCE == null){
            //同步代码块锁定
            synchronized (Singleton6.class){
     //第二重锁检查(针对比如A,B两个线程都为null,第一个线程创建完对象,第二个等待锁的线程拿到锁的情况)
                if(INSTANCE == null){
                    INSTANCE = new Singleton6();
                }
            }
        }
        return INSTANCE;
    }
}

为什么要在这里加上 volatile

因为创建对象分为 3 步:

  1. 分配内存空间;
  2. 初始化对象
  3. 设置实例执行刚分配的内存地址【正常流程走:instance ! = null】
    但是,由于这 3 步不存在数据依赖关系 ,所以可能进行重排序优化,造成下列现象:
  4. 分配内存空间
  5. 设置实例执行刚分配的内存地址【instance ! = null 有名无实,初始化并未完成!】
  6. 初始化对象
    所以当另一条线程访问 instance 时 不为null,但是 instance 实例化未必已经完成,也就造成线程安全问题!

3 JMM

Java内存模型:Java Memory Model,是一种抽象概念并不真实存在,描述的是一种规则或规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段和构成数组对象的元素)的访问方式。

具体的JMM 规定如下:

  1. 所有 共享变量 储存于 主内存 中;
  2. 每条线程拥有自己的工作内存,保存了被线程使用的变量的副本拷贝;
  3. 线程对变量的所有操作(读,写)都必须在自己的 工作内存 中完成,而不能直接读写 主内存 中的变量;
  4. 不同线程之间也不能直接访问对方工作内存中的变量,线程间变量值的传递需要通过主内存中转来完成

JMM三大特性:

  1. 可见性
  2. 原子性
  3. 有序性

参考阳哥教学视频Java面试_大厂高频面试题_阳哥整理

posted @ 2021-02-23 14:04  阿政在努力  阅读(121)  评论(0编辑  收藏  举报