单例模式有这么多种写法(JAVA单例模式浅析)

懒汉单例

首先,写一个比较简单的懒汉模式的单例

public class SimpleSingleton {
    private static SimpleSingleton singleton;

    private SimpleSingleton() {
    }
    public static SimpleSingleton getInstance() {
        if (singleton == null) { // 1.判断是否为空
            singleton = new SimpleSingleton(); //2.进行初始化的操作
        }
        return singleton;
    }
}

懒汉的意思呢,就是我特别懒,我想吃东西的时候我才会去准备.

对应单例来说,就是每次使用都要尝试去创建单例对象。

以上的单例模式存在着一定的问题,首先都会想到的,就是多线程的问题。

因为1,2并不是原子性的操作

如果在多线程访问的情况下,其中一个线程执行到了第2步

在初始化成员变量时如果没有执行完,这个时候另外的线程 进行 第1步,判断成员变量依然为空,将执行instance 的 初始化,如此,会出现多个线程都进行初始化操作,从而获取不同的单例对象,就不符合单例的要求了。

既然原理了解后,可以进行下测试,借用IDEA的Debug 工具

单例模式有这么多种写法(JAVA单例模式浅析)

将第一个线程Thread0 阻塞在 instance 初始化时,继续执行其他线程 

 最终的结果,则为 Thread0 和 其它线程获取的 对象并非同一个

单例模式有这么多种写法(JAVA单例模式浅析)

如果存在多线程问题,我们想到的第一方法就是 加锁,如下所示

public static SimpleSingleton getInstance() {
        synchronized (SimpleSingleton.class) {
            if (singleton == null) { // 1.判断是否为空
                singleton = new SimpleSingleton(); //2.进行初始化的操作
            }
        }
        return singleton;
    }

这样第1步和第2步就变成了原子操作,但是也导致了多线程的串行化,对效率存在一定的影响,

因在此基础上又出现了一种解决方案,通过Double Checking Locking(DCL) 将 第1步前置,相当于instance初始化完成后的其他线程获取实例时并不会存在加锁和解锁的开销,毕竟锁还是比较影响性能的。

经过修改后,代码如下所示

public static SimpleSingleton getInstance() {
        if (null == singleton) {
            synchronized (SimpleSingleton.class) {
                if (singleton == null) { // 1.判断是否为空
                    singleton = new SimpleSingleton(); //2.进行初始化的操作
                }
            }
        }
        return singleton;
    }

但以上代码 依然会存在问题,这就要回到之前第2步 在底层执行的操作问题,分为三个操作:

(1)分配一块内存

(2)在内存上初始化成员变量

(3)将instance 引用指向内存

由于操作(2) 和 (3)之前会存在 重排序,即先将instance执向内存,在初始化成员变量。

此时,另外一个线程可能拿到一个未完全初始化的对象。

如果直接访问单例对象中的成员变量,就可能出错。

造成了一个典型的“构造函数溢出”问题。

解决方法也比较简单,使用volatile 对instance 进行修饰,代码如下:

private volatile static SimpleSingleton singleton;

    private SimpleSingleton() {
    }
    public static SimpleSingleton getInstance() {
        if (null == singleton) {
            synchronized (SimpleSingleton.class) {
                if (singleton == null) { // 1.判断是否为空
                    singleton = new SimpleSingleton(); //2.进行初始化的操作
                }
            }
        }
        return singleton;
    }

这样多线程安全的问题就解决了。至于 volatile 关键字的作用,这要讲起来内容可就多了,暂时只谈单例相关的/

当然懒汉的单例模式在实际项目中使用的并不是很多,下面分析一下饿汉的单例模式

饿汉单例

饿汉的意思就是我一直都是饿的,因此只好把食物都准备好,我想吃就吃;

public class HungrySingleton {

    private final static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
    }

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

看样子饿汉比较简单哈,示例可以参考 Java 中的Runtime 类,非常典型的饿汉模式

单例模式有这么多种写法(JAVA单例模式浅析)

存在问题,序列化和反序列化问题

public class HungrySingleton implements Serializable {

    private final static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return instance;
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        HungrySingleton instance = HungrySingleton.getInstance();
        ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singletonFile"));
        out.writeObject(instance);

        ObjectInputStream inputStream = new ObjectInputStream(new FileInputStream("singletonFile"));
        HungrySingleton newInstance = (HungrySingleton) inputStream.readObject();

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

执行结果:instance 和 newInstance 并非同一个对象

单例模式有这么多种写法(JAVA单例模式浅析)

 这就需要去仔细走读下反序列化的源码了,通过代码走读会发现 反序列化 通过了 反射的方法创建了对象,则造成了反序列化后的对象与序列化之前并不是同一个

单例模式有这么多种写法(JAVA单例模式浅析)

具体代码走读,可参考链接 单例、序列化和readResolve()方法 - 掘金

解决方案的代码

public class HungrySingleton implements Serializable {

    private final static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
    }

    public static HungrySingleton getInstance() {
        return instance;
    }   

    // 用于解决序列化和反序列化的问题
    public Object readResolve() {
        return instance;
    }
}

在反序列化时,会调用readResolve方法,通过反射获取原来的对象,对进行替换,这样,就可以保证序列化前后的对象是同一个了。

既然在反序列化过程中,出现了这个问题,那么在项目中也有可能有人会使用反射来创建该对象,如此就又会出现问题,

简单的解决办法就是,禁止该实例通过反射进行创建

public class HungrySingleton implements Serializable {

    private final static HungrySingleton instance = new HungrySingleton();

    private HungrySingleton() {
        if (instance != null) {
            throw new RuntimeException("禁止通过反射创建");
        }
    }

    public static HungrySingleton getInstance() {
        return instance;
    }

    public Object readResolve() {
        return instance;
    }
}

当然,这个方法不能用于懒汉模式,因为懒汉模式是属于延迟创建,不能创建时进行抛错吧。

静态内联单例

饿汉单例还存在着延迟加载的问题,例如 instance 会随项目一同初始化,有可能在之后的项目运行中,很长时间都不会用到,但是却一直常驻内存,浪费资源 。

可以通过 Inner Class 的方法解决

public class StaticInnerClassSingleton {

    public static class InnerClass {
        static {
            System.out.println("inner class");
        }
        private static StaticInnerClassSingleton staticInnerClassSingleton;
    }

    private StaticInnerClassSingleton() {}

    public static StaticInnerClassSingleton getInstance() {
        System.out.println("get instance");
        return InnerClass.staticInnerClassSingleton;
    }

    public static void main(String[] args) {
        StaticInnerClassSingleton.getInstance();
    }
}

这样就保证了当需要该对象时才会在内存中初始化。

容器单例

public class ContainerSingleton {

    private static HashMap map =  new HashMap<String, Object>();
    private ContainerSingleton() {}
    public static void putInstance(String key, Object o) {
        if (key != null && key.length() > 0 && o != null){
            map.put(key,o);
        }
    }

    public static Object getInstance(String key) {
        return map.get(key);
    }

}

直接上代码吧,这个也是项目中比较常见的,借用map的key-value结构,保证单例的唯一性。

具体的示例可参考 Spring 中的  SingletonBeanRegistry 接口

单例模式有这么多种写法(JAVA单例模式浅析)

枚举单例

最简单,最安全的写法

public enum EnumSingleton {
    INSTANCE {
    };

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

 ThreadLocal 单例

和线程相关的单例模式

public class ThreadLocalSingleton {
    private static final ThreadLocal<ThreadLocalSingleton> threadLocal = new ThreadLocal(){
        @Override
        protected Object initialValue () {

            return new ThreadLocalSingleton();
        }
    };

    private ThreadLocalSingleton()
    {}

    public static ThreadLocalSingleton getInstance(){
        return threadLocal.get();
    }
}

Mybatis 中的 ErrorContext使用了该单例模式

单例模式有这么多种写法(JAVA单例模式浅析)

 不过要注意垃圾回收的问题

原文链接:单例模式有这么多种写法(JAVA单例模式浅析)

上一篇:【赵强老师】Redis的慢查询日志


下一篇:单例模式的优缺点和使用场景