美文网首页Java线程基础
面试题剖析:单例设计模式线程安全问题

面试题剖析:单例设计模式线程安全问题

作者: 叩丁狼教育 | 来源:发表于2018-12-05 17:10 被阅读101次

    本文作者:黄海燕,叩丁狼高级讲师。原创文章,转载请注明出处。

    1. volatile 关键字

    1.1 volatile 关键字作用:

    在百度百科截取的描述如下:

    image.png

    说明volatile 关键字作用作用有两点:

    1. 防止指令重排:规定了volatile 变量不能指令重排,必须先写再读。

    2. 内存可见:线程从内存中读取volatile修饰的变量的数据,直接从主内存中获取数据,不需要经过CPU缓存,这样使得多线程获取的数据都是一致的。如图所示:


      image.png

    1.2 volatile和synchronized的区别

    volatile不能够替代synchronized,原因有两点:

    1.对于多线程,不是一种互斥关系
    2.不能保证变量状态的“原子性操作”,所以volatile不能保证原子性问题

    1.3解决单例设计模式线程安全问题

    实现单例设计模式两种

    1. 饿汉式(不存在原子性,是线程安全的)

    实现1:

    //饿汉式:很饿需要立马创建对象
    public class Singleton1 {
        //1.定义一个对象
        private static final Singleton1 instance = new Singleton1();
        //2.私有化构造器,避免外部类创建对象
        private Singleton1(){}
        //3.获取对象的静态方法
        public static Singleton1 getInstance(){
            return instance;
        }
    }
    

    实现2:枚举方式(最安全)

    //饿汉式(枚举)
    public enum EnumSingleton {
       INSTANCE;
    }
    
    1. 懒汉式(懒加载):存在原子性问题,线程不安全
    //懒汉式:很懒,使用对象的时候才创建对象,但是省资源
    public class Singleton2 {
        private static Singleton2 instance;
    
        private Singleton2() {
        }
    
        public static Singleton2 getInstance() {
            if (instance == null) {
                instance = new Singleton2();
            }
            return instance;
        }
    }
    

    ①由于 instance = new Singleton2();存在原子性问题,所以我们应该用synchronized代码块将其同步。这里由于synchronized很耗资源,所以粒度越小越好,最好不要使用同步方法。

     public static Singleton2 getInstance() {
         if (instance == null) {
              synchronized (Singleton2.class) {
                  instance = new Singleton2();
              }
         }
         return instance;
     }
    

    ②在多个线程的情况,可能存在线程1和线程2都已经执行了instance == null的判断,可能线程1抢到了锁线程2就阻塞在了同步代码块入口,当线程1执行完毕释放锁,线程2拿到锁的时候因为之前判断instance == null为true就会创建对象,那么此时就无法保证单例了,所以我们应该继续在同步代码块中再判断一次instance == null。这样的做法我们有个专业名词,称之为双重检查锁定。

        public static Singleton2 getInstance() {
            if (instance == null) {
                synchronized (Singleton2.class){
                    if (instance == null) {
                        instance = new Singleton2();
                    }
                }
            }
            return instance;
        }
    

    ③instance = new Singleton2();这句代码存在指令重排问题,什么意思?

    一般的执行顺序为:

    1)给对象分配内存空间
    2)初始化对象
    3)变量instance 指向内存空间

    在单线程中,由于步骤2)和步骤3)即使交换顺序也不会影响最终效果,所以可能发生指令重排,顺序为:

    1)给对象分配内存空间
    3)变量instance 指向内存空间
    2)初始化对象

    如果出现指令重排就会发生以下问题,如图所示:

    image.png

    注意:由于线程2在外面的判断就为false,没有去运行需要竞争锁的代码,所以没有进入阻塞状态,和线程1是并行状态,导致访问对象出现问题,所以为了避免这个问题,我们应该不让指令重排发生,那么使用volatile修饰对象,让对象先写再读,固定对象的指令,避免指令重排。

    最终线程安全的单例懒汉式代码如下:

    public class Singleton2 {
        private static volatile Singleton2 instance;
        private Singleton2() {}
        public static Singleton2 getInstance() {
            if (instance == null) {
                synchronized (Singleton2.class){
                    if (instance == null) {
                        instance = new Singleton2();
                    }
                }
            }
            return instance;
        }
    }
    

    想获取更多技术视频,请前往叩丁狼官网:http://www.wolfcode.cn/all_article.html

    相关文章

      网友评论

        本文标题:面试题剖析:单例设计模式线程安全问题

        本文链接:https://www.haomeiwen.com/subject/gyywcqtx.html