单例模式漫谈

单例模式

单例模式属于创建型模式,是Gangs of Four Design patterns中其中的一种。单例模式的实现有多种方式,很多实现方式在开发者中也是比较有争议的。本文就单例模式的应用场景,实现方式,最佳实践做一些浅显的讨论。

java singleton

单例模式确保在虚拟机中只实例化一个对象,该singleton class要提供一个对此实例的全局访问点。单例模式一般用来实现日志,缓存,线程池等。

单例模式uml类图

java singleton patten

java单例模式的实现由多种方式,但是所有的实现方式都遵循以下几个概念

  • 私有的构造函数,限制其它类对其实例化
  • 该单例类私有的全局静态变量
  • 访问该单例的共有静态方法

下面我们来讨论单例模式的几种实现方式以及实现中的一些设计理念

  • 饥饿式

    public class EagerSingleton {
        private static EagerSingleton INSTANCE = new EagerSingleton();
    
        private EagerSingleton() {
        }
    
        public static EagerSingleton getInstance(){
            return INSTANCE;
        }
    }
    

    饥饿式是在类加载的时候就实例化了,这样就有一个弊端,就是有时候我们并没有使用该实例,但是它却存在虚拟机中,如果该实例引用的资源较少则没什么关系,反之则显得比较浪费。

  • 懒汉式
public class LazySingleton {
    private static LazySingleton INSTANCE;

    private LazySingleton(){}

    public static LazySingleton getInstance(){
        if (INSTANCE == null){
            INSTANCE = new LazySingleton();
        }
        return INSTANCE;
    }
}

懒汉式在我们需要使用实例的时候它才初始化,这样就没有饥饿式和静态块方式的弊端,但是会有线程安全的问题。

  • 静态块单例模式实现方式
public class StaticBlockSingleton {

    private static  StaticBlockSingleton INSTANCE;

    static {
        try {
            INSTANCE = new StaticBlockSingleton();
        } catch (Exception e) {
            throw new RuntimeException("Exception occured in creating singleton instance");
        }
    }

    private StaticBlockSingleton(){}

    public static StaticBlockSingleton getInstance(){
        return INSTANCE;
    }
}

静态块方式和饥饿式比较类似

  • 线程安全的单例模式
public class SynchronizedSingleton {

    private static SynchronizedSingleton INSTANCE;

    private SynchronizedSingleton(){}

    public static synchronized  SynchronizedSingleton  getInstance(){
        if (instance == null){
            INSTANCE = new SynchronizedSingleton();
        }
        return INSTANCE;
    }
}

在访问该实例的静态方法前加上synchronized关键字就可以保证线程安全,但是这样的方式会让计算机付出一些额外的代价,从而影响了程序性能。为了尽量在不影响性能的情况下实现线程安全的单例模式,double checked locking方式就出现了,在这种方式下,synchronize块在方法内的if条件中使用,且使用了俩个if条件检查实例是否为null,确保只有一个实例被创建。代码如下

public class DoubleCheckSingleton {
    private static DoubleCheckSingleton INSTANCE;

    private DoubleCheckSingleton(){}

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

  • Bill Pugh的单例模式实现方式

    在java 5以前,java的内存模型在上述方式实现单例模式的时候存在许多问题,当多个线程同时获取实例的时候可能会失败。所以Bill Pugh提出了一种使用内部静态帮助类的方式来实现单例模式。

    public class BillPughSingleton{
        private BillPughSingleton(){}
      
          private static class SingletonHelper{
            private static final BillPughSingleton INSTANCE = new BillPughSingleton();
        }
      
          public static BillPughSingleton getInstance(){
            return SingletonHelper.INSTANCE;
        }
    }
    

    值得注意的是该单例模式的实例放在私有的内部静态类中,当外部类加载到内存中的时候,内部的帮助内并没有加载到内存,直到’getInstance()‘方法被调用,内部帮助类才会加载到内存中并创建实例。

    这种方式有很多好处,其保证了线程安全,且并没有使用synchronized关键字,所以也就不存在额外开销的问题。

  • Reflection对单例模式的破坏

反射能破坏以上所有的实现方式,其能确保获取到不同的实例。测试代码如下

public class ReflectionSingletonTest {
    public static void main(String[] args) {
        EagerSingleton eagerSingleton = EagerSingleton.getInstance();
        System.out.println(eagerSingleton.hashCode());


        Constructor [] constructors = EagerSingleton.class.getDeclaredConstructors();
        Arrays.asList(constructors).forEach(constructor -> {
            constructor.setAccessible(true);
            try {
                EagerSingleton anotherEagerSingleton = (EagerSingleton) constructor.newInstance();
                System.out.println(anotherEagerSingleton.hashCode());
            } catch (InstantiationException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        });

    }
}

运行结果如下

356573597
1747585824
  • enum实现单例模式

    为了解决反射场景下的单例模式,Joshua Bloch建议使用enum来实现单例模式,因为enum在java中被确保只实例化一次。

    public enum EnumSinglton {
        INSTANCE;
    
        public void dosomething(){
            System.out.println("enum singleton");
        }
    }
    

  • 序列化和单例

    在分布式系统中,我们需要在单例类中实现序列化接口,这样我们就能保存实例的状态,并在合适的时候根据状态恢复实例。

    public class SerializedSingleton implements Serializable{
    
        private SerializedSingleton(){
        }
    
        private static class SerializedSingletonHelper{
            public static final SerializedSingleton INSTANCE = new SerializedSingleton();
        }
    
        public static SerializedSingleton getInstance(){
            return SerializedSingletonHelper.INSTANCE;
        }
    }
    

    测试代码

    public class SerializedSingletonTest {
        public static void main(String[] args) throws IOException, ClassNotFoundException {
            ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"));
            oos.writeObject(SerializedSingleton.getInstance());
            System.out.println(SerializedSingleton.getInstance().hashCode());
    
            ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser"));
            SerializedSingleton anotherSingleton = (SerializedSingleton) ois.readObject();
            System.out.println(anotherSingleton.hashCode());
        }
    }
    

    测试结果

    325040804
    999966131
    
    Process finished with exit code 0
    

    以上测试结果证明了序列化违反了单例模式的原则,要解决这个问题我们需要在单例类中提供一个readResolve()方法的实现

    protected Object readResolve() {
        return getInstance();
    }
    

    这样当JVM从内存中反序列化地"组装"一个新对象时,就会自动调用这个 readResolve方法来返回我们指定好的对象了, 单例规则也就得到了保证.

    我们在运行测试代码 测试结果如下

    325040804
    325040804
    

单例模式线程安全的分析与测试

android中单例模式引起的内存泄漏

Java Singleton Design Pattern Best Practices with Examples

你可能感兴趣的:(单例模式漫谈)