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面试_大厂高频面试题_阳哥整理

  • 相关阅读:
    PythonStudy——数据类型总结 Data type summary
    PythonStudy——可变与不可变 Variable and immutable
    PythonStudy——列表操作 List operatio
    PythonStudy——列表的常用操作 List of common operations
    PythonStudy——列表类型 List type
    PythonStudy——字符串扩展方法 String extension method
    PythonStudy——字符串重要方法 String important method
    AWT,Swing,RCP 开发
    JQuery插件机制
    最新知识网站
  • 原文地址:https://www.cnblogs.com/chaozhengtx/p/14435659.html
Copyright © 2011-2022 走看看