7 种单例模式的写法

143 阅读7分钟

本文已参与「新人创作礼」活动,一起开启掘进创作之路。

单例模式

单例模式是 Java 中最简单,也是最基础,最常用的设计模式之一。在运行期间,保证某个类只创建一个实例,保证一个类仅有一个实例,并提供一个访问它的全局访问点。

单例模式分为饿汉模式和懒汉模式。

定义

确保一个类只有一个实例,并提供一个全局访问点

饿汉模式

所谓饿汉模式,是指在类加载的时候立即初始化,并创建单例对象。它能保证绝对线程安全,在线程还没有出现之前就实例化了。

代码实现

package com.study.design.Factory.Singleton;

public class HungrySingleton {
    /*静态实例在类加载时直接初始化创建*/
    private static final HungrySingleton instance = new HungrySingleton();
    /*构造方法私有化, 禁止外部进行创建该类实例*/
    private HungrySingleton(){};
    /*提供一个静态方法,返回唯一的实例对象*/
    public static HungrySingleton getInstance(){
        return instance;
    }
}

饿汉模式适用于单例对象较少的情况。这样写可以保证绝对的线程安全,执行效率比较高。缺点是所有对象类在加载时就实例化,这样一来,如果系统中有大量的单例对象存在,在系统初始化时会造成系统内存浪费,导致系统内存不可控。也就是说,系统初始化时,无论用或者不用的单例对象都会初始化,并占用内存空间。可以通过懒汉模式解决。

懒汉模式

为了解决饿汉模式可能带来的内存浪费,于是出现了懒汉模式的写法。懒汉模式也就是延迟加载,只有单例对象在被使用时才会初始化。

懒汉模式需要解决线程安全的问题。在多线程环境下,在单例对象还未初始化,如果同时多个线程访问单例对象,会造成对象重复创建的情况,这就违背了单例的初衷,也可能会造成程序执行异常。

synchronize代码实现

package com.study.design.Factory.Singleton;

public class LazySynchronizeSingleton {
    /*声明一个单例实例对象引用*/
    private static LazySynchronizeSingleton instance= null;
    /*私有化构造方法,禁止外部创建对象实例*/
    private LazySynchronizeSingleton(){}

    /**
     * 通过 synchronized 同步锁保证线程安全
     * 在使用单例对象的时候创建
     * @return
     */
    public synchronized static LazySynchronizeSingleton getInstance() {
        if (instance == null) {
            instance = new LazySynchronizeSingleton();
        }
        return instance;
    }
}

synchronized 固然可以保证线程的安全,但是在线程数量剧增的情况下,会导致大批线程阻塞,从而导致程序性能下降。而且,单例对象只有在第一次使用的时候才需要加锁 ,一旦实例化之后,以后每次使用并不需要同步加锁。通过下面双重锁机制来优化

双重检查锁

package com.study.design.Factory.Singleton;

public class LazyDoubleCheckSingleton {
    /**
     * volatile 关键字修饰单例保证线程之间的可见性,在多个线程同时调用时正确处理 instance 变量
     */
    private volatile static LazyDoubleCheckSingleton instance;
    /*私有化构造方法, 禁止外部创建实例对象*/
    private LazyDoubleCheckSingleton(){}
    /*提供一个静态方法,返回唯一的实例对象*/
    public static LazyDoubleCheckSingleton getInstance(){
        /**
         * 第一次 null 判断,多个线程可以同时执行此处,如果单例对象还未创建,只有一个会继续执行,而其他线程会阻塞
         * 如果单例对象已经创建,则会直接返回实例对象
         */
        if (instance == null) {
            // 只有一个线程会获取到 synchronized 锁,其他线程阻塞在此处
            synchronized (LazyDoubleCheckSingleton.class){
                /**
                 * 第二次 null 判断,这里是避免对象重复创建的关键
                 * 早些被阻塞的线程,获取到锁之后, 会再次进行一次 null 判断
                 * 此时,实例对象应该是已经被创建了 instance == null 为 false,不会重新创建对象
                 */
                if (instance == null)
                    instance  = new LazyDoubleCheckSingleton();
            }
        }
        // 返回单例对象
        return instance;
    }
}

双重检查锁机制,解决了线程安全问题和性能问题。

volatile 关键字保证了线程之间的可见性,双重 null 判断解决了对象重复实例化的问题。但只要是使用 synchronized 关键字总是要上锁,对程序还是有一定影响的。我们可以从类初始化的角度考虑,采用静态内部类的方式来实现单例模式。

静态内部类

package com.study.design.Factory.Singleton;

public class LazyStaticInnerClassSingleton {

    /*私有化构造方法,禁止外部创建实例对象*/
    private LazyStaticInnerClassSingleton(){}
    /*提供一个静态方法,返回唯一的实例对象*/
    private LazyStaticInnerClassSingleton geInstance(){
        return LazyHolder.INSTANCE;
    }
    /*默认是不加载内部类的*/
    private static class LazyHolder{
        private static final LazyStaticInnerClassSingleton INSTANCE = new LazyStaticInnerClassSingleton();
    }
}

利用 Java 虚拟机对类实例化的机制,内部类只有在被使用的时候才会初始化。这种模式兼顾了饿汉模式写法的内存浪费和 synchronized 性能问题,实现也比较简单。

禁止反射机制破坏单例模式

以上三种写法,构造方法只是使用了 private 关键字修复,并无其他任何处理。如果是这样,我们仍然可以通过使用反射机制来调用其构造方法,从而创建另外一个实例对象 ,所以以上三种还不能保证绝对的单例。

我们需要在构造方法中加上一个判断,如果实例已经创建过,直接抛出异常:

if (LazyHolder.INSTANCE != null){
    throw new RuntimeException("不允许创建多个实例");
}

关于序列化

对于一个单例模式的实例,在其被实例化后,重新被反序列化为一个对象的时候,应该与被序列化时的对象是同一个实例,这样才是更为严格的单例模式。

为了将上诉单例模式实现方法变成可序列化的,单纯的实现 Serrializble 接口,是不够的。必须声明所有的实例变量都是 transient 类型,并且提供一个 readResolve() 方法,否则每次反序列化时都会创建一个新的实例。

package com.study.design.Factory.Singleton;

import java.io.Serializable;

public class SerializableSingleton implements Serializable {

    private transient volatile static SerializableSingleton instance;
    /*私有化构造方法, 禁止外部创建实例对象*/
    private SerializableSingleton(){}
    /*提供一个静态方法,返回唯一的实例对象*/
    public static SerializableSingleton getInstance(){
        if (instance == null) {
            // 只有一个线程会获取到 synchronized 锁,其他线程阻塞在此处
            synchronized (LazyDoubleCheckSingleton.class){
                if (instance == null)
                    instance  = new SerializableSingleton();
            }
        }
        // 返回单例对象
        return instance;
    }

    private SerializableSingleton readResolve(){
        return instance;
    }

}

对于一个正在被反序列化的对象,如果它的类定义了一个 readResolve() 方法,并且具备正确的声明,那么在反序列化之后 ,新建对象上的 readResolve() 方法就会被调用,然后该方法返回的对象应用将会被返回,取代新建的对象。

但其实新对象还是被创建了的,只是创建之后没有任何引用会被垃圾回收器回收。

枚举

package com.study.design.Factory.Singleton;

public enum EnumSingleton {

    INSTANCE;

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

这种试下方式更加简洁,无偿提供了序列化机制。可以绝对放置多次实例化,也可以防止反射机制创建新的对象。枚举类型写法是 Effective Java 一书中推荐的写法。

容器式单例写法

虽然枚举式单例写法更加优雅,但是也会存在一些问题,因为它在类加载时将所有对象初始化都放在类内存中,这其实和饿汉式写法并无差异,不适合大量单例对象的场景。通过使用容器式单例写法可以解决大规模生产单例的问题。

package com.study.design.Factory.Singleton;

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 容器式单例模式
 * 决大规模生产单例的问题
 */
public class ContainerSingleton {
    /*定义一个容器*/
    private static Map<String,Object> ioc = new ConcurrentHashMap<>();
    /*构造方法私有化, 禁止外部进行创建该类实例*/
    private ContainerSingleton(){}
    /*提供一个静态方法,返回唯一的实例对象*/
    public static Object getBean(String className){
        synchronized (ioc){
            if (!ioc.containsKey(className)){
                Object obj = null;
                try {
                    obj = Class.forName(className).newInstance();
                    ioc.put(className,obj);
                } catch (Exception e) {
                    e.printStackTrace();
                }
                return obj;
            }else{
                return ioc.get(className);
            }
        }
    }
}