单例模式

Posted weixk

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了单例模式相关的知识,希望对你有一定的参考价值。

一、单例模式介绍

1、定义与类型

定义:保证一个类仅有一个实例,并提供一个全局访问点
类型:创建型

2、适用场景

想确保任何情况下都绝对只有一个实例

3、优点

在内存里只有一个实例,减少了内存开销
可以避免对资源的多重占用
设置全局访问点,严格控制访问

4、缺点

没有接口,扩展困难

5、重点

私有构造器:禁止从单例类外部构造对象
线程安全
延迟加载:使用时才创建
序列化和反序列化安全:序列化和反序列化会对单例模式进行破坏
反射:防御反射攻击

二、代码示例

1、懒汉式及多线程

注重延迟加载:

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton(){
    }
    public static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

但是存在线程安全问题,所以可以增加synchronized:

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton(){
    }
    public synchronized static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

2、Double Check双重检查

但是 synchronized 对性能存在影响,所以可以使用Double Check双重检查:

public class LazyDoubleCheckSingleton {
    private static LazyDoubleCheckSingleton lazyDoubleCheckSingleton = null;
    private LazyDoubleCheckSingleton(){
    }
    public static LazyDoubleCheckSingleton getInstance(){
        if(lazyDoubleCheckSingleton == null){
            synchronized (LazyDoubleCheckSingleton.class){
                if(lazyDoubleCheckSingleton == null){
                    lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();
                }
            }
        }
        return lazyDoubleCheckSingleton;
    }
}

其中

lazyDoubleCheckSingleton = new LazyDoubleCheckSingleton();

这一句代码包含三个步骤:
1.分配内存给这个对象
2.初始化对象
3.设置lazyDoubleCheckSingleton 指向刚分配的内存地址
在java语言规范中 允许在单线程内,不会改变单线程执行结果的重排序。
所以 2和3步可能会存在指令重排序,在单线程中,不会影响执行结果:
技术图片

此时在多线程中:
技术图片

此时线程1访问对象,但是对象在线程0中还没有初始化完成,可能就会报异常。
解决方案:
方案1、不允许2、3步骤重排序:
使用volatile关键字:

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

使用了volatile后,所有线程都可以看到共享内存的最新状态,保证了内存的可见性。用volatile关键字修饰的变量,在进行写操作时,会多出一些汇编代码,将当前处理器缓存行的数据写回到内存,其中涉及到缓存一致性协议。

方案2、允许重排序,但不允许其他线程看到这个重排序,即静态内部类

3、静态内部类

基于类初始化的延迟加载解决方案

public class StaticInnerClassSingleton {
    private static class InnerClass{
        private static StaticInnerClassSingleton staticInnerClassSingleton = new StaticInnerClassSingleton();
    }
    public static StaticInnerClassSingleton getInstance(){
        return InnerClass.staticInnerClassSingleton;
    }
    private StaticInnerClassSingleton(){
    }
}

原理:存在Class对象的初始化锁,并且非构造线程,是看不到指令重排序的。
线程0初始化Class,线程1看不到初始化过程。所以静态内部类这种方法的核心在于InnerClass这个类的对象初始化锁
技术图片

补充:类在以下几种情况下被初始化,1.实例被创建(new、反射、序列化),2.静态方法被调用,3.静态成员被赋值,4.非常量静态成员被使用,5.顶级类中有嵌套的断言语句,6.子类被初始化

4、饿汉式

最简单的写法:

public class HungrySingleton {
    private final static HungrySingleton hungrySingleton;
    static{
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

优点是类加载的时候就完成了初始化,避免了线程同步的问题
缺点是没有延迟加载的效果,可能造成累成内存浪费
饿汉与懒汉之间最大的区别就是延迟加载:饿汉式很饿,一上来就想吃东西,马上就把对象创建好了;而懒汉式非常懒,不用它的时候都不会创建这个对象。

5、序列化破坏单例模式

以下序列化和反序列化 将会破坏单例模式:

// 实现序列化接口
public class HungrySingleton implements Serializable {
    private final static HungrySingleton hungrySingleton;
    static{
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
}

测试类:

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("singleton_file"));
        oos.writeObject(instance);
        File file = new File("singleton_file");
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream(file));
        HungrySingleton newInstance = (HungrySingleton) ois.readObject();
        System.out.println(instance);
        System.out.println(newInstance);
    }
}

反序列化是通过反射生成对象,反射时会判断是否存在并调用readResolve方法
技术图片

所以可通过增加readResolve方法防止反序列化:

public class HungrySingleton implements Serializable{
    private final static HungrySingleton hungrySingleton;
    static{
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
    private Object reaResolve(){
        return hungrySingleton;
    }
}

但是在这个过程中,仍然被创建了新的对象,只是最后没有返回而已。

6、反射攻击

public class Test {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        Class<HungrySingleton> hungrySingletonClass = HungrySingleton.class;
        Constructor<HungrySingleton> declaredConstructor = hungrySingletonClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        HungrySingleton instance = HungrySingleton.getInstance();
        HungrySingleton newInstance = declaredConstructor.newInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

对于饿汉式单例、静态内部类单例,因为是在类初始化时就创建了对象,所以可在构造器中进行反射防御:

public class HungrySingleton implements Serializable{
    private final static HungrySingleton hungrySingleton;
    static{
        hungrySingleton = new HungrySingleton();
    }
    private HungrySingleton(){
        // 反射防御、第二次调用就会报错	
        if(hungrySingleton != null){
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }
    public static HungrySingleton getInstance(){
        return hungrySingleton;
    }
    private Object readResolve(){
        return hungrySingleton;
    }
}

而对于不是在类初始化时创建对象的单例模式,则无法防御反射攻击,例如懒汉式单例模式:

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private LazySingleton(){
        if(lazySingleton != null){
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }
    public synchronized static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

因为在被反射攻击的时候,单例可能还没有被创建,所以会产生不同实例,测试类:

public class Test {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {

        // 反射攻击
        Class<LazySingleton> lazySingletonClass = LazySingleton.class;
        Constructor<LazySingleton> declaredConstructor = lazySingletonClass.getDeclaredConstructor();
        declaredConstructor.setAccessible(true);
        // 先反射
        LazySingleton newInstance = declaredConstructor.newInstance();
        // 后取单例,构造器的判断没有起到想要的作用
        LazySingleton instance = LazySingleton.getInstance();

        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}

可以增加信号量进行控制:

public class LazySingleton {
    private static LazySingleton lazySingleton = null;
    private static boolean flag = true;
    private LazySingleton(){
        if (flag){
            flag = false;
        } else {
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }
    public synchronized static LazySingleton getInstance(){
        if(lazySingleton == null){
            lazySingleton = new LazySingleton();
        }
        return lazySingleton;
    }
}

但是信号量仍然可以被修改,以达到反射破坏:

public class Test {
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InstantiationException, NoSuchFieldException, InvocationTargetException {
        Class objectClass = LazySingleton.class;
        Constructor c = objectClass.getDeclaredConstructor();
        c.setAccessible(true);

        LazySingleton o1 = LazySingleton.getInstance();

        Field flag = o1.getClass().getDeclaredField("flag");
        flag.setAccessible(true);
        flag.set(o1,true);

        LazySingleton o2 = (LazySingleton) c.newInstance();

        System.out.println(o1);
        System.out.println(o2);
        System.out.println(o1==o2);
    }
}

7、Enum枚举单例

枚举类型天然的可序列化机制,能够强有力得保证不会多次实例化的情况。即使在复杂的序列化或者反射攻击下,枚举模式都没有问题。

public enum EnumInstance {
    INSTANCE{
        protected  void printTest(){
            System.out.println("Geely Print Test");
        }
    };
    protected abstract void printTest();
    private Object data;
    public Object getData() {
        return data;
    }
    public void setData(Object data) {
        this.data = data;
    }
    public static EnumInstance getInstance(){
        return INSTANCE;
    }
}

在ObjectInputStream中,对于枚举类型,是通过枚举类直接获得唯一的枚举常量,没有创建新的对象,维护了枚举的单例属性:
技术图片

而对于反射,在调用
objectClass.getDeclaredConstructor();
时会直接报错:
java.lang.NoSuchMethodException
原因在于Enum本身就只有一个构造器:
技术图片

而如果调用

Constructor constructor = objectClass.getDeclaredConstructor(String.class,int.class);
constructor.setAccessible(true);
EnumInstance instance = (EnumInstance) constructor.newInstance("11",22);

也会直接报错:java.lang.IllegalArgumentException: Cannot reflectively create enum objects

如果通过jad反编译枚举类,可以看到:1.class类为final的;2.构造器为private;3.声明的枚举对象是static和final的;4.枚举对象在static代码块中实例化
所以枚举单例是最安全的单例模式

8、容器单例

public class ContainerSingleton {

    private ContainerSingleton(){
    }
    private static Map<String,Object> singletonMap = new HashMap<String,Object>();

    public static void putInstance(String key,Object instance){
        if(StringUtils.isNotBlank(key) && instance != null){
            if(!singletonMap.containsKey(key)){
                singletonMap.put(key,instance);
            }
        }
    }
    public static Object getInstance(String key){
        return singletonMap.get(key);
    }
}

容器单例与享元模式相似
优点:统一管理,节省资源,相当于缓存
缺点:存在线程安全问题

9、ThreadLocal线程单例

public class ThreadLocalInstance {
    private static final ThreadLocal<ThreadLocalInstance> threadLocalInstanceThreadLocal
             = new ThreadLocal<ThreadLocalInstance>(){
        @Override
        protected ThreadLocalInstance initialValue() {
            return new ThreadLocalInstance();
        }
    };
    private ThreadLocalInstance(){

    }
    public static ThreadLocalInstance getInstance(){
        return threadLocalInstanceThreadLocal.get();
    }
}

这个单例 并不能保证整个应用全局唯一,但能保存线程唯一。
ThreadLocal会为每一个线程提供一个变量副本,本身是基于ThreaLocalMap实现的,维持了线程间的隔离。原理是以空间换时间的方式,会创建很多对象,在一个线程里会创建唯一的一个对象。在多线程访问的时候,彼此不会相互影响。

三、源码示例

1、JDK中的Runtime:饿汉式

技术图片

2、JDK中的Desktop:懒汉式+容器式+线程安全控制

技术图片

3、spring

技术图片

4、mybatis:ThreadLocal

技术图片

以上是关于单例模式的主要内容,如果未能解决你的问题,请参考以下文章

常用代码片段

性能比较好的单例写法

片段作为 Android 中的单例

单例片段或保存网页视图状态

你熟悉的设计模式都有哪些?写出单例模式的实现代码

单例模式以及静态代码块