• 单例模式和volatile


    单例模式的演化

    1)饿汉式:利用static关键字,在类初始化的时候就会调用静态方法 

    public class Singleton {
        private static  final Singleton singleton=new Singleton();
        private Singleton(){
    
        }
        public static Singleton getInstance(){
            return singleton;
        }
    }

    缺点:这个时候可能还没使用这个对象,浪费资源 (参考:类的初始化时机)

    2)单例模式优化

    懒汉式:声明为null,用到的时候 在初始化,但是需要加锁防止线程并发的时候,产生两个对象

    public class Singleton {
        private static   Singleton singleton=null;
        private Singleton(){
    
        }
        public synchronized static Singleton getInstance(){
             System.out.println("我的其他业务");
            if (singleton==null){        
                singleton=new Singleton();
            }
            return singleton;
        }
    }

    缺点:方法中有一些不需要上锁的业务代码也给锁上了,锁的粒度在粗了

    3)深度优化(把锁细化)

    细化锁的时候需要加两次锁

    public class Singleton {
        private static   Singleton singleton=null;
        private Singleton(){
    
        }
        public  static Singleton getInstance(){
            System.out.println("我的其他业务");
            if (singleton==null){        
                synchronized (Singleton.class){//Double Check Lock
                    if (singleton==null){
                        singleton=new Singleton();
                    }
                }
            }
            return singleton;
        }
    }    

    之所以DCL加两把锁: 防止多个线程同时执行到了加锁的代码

    4)单例最终版本:CPU指令重排导致的安全性

    因为CPU可能指令重排:声明变量的时候 需要加上volatile关键字

        private static  volatile Singleton singleton=null;

    下面解释 为什么要加volatile关键字(先说结论:volatile可以禁止指令重排序)

    在我们的idea中 idea-view-Show Bytecode可以看到我们方法的字节码 

    比如 Object o = new Object();

    字节码

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

    当我们new一个对象的时候,基本上分三步指令(单条指令都有可能不是原子性)

    1)在堆内存申请一个对象 分配一块内存,此时对象里面的值是一个默认值,

    2)然后调用构造方法 初始化,

    3)把堆内存的引用赋值给o  o来执行堆内存中的Object对象,建立关联

    分配内存—初始化—建立关联

    当一个线性1new的时候,走到第一步 分配内存时,发生了CPU的指令重排序,先建立关联(此时关联的对象 值都是空的 因为还没经过初始化),

    这是线程2进来,发现对象不为空(因为第三步已经建立关联已经) 直接拿去用了 此时用的对象是一个半成品(因为线程1还没有进行初始化)

    解释volatile关键字

    而volatile的作用有两点

    1)多线程之间的可见性(类似CPU缓存一致性协议 保持缓存行里的数据一致性)

      当一个变量定义为volatile之后,此变量对所有线程可见,这里的"可见性"是指当一条线程修改了这个变量的值,新值对其他线程来说是立即得知的。

      而普通变量不能做到这一点,普通变量的值在线程之间传递需要通过主内存来完成,列如,当线程A修改一个普通变量的值,然后从工作内存(类比CPU的高速缓存)向主内存进行回写,

      另一个线程B在线程A回写完成了之后再从主内存(类比物理硬件的主内存)进行读取操作,新变量的值才会对线程B可见。

      普通变量和volatile变量的区别是。volatile的特殊规则(主内存和工作内存之间具体的交互协议)保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。

      因此,可以说volatile保证了多线程操作时变量的可见性,而皮套办理不能保证这一点

      关于volatile变量的可见性,经常会被误解,"volatile变量对所有线程是立即可见的,换句话说 volatile变量在各个线程中是一致的,所以基于volatile变量的运算在并发下是安全的"

      加粗字体是错误的,比如i++ 这行代码转成字节码有如下指令,i++的操作过程需要多个指令,所以volatile不能保证原子性,即不能保证线程安全

     0 aload_0
     1 dup
     2 getfield #3 //从主内存取值
     5 iconst_1
     6 iadd //加1
     7 putfield #3 //值重新写回主内存
    10 return

    当我们进行i++的时候 大致分三部。

    1.从主内存取值;
    2.执行+1;
    3.值重新写回主内存

    如果仅仅用volatile关键字,在执行第二个指令的时候,其他线程执行了第一个指令,那么拿到的还是旧值。

    2)阻止CPU指令重排序(JVM规范要求 对内存的时候加屏障)

      指令重排序时不能把后面的指令重排到内存屏障之前的位置

    https://www.cnblogs.com/ssskkk/p/12813115.html

    CPU为什么会指令重排

    假如有两条指令让CPU执行 并且两条指令直接没有前后依赖关系的时候,

    在第一条指令的执行过程之中 如果需要从内存中读数据,可以先把第二条指令执行完,因为CPU的运算速度百倍于内存的读取速度

    这么做可以增加计算器整个的运行效率

    比如 我们在烧水的时候 可以洗碗一样, 虽然先烧水,但是洗碗的动作可能先执行完。

    这个时候可能第二条指令会比第一条指令先执行完,原来的执行时1 2 背后CPU执行的顺序可能是 2 1(因为是两条指令 所以只有在并发的情况 才会出现这种可能)。

    public static void main(String[] args) throws Exception{
            int i=0;
            for (;;){
                i++;
                x=0;y=0;
                a=0; b=0;
                Thread one = new Thread(() -> {
                     a=1;x=b;
                });
                Thread two = new Thread(() -> {
                    b=1;y=a;
                });
                one.start();two.start();
                one.join(); two.join();
                String result="第"+i+"次执行 ("+x+" "+y+")";
                if (x==0&&y==0){
                    System.out.println(result);
                    break;
                }else{
    
                }
            }
        }
    View Code

    以上代码在运行了214609次之后,打印了出现了 x=0 y=0,证明了CPU存在指令重排序

  • 相关阅读:
    展望未来,总结过去10年的程序员生涯,给程序员小弟弟小妹妹们的一些总结性忠告
    马士兵_JAVA自学之路(为那些目标模糊的码农们)
    Java知识点总结
    解决:对COM组件的调用返回了错误HRESULT E_FAIL
    平差方法
    二进制、八进制、十进制、十六进制之间转换
    解决电脑复选框图标不正确方法
    SQL语句中的Create
    字段的值转换为大小写
    SQL NOW() 函数
  • 原文地址:https://www.cnblogs.com/ssskkk/p/12708783.html
Copyright © 2020-2023  润新知