zoukankan      html  css  js  c++  java
  • 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面试_大厂高频面试题_阳哥整理

  • 相关阅读:
    守卫者的挑战
    黑魔法师之门
    noip2015 普及组
    noip2015 提高组day1、day2
    40026118素数的个数
    高精度模板
    经典背包系列问题
    修篱笆
    [LintCode] Linked List Cycle
    [LintCode] Merge Two Sorted Lists
  • 原文地址:https://www.cnblogs.com/chaozhengtx/p/14435659.html
Copyright © 2011-2022 走看看