单例模式的问题及解决方案探究

537 阅读3分钟

小知识,大挑战!本文正在参与“程序员必备小知识”创作活动。

背景

单例模式作为比较易于理解的设计模式,这里就不过多做介绍了。其确保一个类只有一个实例,且提供一个全局访问点

最经典的实现方法如下:

public class Singleton {

    private static Singleton instance;

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        
        return instance;
    }
}

那么说他有问题,问题在哪呢?

单线程环境下,这段代码是没有任何问题的。但是在多线程环境下,这有可能会导致不同线程创建了不同的instance,那么这就称不上是单例模式了。

具体是因为多个线程在访问这段代码时候,可能会同时进入 if (instance == null) { 这一行,这就导致了它们同时都判断出这条语句为 true,从而导致各自执行 instance = new Singleton();

解决方案1:同步

要解决这个问题,最简单直接的方法就是将这个方法改为同步方法,即加上关键字 synchronized,这就能确保同时只能有一个线程进入这个方法,直到它释放锁后其他线程才可以进入。

public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }

        return instance;
    }

但是这么做,你是否会觉得有点“小题大做”了呢?因为实际上这个问题只会发生在第一次创建instance实例的时候。而使用这个方法会导致每一次多个线程都要互斥地执行这个方法。如果在一个很多很多线程的环境下,这样会对性能造成很大的影响。

解决方案2:不推迟实例化

如果你的应用程序每次一定会使用到这个单件实例,或者在创建时负担不是很重(几乎不会发生多线程同时创建实例的话)。那么就可以“急切”地创建好实例,不延迟创建。

public class Singleton {

    private static Singleton instance = new Singleton();

    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

JMM会确保在``对任何线程可见之前,它的实例会先被创建。

解决方案3:双重检查加锁

public class Singleton {

    private volatile static Singleton instance;

    private Singleton() {}

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

volatile 确保对一个变量的写对这个变量的读可见。也就是说一个线程对这个变量的写操作一定会被其他线程对这个变量的读操作知道。

由于这个特性,只有第一次执行这个方法时才能从第一个 if (instance == null) { 走下去。而后面进入同步代码块后再检查一次确保实例为 null 才进行创建。

这个方法就很好的解决了使用 synchronized 的问题,大大减少了 getInstance() 的时间耗费。

总结

如果确保仅仅是单线程环境下,那么就无需做出这些解决方案。如果是有多线程且不频繁,getInstance()的性能对你的应用程序不是很重要,那么使用方案1同步方法也是合理的。如果总是需要创建实例,那么使用方案2不推迟实例化也合理。而方案3又能确保同步同时又能减少使用同步来提高性能,只是代码稍显复杂。

所以说,只有针对不同的场景来做不同的方案才是最合理的。