volatile的作用,线程可见,禁止指令重排序

volatile

第01章 volatile的作用

1.线程间可见性

说明: 一个线程对共享变量值的修改,能够及时的被其他线程看到。

2.共享变量可见性实现的原理

线程1对共享变量的修改要想被线程2及时看到,必须经过如下2个步骤:

  1. 把工作内存1中更新过的共享变量刷新到主内存中
  2. 将主内存中最新的共享变量的值更新到工作内存2中

代码演示

import java.util.concurrent.TimeUnit;

public class T01_HelloVolatile {
   /*volatile*/ boolean running= true;

    void m() {
        System.out.println("m start");
        while (running) {
        }
        System.out.println("m end!");
    }

    public static void main(String[] args) {
        T01_HelloVolatile t=new T01_HelloVolatile();
        new Thread(t::m, "t1").start();
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        t.running=false;
    }
}

效果图
在这里插入图片描述加上volatile关键字后的效果图

   volatile boolean running= true;

在这里插入图片描述

注意事项:

  • 很多的执行语句有对内存进行同步的过程 例如 System.out.println("hello");在执行的过程中,内存与本地缓存做同步, 很有可能在同步过程中又得到了新的volatile的共享变量的值 (此处打印语句中使用了同步锁synchronized )

2.禁止指令重排序

带着一个问题: 美团面试题: DCL单例要不要加volatile? 问题的来源在单例中

cpu的乱序执行概念

cpu在进行读等待的同时执行命令, 是cpu乱序的根源 , 不是乱 , 而是提高效率

证明cpu是乱序的, 使用java代码证明 了解一下

public class Test02 {
    private static int x=0,y=0;
    private static int a=0,b=0;

    public static void main(String[] args) throws InterruptedException {
        int i=0;
        for (; ; ) {
            i++;
            x=0;y=0;
            a=0;b=0;
            Thread one=new Thread(new Runnable() {
                @Override
                public void run() {
                    //由于线程one先启动,下面这句话让它等一等线程two. 可以根据自己的电脑进行设置
                    //shortWait(1000);
                    a=1;
                    x=b;
                }
            });
            Thread other=new Thread(new Runnable() {
                @Override
                public void run() {
                    b=1;
                    y=a;
                }
            });
            one.start();other.start();
            one.join();other.join();
            String result = "第" + i + "次(" + x + "," + y + ")";
            if (x == 0 && y == 0) {
                System.err.println(result);
                break;
            }else {
                //不做处理
                /*
                * 情况分析 :
                *   1, 正常的情况下会出现 x=0,y=1
                *                      y=1,x=0;
                *                      x=1,y=1;
                * */
            }
        }
    }
}

运行结果

在这里插入图片描述
半初始化状态的概念:

Object o=new Object()有那几个执行步骤?

汇编结果展示:

0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init>>
7 astore_1
8 return

半初始化状态:

  1. 对象在new 出来是默认值是null ,经过init方法后才会变成我们想要的

3.单例讲解


单例的创建方式

  1. /*
    * 饿汉式
    * 类加载到内存后,就实例化一个单例,jvm保证线程安全
    * 简单实用,推荐使用
    * 唯一缺点: 不管用到与否,类装载时就会完成实例化
    * Class.forName("")
    * */
    public class Demo01 {
        private static final Demo01 INSTANCE = new Demo01();
        
    	//构造方法私有化,不允许别人new对象
        private Demo01() {
        }
    	
        //无论调用多少次getInstance方法,拿到的都是唯一的一个INSTANCE对象
        public static Demo01 getInstance() {
            return INSTANCE;
        }
    
        public void m() {
            System.out.println("m");
        }
    
        public static void main(String[] args) {
            Demo01 demo01 = Demo01.getInstance();
            Demo01 demo02 = Demo01.getInstance();
            System.out.println(demo01==demo02);//true
        }
    }
    
  2. /*
    * 优化了,无论用不用都要初始化的问题
    * 产生了问题:
    *   虽然达到了按需初始化的目的,但却带来线程不安全的问题
    * */
    public class Mgr03 {
        private static Mgr03 INSTANCE;
        private Mgr03(){}
    
        public static Mgr03 getInstance() {
            if (INSTANCE == null) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Mgr03();
            }
            return INSTANCE;
        }
    
        public void m() {
            System.out.println("m");
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 100; i++) {
                new Thread(()-> System.out.println(Mgr03.getInstance().hashCode())).start();
            }
        }
        //结果 1703607146 877465552 1703607146 2062736005
    }
    
  3. /*
    * 虽然达到了按需初始化的目的,但却带来线程不安全的问题
    * 解决方法,加锁
    * 产生了问题: 锁的粒度太粗,锁住的代码过多,效率变得很低
    * */
    public class Mgr04 {
        private static Mgr04 INSTANCE;
        private Mgr04(){}
    
        public synchronized static Mgr04 getInstance() {
            //业务逻辑,此处省略了一万行
            if (INSTANCE == null) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Mgr04();
            }
            return INSTANCE;
        }
    
        public void m() {
            System.out.println("m");
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 100; i++) {
                new Thread(()-> System.out.println(Mgr04.getInstance().hashCode())).start();
            }
        }
    }
    
    
  4. /*
    * 感觉上解决了锁的粒度问题,实际上并没有
    * */
    public class Mgr05 {
        private static Mgr05 INSTANCE;
        private Mgr05(){}
    
        public  static Mgr05 getInstance() {
            if (INSTANCE == null) {
                //第一步判断会出现线程安全问题
                synchronized (Mgr05.class){
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr05();
                }
            }
            return INSTANCE;
        }
    
        public void m() {
            System.out.println("m");
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 100; i++) {
                new Thread(()-> System.out.println(Mgr05.getInstance().hashCode())).start();
            }
        }
    }
    
  5. /*
    * 双重检查解决方案DCL  //此手法用户众多开源软件的项目中   ==重点掌握==
    * 问题的来源 DCL单例到底要不要加volatile,
    * */
    public class Mgr06 {
        
        //private static Mgr06 INSTANCE;
        private volatile static Mgr06 INSTANCE;
        private Mgr06(){}
    
        public  static Mgr06 getInstance() {
            if (INSTANCE == null) {
                synchronized (Mgr06.class){
                    if (INSTANCE==null){ //Double Check Lock
                        try {
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        INSTANCE = new Mgr06();
                    }
                }
            }
            return INSTANCE;
        }
    
        public void m() {
            System.out.println("m");
        }
    
        public static void main(String[] args) {
            for (int i = 0; i < 100; i++) {
                new Thread(()-> System.out.println(Mgr06.getInstance().hashCode())).start();
            }
        }
    }
    

美团面试题: DCL单例要不要加volatile?答案: 必须加

  1. 因为程序是可以乱序执行的
  2. new 一个对象是存在半初始化状态的

存在这种可能( 该可能概率就是在大海里找到了我标记的一粒沙子 )

第一个线程进来才执行了半初始化指令

后面发生了指令重排序(cpu乱来了) 执行了astore_1指令( 建立了引用连接 ) , 使得 t 指向了 m=0 然而我的init还没有执行

第二个线程进来了判断了第一个是否==null 发现不是,它是半初始化的了

第二个线程直接没有进入synchronized代码块 , 最后拿着半初始化状态的对象跑了

图解:

在这里插入图片描述

结论

volatile修饰的变量在内存空间中 对它执行的指令不可以乱序

防止了其他线程拿到一个半初始化的对象

引发的问题

volatile在底层是怎么防止指令重排序的呢?

这就是内存屏障

jvm中的内存屏障:

​ 屏障两边的指令不可以重拍! 保证有序!

底层实现追溯到汇编指令就是Lockaddl指令

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值