• 多线程-volatile


    首先volatile有两大功能:

    • 保证线程可见性
    • 禁止指令重排序

    1、保证线程可见性

    首先我们来看这样一个程序,其中不加volatile关键字运行的结果截然不同,加上volatile程序能够正常结束,不加则程序进入死循环;

    package com.designmodal.design.juc01;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author D-L
     * @Classname T001_volatile
     * @Version 1.0
     * @Description volatile 保证线程的可见性
     * @Date 2020/7/19 17:30
     */
    public class T001_volatile {
        //定义一个变量running
        volatile boolean running = true;
    
        public void m(){
            while(running){
                //TODO 不做任何的处理
                System.out.println("while is running When can I stop -------------");
            }
            System.out.println("method is end ---------------");
        }
    
        public static void main(String[] args) {
            T001_volatile t001_volatile = new T001_volatile();
            new Thread(t001_volatile::m , "Thread t1").start();
    
            //停一秒
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //修改running的值
            t001_volatile.running = false;
        }
    }

    通过上面的小程序说明volatile是具有保证线程之间的可见性的功能的,具体是如何实现的呢?下面给大家解释一下:

    之前在上一篇讲synchronized时提到了 堆内存是线程共享的,而线程在工作时有自己的工作内存,对于共享变量running来说,线程1和线程2在运行的时候先把running变量copy到自己工作内存,对这个变量的改变都是在自己的工作内存中,并不会直接的反映到其他线程,如果加了volatile,running变量改变其他线程很快就会知道,这就是线程的可见性;

    这里用到的是:MESI(CPU缓存一致性协议)  MESI的主要思想:当CPU写数据时,如果该变量是共享数据,给其他CPU发送信号,使得其他的CPU中的该变量的缓存行无效;归根结底这里需要借助硬件来帮助我们。

     

    volatile保证线程可见性但是不能代替synchronized:

    package com.designmodal.design.juc01;
    
    import java.util.ArrayList;
    import java.util.List;
    
    /**
     * @author D-L
     * @Classname VolatileAndSynchronized
     * @Version 1.0
     * @Description  synchronized can not be replaced by volatile
     *               volatile 不能代替synchronized
     *               只能保证可见性 不能保证原子性
     *               count++ 不是原子性操作
     * @Date 2020/xx/xx 23:25
     */
    public class VolatileAndSynchronized {
        volatile int count = 0;
        public synchronized void m(){
            for (int i = 0; i < 1000; i++) {
                //非原子性操作 汇编指令至少有三条
                count++;
            }
        }
    
        public static void main(String[] args) {
            VolatileAndSynchronized v = new VolatileAndSynchronized();
            List<Thread> threads = new ArrayList<>();
            for (int i = 0; i < 10; i++) {
                threads.add(new Thread(v::m , "Thread"+ i));
            }
            threads.forEach(o ->o.start());
            threads.forEach(o ->{
                try {
                    o.join();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
            System.out.println(v.count);
        }
    }

    2、禁止指令重排序

    指令重排序也是和CPU有关系,加了volatile之后,每次写都会背线程看到。CPU原来执行指令时,是按照一步一步顺序来执行的,但是CPU为了提高效率它会把指令并发来执行,第一个指令执行到一半的时候第二条指令就可能已经开始执行了,这叫流水线式的执行;为了充分的利用CPU,就要求编译器把编译完的源码指令,可能会进行一个指令重新排序;这种架构通过实际验证,很大效率上提高了CPU的使用效率

    下面从一个面试题来讨论一下指令重排序:

    面试官:你听过单例模式吗?

    你:当然听过,不然没法聊了。

    面试官:那你听过单例模式的双重检查吗?

    你:那那那.....当然也是听过喽(这里你没听过也没关系,我在之前的设计模式中写过这个单例模式的双重检查,你可以看一下写的应该很详细了)。

    package com.designmodal.design.juc01;
    
    import java.util.concurrent.TimeUnit;
    
    /**
     * @author D-L
     * @Classname T002_volatile
     * @Version 1.0
     * @Description volatile 指令重排序
     * @Date 2020/7/20 00:48
     */
    public class T002_volatile {
        //创建私有的 T002_volatile 有人会问这里的volatile要不要使用,这里的答案是肯定的
        private static /**volatile*/ volatile T002_volatile INSTANCE;
    
        public T002_volatile() {}
    
        public T002_volatile getInstance(){
            //模拟业务代码  这里为了synchronized更加细粒度,所以使用了双重检查
          if(INSTANCE == null){
              synchronized (this){
                  //双重检查
                  if(INSTANCE == null){
                      //避免线程之间的干扰 在这里睡一秒
                      try {
                          TimeUnit.SECONDS.sleep(1);
                      } catch (InterruptedException e) {
                          e.printStackTrace();
                      }
                      //创建实例对象
                      INSTANCE = new T002_volatile();
                  }
              }
          }
          return INSTANCE;
        }
    
        /**
         * 创建100个线程 调用getInstance() 打印hashcode值
         * @param args
         */
        public static void main(String[] args) {
            T002_volatile t001_volatile = new T002_volatile();
            for (int i = 0; i < 100; i++) {
                new Thread(() ->{
                    T002_volatile instance = t001_volatile.getInstance();
                    System.out.println(instance.hashCode());
                }).start();
            }
    
        }
    }

    在上述的代码中:  INSTANCE = new T002_volatile();  经过编译后的指令是分三步的(这个知识点我在之前JVM模块中对象的内存布局中讲过)

    • 1、给指令申请内存
    • 2、给成员变量初始化
    • 3、把这块对象的内容赋给INSTANCE

    在第二步这里既然已经有默认值了,第二个线程来检查,发现已经有值了根本就不会进入锁住的那份代码;加了volatile就不会出现指令重排序了,所以在这个时候一定要保证初始化完成之后才会赋值给这个变量,这就是volatile存在的意义。

  • 相关阅读:
    jsp4个作用域
    jsp9个内置对象
    jsp指令
    jsp注释
    jsp原理
    java面试
    代理
    泛型
    exception
    基础
  • 原文地址:https://www.cnblogs.com/dongl961230/p/13342688.html
Copyright © 2020-2023  润新知