浅谈Java设计模式之单例模式

506 阅读5分钟

介绍单例模式

单例模式(Singleton Pattern):确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法。单例模式是一种对象创建型模式。

单例模式的三要点

  • 构造方法私有化;
  • 实例化的变量引用私有化;
  • 获取实例的方法共有;

单例模式的写法

饿汉式

public class Singleton{
    
    private static Singleton singleton = new Singleton();
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        return singleton;
    }
}

优点:简单,使用时没有延迟;在类加载时就完成实例化,线程安全。

缺点:没有懒加载,启动较慢;如果由始至终都没有使用这个实例则会造成内存的浪费。

懒汉式

public class Singleton{
    
    private static Singleton singleton;
    
    private Singleton(){}
    
    //同步方法,解决了多线程下可能导致产生多个实例
    public static synchronized Singleton getInstance(){
        if(singleton == null){
            singleton = new Singleton();
        }
        return instance;
    }
}

优点:懒加载,启动速度快,如果由始至终没有使用该实例则不会初始化该类,节约资源;线程安全

缺点synchronized对整个getInstance()方法都进行了同步,性能较差

双重检查锁

public class Singleton{
    /*
    	volatile关键字的作用:
    		1.保证了不同线程对这个变量进行操作时的可见性
    		2.禁止进行指令重排序
    	由于JVM具有指令重排的特性,在多线程环境下可能出现singleton已经赋值但还没初始化的情况,
    	导致一个线程获得还没有初始化的实例,为了避免这个情况,给singleton加关键字volatile。
    */
    private static volatile Singleton singleton;
    
    private Singleton(){}
    
    public static Singleton getInstance(){
        if(singleton == null){
            synchronized(Singleton.class){
             	if(singleton == null){
                    singleton = new Singleton();
                }   
            }
        }
        return instance;
    }
}

优点:线程安全;延迟加载;效率较高。

静态内部类

public class Singleton{
    
    private Singleton(){}
    
    private static class SingletonInstance{
        private static final Singleton singleton = new Singleton();
    }
    
    public static Singleton getInstance(){
     	return SingletonInstance.singleton;   
    }
}

优点:线程安全,延迟加载,效率高。

静态内部类的方式利用了类加载机制来保证线程安全,只有在第一次调用getInstance()方法时才会加载SingletonInstance内部类完成Singleton的实例化,因此也有懒加载的效果。

枚举

public enum Singleton{
    SINGLETON;
    
    public void whateverMethod(){
        
    }
}

优点:写法简单,线程安全,还能防止反序列化重新创建新的对象。

单例模式的安全性

通过反射破坏单例

public class BreakSingleton{
    public static void main(String[] args){
        Singleton singleton1 = Singleton.getInstance();
        
        //通过反射获取该单例类的构造器,并修改该构造器的访问权限,然后进行实例化
        Constructor constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton singleton2 = (Singleton) constructor.newInstance();
        
        //打印结果为false,说明单例模式已被破坏
        System.out.println(singleton1 == singleton2);
    }
}

如何防止通过反射破坏单例模式?

为了避免单例模式被破坏,Java5提供了枚举,举个例子:

public enum Singleton{
    SINGLETON;
    
    public static Singleton getInstance(){
        return SINGLETON;
    }
}

这个时候如果再通过反射获取类的构造器:

Constructor constructor = Singleton.class.getDeclaredConstructor();

会抛出NoSuchMethodException异常:

Exception in thread "main" java.lang.NoSuchMethodException: com.honzooban.demo.<init>()
    at java.lang.Class.getConstructor0(Class.java:3082)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at com.honzooban.demo.BreakSingleton.main(BreakSingleton.java:11)

对于枚举类,JVM会自动对其进行实例化,其构造方法由JVM在实例化过程中进行调用,因此我们在代码中是获取不到enum类的构造方法的。

通过序列化破坏单例

public class BreakSingleton{
    public static void main(String[] args){
        Singleton singleton1 = Singleton.getInstance();
        
        //instance2将从instance1序列化后,反序列化而来
        Singleton instance2 = null;
        ByteArrayOutputStream bout = null;
        ObjectOutputStream out = null;
        try {
            bout = new ByteArrayOutputStream();
            out = new ObjectOutputStream(bout);
            out.writeObject(instance1);

            ByteArrayInputStream bin = new ByteArrayInputStream(bout.toByteArray());
            ObjectInputStream in = new ObjectInputStream(bin);
            instance3 = (Singleton) in.readObject();
        } catch (Exception e) {
        } finally {
            //关闭流
        }

        //打印结果为false,说明单例模式已被破坏
        System.out.println(singleton1 == singleton2);
    }
}

如何防止通过序列化破坏单例模式?

让单例类实现Serializable接口,并在该类中加入readResolve()方法,在方法中返回实例即可。举个例子:

public class Singleton implements Serializable {
    
    //实现单例的代码在这里省略..
    
    public Object readResolve() throws ObjectStreamException {
        return singleton;
    }
}

因为在反序列化的时候,JVM会自动调用readResolve()这个方法,我们只需要在这个方法中替换掉从流中反序列化回来的对象即可避免单例模式被破坏。

总的来说,实现单例模式推荐使用枚举类实现,理由如下:

  • 枚举单例写法简单

  • 线程安全,懒加载

  • 枚举单例自己能避免序列化攻击

  • 枚举单例自己能避免反射攻击,因为反射不支持创建枚举对象

    Constructor类的 newInstance方法中会判断是否为 enum,若是会抛出异常

    public T newInstance(Object ... initargs)
            throws InstantiationException, IllegalAccessException,
                   IllegalArgumentException, InvocationTargetException
        {
            if (!override) {
                if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                    Class<?> caller = Reflection.getCallerClass();
                    checkAccess(caller, clazz, null, modifiers);
                }
            }
            //不能通过反射创建enum对象
            if ((clazz.getModifiers() & Modifier.ENUM) != 0)
                throw new IllegalArgumentException("Cannot reflectively create enum objects");
            ConstructorAccessor ca = constructorAccessor;   // read volatile
            if (ca == null) {
                ca = acquireConstructorAccessor();
            }
            @SuppressWarnings("unchecked")
            T inst = (T) ca.newInstance(initargs);
            return inst;
        }
    

单例模式的总结

单例模式的主要优点

  • 单例模式提供了对唯一实例的受控访问。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式可以提高系统的性能。
  • 允许可变数目的实例。基于单例模式我们可以进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例,既节省系统资源,又解决了单例对象共享过多有损性能的问题。

单例模式的主要缺点

  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。
  • 如果实例化的共享对象长时间不被利用,系统可能会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致共享的单例对象状态的丢失。

单例模式的典型应用

JDk Runtime 饿汉单例

public class Runtime {
    //类加载时就完成对Runtime的实例化
    private static Runtime currentRuntime = new Runtime();
	
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    //隐藏构造器
    private Runtime() {}

    //..
}

参考:

结城浩:《图解设计模式》

Coosee:Java对象的序列化和反序列化

DobyJin:Java实现单例模式(懒汉式、饿汉式、双重检验锁、静态内部类方式、枚举方式)

Javadoop:单例模式的安全性