单例模式之线程安全性讨论


在 23 种设计模式中单例模式是比较常见的,在非多线程的情况下写单例模式,考虑的东西会很少,但是如果将多线程和单例模式结合起来,考虑的事情就变多了,如果使用不当(特别是在生产环境中)就会造成严重的后果。所以如何使单例模式在多线程中是安全的显得尤为重要。

发布对象

  • 发布对象:使一个对象能够被当前范围之外的代码所使用
  • 对象逸出:一种错误的发布,当一个对象还没有构造完成时,就使他被其他线程所见。

在我们的日常开发中,我们经常要发布一些对象,比如通过类的非私有方法返回对象的引用,或者通过公有静态变量发布对象,我们来看看这两个例子。

  • 例子一:

    public class UnsafePublish {
    
        private String[] states = {"a", "b", "c"};
        
        private static final Logger LOGGER = LoggerFactory.getLogger(UnsafePublish.class);
    
        public String[] getStates() {
            return states;
        }
    
        public static void main(String[] args) {
            UnsafePublish unsafePublish = new UnsafePublish();
            LOGGER.info("{}", Arrays.toString(unsafePublish.getStates()));
    
            unsafePublish.getStates()[0] = "d";
            LOGGER.info("{}", Arrays.toString(unsafePublish.getStates()));
        }
    }
    
  • 从这个例子中我们发现,states 对象可以被任何线程所修改,不同线程得到的值很可能不是所期望的值,所以这个是线程不安全的。

  • 例子二:

    public class Escape {
    
        private int thisCanBeEscape = 0;
    
        private static final Logger LOGGER = LoggerFactory.getLogger(Escape.class);
    
        public Escape () {
            new InnerClass();
        }
    
        private class InnerClass {
            public InnerClass() {
                LOGGER.info("{}", Escape.this.thisCanBeEscape);
            }
        }
    
        public static void main(String[] args) {
            new Escape();
        }
    }
    
  • 这里例子很有意思,仔细走一下代码,我们发现 Escape 对象未被正确构造完成之前就已经被发布使用,即 Escape.this 对象未被构造完成就已经发布使用了,这样就有不安全的因素在里面,这里就出现了this 引用在构造期间逸出的错误

安全发布对象

上面提到两种不安全的发布对象方式,那如何安全的发布对象呢,这里提出了四种方式:

  • 在静态初始化函数中初始化一个对象引用
  • 将对象的引用保存到 volatile 类型或者 AtomicReference 对象中
  • 将对象的引用保存到某个正确构造对象的 final 类型域中
  • 将对象的引用保存到一个由锁保护的域中

这四种方法该如何理解呢?我们在学习 Java 做项目的时候,都会涉及到 Java 里面的单例模式,特别是 Spring 相关框架来进行开发的时候都知道 Spring 管理的类默认都是用单例模式的,那么如何保证一个实例只会初始化一次且线程安全呢,现在我们就借助不同单例的写法来具体说一下四种安全发布对象的方式。

单例模式

设计思路:一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用 getInstance 这个名 称)。当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例

先来看实例一:

public class SingletonExample1 {
    /**
     * 私有构造函数
     */
    private SingletonExample1() {
    }

    /**
     * 单例对象
     */
    private static SingletonExample1 instance = null;

    /**
     * 静态的工厂方法
     *
     * @return
     */
    public static SingletonExample1 getInstance() {
        if (instance == null) {
            instance = new SingletonExample1();
        }
        return instance;
    }
}

这就是单例模式的懒汉模式,懒汉模式他不会优先创建对象,而是判断对象如果为空才去创建并返回对象实例。很明显,这个实例在单线程模式下完全没问题,但是在多线程模式下存在安全性问题,比如说 A 线程判断 instance 为空,于是进入到了20行,同时这时候 B 线程也判断 instance 为空(A 线程还未创建 instance 的实例),B 线程也进入了 20 行,导致两个线程进行了对象的实例化,这就出现了线程安全问题。

来看看实例二:

public class SingletonExample2 {
    /**
     * 私有构造函数
     */
    private SingletonExample2() {
    }

    /**
     * 单例对象
     */
    private static SingletonExample2 instance = new SingletonExample2();

    /**
     * 静态的工厂方法
     *
     * @return
     */
    public static SingletonExample2 getInstance() {
        return instance;
    }
}

这个是单例模式的饿汉模式,即对象实例在类加载的时候就创建了,其他地方需要该对象直接获取就行。饿汉模式不存在线程安全问题,但是不足之处在于,如果它的构造方法中存在太多的处理,会导致类加载的时候特别慢,因此可能会引起性能问题,如果只进行对象的创建,而没有调用的话,也会造成资源的浪费,所以在使用饿汉模式要考虑两个问题,一个是私有构造函数没有太多的处理,一个是这个类在实际过程中肯定会被使用

饿汉模式是线程安全的,懒汉模式也能够变成性能安全吗,答案当然是肯定的。那现在我们逐步来优化上面懒汉模式那个实例:

优化一:

public class SingletonExample3 {
    /**
     * 私有构造函数
     */
    private SingletonExample3() {
    }

    /**
     * 单例对象
     */
    private static SingletonExample3 instance = null;

    /**
     * 静态的工厂方法
     *
     * @return
     */
    public static synchronized SingletonExample3 getInstance() {
        if (instance == null) {
            instance = new SingletonExample3();
        }
        return instance;
    }
}

我们在 getInstance 方法加个 synchronized 关键字同步这个代码块是不是可以了呢,是的,这样保证了线程安全。但是这种方法并不推荐,它会带来性能上的开销。

优化二:

public class SingletonExample4 {
    /**
     * 私有构造函数
     */
    private SingletonExample4() {
    }

    /**
     * 单例对象
     */
    private static SingletonExample4 instance = null;

    /**
     * 静态的工厂方法
     */
    public static SingletonExample4 getInstance() {
        if (instance == null) { // 双重检测机制          // B
            synchronized (SingletonExample4.class) {    // 同步锁
                if (instance == null) {
                    instance = new SingletonExample4(); // A - 3
                }
            }
        }
        return instance;
    }
}

这个优化我们利用了双重检测机制和同步锁,这种方式也称为双重同步锁单例模式,但是这个案例还是线程不安全的,大家通过代码层面的分析后,发现其实根本不会由线程安全问题,那问题出现在哪呢?这个其实要和对象创建步骤和 jvm 指令重排有关系,我们正常创建对象的指令步骤是这样的:

  1. memory = allocate() 分配对象的内存空间
  2. ctorInstance() 初始化对象
  3. instance = memory 设置instance指向刚分配的内存

但是因为JVM和cpu优化,发生了指令重排

  1. memory = allocate() 分配对象的内存空间
  2. instance = memory 设置instance指向刚分配的内存
  3. ctorInstance() 初始化对象

我们可以结合代码,假如在A线程在 20 行刚执行完第一个步骤,分配对象内存空间,这个时候B如图B的位置,判断 instance 不为空,直接返回 instance 实例,这时候就出现问题了,B 得到的这个实例并没有完全初始化(A 还没有执行完对象的初始化步骤)就已经使用了。

关于对象创建过程可以参考 Java 对象的创建、内存布局和访问定位

那如何禁止指令重排呢,很简单,用我们前面文章提到的volatile 关键字就可以了

最终优化:

public class SingletonExample5 {
    /**
     * 私有构造函数
     */
    private SingletonExample5() {
    }

    /**
     * 单例对象 volatile + 双重检测机制 -> 禁止指令重排
     */
    private volatile static SingletonExample5 instance = null;

    /**
     * 静态的工厂方法
     */
    public static SingletonExample5 getInstance() {
        if (instance == null) { // 双重检测机制        // B
            synchronized (SingletonExample5.class) { // 同步锁
                if (instance == null) {
                    instance = new SingletonExample5(); // A - 3
                }
            }
        }
        return instance;
    }
}

当然,饿汉模式的另一种写法,在静态代码块中初始化实例也是一样的。

public class SingletonExample6 {
    /**
     * 私有构造函数
     */
    private SingletonExample6() {
    }

    /**
     * 单例对象
     */
    private static SingletonExample6 instance = null;

    static {
        instance = new SingletonExample6();
    }

    /**
     * 静态的工厂方法
     */
    public static SingletonExample6 getInstance() {
        return instance;
    }
}

还有一种实现单例的方式:枚举模式(推荐)

public class SingletonExample7 {
    /**
     * 私有构造函数
     */
    private SingletonExample7() {
    }

    public static SingletonExample7 getInstance() {
        return Singleton.INSTANCE.getInstance();
    }

    private enum Singleton {
        /**
         * 实例
         */
        INSTANCE;

        private SingletonExample7 singleton;

        /**
         * JVM保证这个方法绝对只调用一次
         */
        Singleton() {
            singleton = new SingletonExample7();
        }

        public SingletonExample7 getInstance() {
            return singleton;
        }
    }
}
  • 4
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

✦昨夜星辰✦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值