单例模式双端校验锁单例模式下为什么能保证线程安全

79 阅读2分钟

本文已参与「新人创作礼」活动, 一起开启掘金创作之路。

package com.wsx;
                public class SingletonThreadDemo {
//加volatile防止指令重排

                    private static volatile SingletonThreadDemo instance = null;



                    private SingletonThreadDemo(){

                        System.out.println(Thread.currentThread().getName()+"\t"+"我是单例模式的构造方法");

                    }

                    public static SingletonThreadDemo getInstance(){//此处加sync可以解决,但是锁了整个方法

//双端检锁机制

                        if(instance == null) {
                            synchronized (SingletonThreadDemo.class){
                                if(instance == null){
                               instance = new SingletonThreadDemo();
                          }

                            }
                        }

                        return instance;

                    }
                    public static void main(String[] args) {

                        for (int i = 0; i < 10; i++) {

                            new Thread(()->{

                                SingletonThreadDemo.getInstance();

                            },String.valueOf(i)).start();

                        }

                    }

                }

应该加上volatile关键字。

关键字作用 一:内存可见性

基于缓存一致性协议,当系统或者程序中某个变量发生修改时,此时cpu会同时其他线程,告诉被通知的线程缓存内容已经被修改,通知i线程需要更新缓存,这样每个线程都能获取到最新的变量值。

二:基于内存屏障的防止指令重排

用Voliate修饰的变量,可以防止cpu执行指令重排序,底层的实现方式是基于4中内存屏障:读读,读写,写读,读读屏障。

不加的情况下,假设两个线程,线程A正在执行instance = new Instance()的操作,而线程B开始执行if(instance==null)的判断,当不存在volatile的时候,因为 new Instance()是一个非原子操作,可能发生无序写入,构造函数可能在整个对象构造完成前执行完毕,线程B可能会看到一个不完整的instance对象,因为java的某些实现会在内存中开辟一片存储对象的区域后直接返回内存的引用,所以线程B判断不为null,而这时候实际上,instance的构造函数还没有执行,从而线程b得到不完整的对象。在 Instance 的构造函数执行之前,会在内存中开辟一片存储对象的区域后直接返回内存的引用,赋值给变量 instance,instance也就可能成为非 null 的,即赋值语句在对象实例化之前调用,此时别的线程得到的是一个还会初始化的对象,这样会导致系统崩溃线程B可能会看到一个不完整的instance对象,因为java的某些实现,所以线程B判断不为null。 从而得到不完整的对象。