如何正确地写出单例模式

什么是单例模式

一个类在JVM只有一个实例,并且提供一个全局访问入口。单例模式适用无状态的工具类,比如日志工具、字符串工具;
还有全局信息类,比如全局计数、环境变量;在Java中如下类库是适用单例模式:

  • java.lang.Runtime#getRuntime();
  • java.awt.Desktop#getDesktop();
  • java.lang.System#getSecurityManager();

单例模式的作用:节省内存;节省计算;结果的正确,比如全局计数器;方便管理。其实现方式很多,但不管何种实现方式,共同点:

  • 私有的构造函数;
  • 私有静态类对象;
  • 公有静态方法,唯一一个访问私有静态对象实例的方法。

单例模式实现方式

饿汉式或静态代码模块式

public class EagerSingleton {
    private static final EagerSingleton instance = new EagerSingleton();

    private EagerSingleton() {
    }
    public static EagerSingleton getInstance() {
        return instance;
    }
}

另外一种变种的写法是

/**
 * 饿汉式的变种,静态代码形式
 */
public class StaticBlockSingleton {
    private static final StaticBlockSingleton singleton;

    private StaticBlockSingleton() {}

    static {
        singleton = new StaticBlockSingleton();
    }
    public StaticBlockSingleton getInstance() {
        return singleton;
    }
}

懒加载模式

  • 线程不安全的懒加载模式
/**
 * 懒汉式:只适合单线程模式
 */
public class LazySingleton {
    private static LazySingleton singleton;

    private LazySingleton() {}

    public LazySingleton getInstatnce(){
        if (null == singleton) {
            singleton = new LazySingleton();
        }
        return singleton;
    }
}
  • 线程安全的懒加载模式
/**
 * @description: 线程安全的懒加载单例模式
 * @author: agentzhu
 */
public class ThreadSafeLazySingleton {
    private static ThreadSafeLazySingleton singleton;

    private ThreadSafeLazySingleton() {}

    /**
     * 加锁粒度大,多线程环境不能同时访问,并发效率低
     * @return
     */
    public synchronized ThreadSafeLazySingleton getInstatnce(){
        if (null == singleton) {
            singleton = new ThreadSafeLazySingleton();
        }
        return singleton;
    }
}

双重检验锁模式(double checked locking pattern)

双重检验锁模式也是一种懒加载模式,是一种对安全型懒加载模式的优化,具体如下:

public class Singleton {
    // 关键点1:声明成 volatile,禁止指令重排序
    private volatile static Singleton instance;
    private Singleton (){}

    public static Singleton getSingleton() {
        // 关键点2: 提高并发性
        if (instance == null) {
            synchronized (Singleton.class) {
                // 关键点3:防止创建多个实例
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

volatile关键字的原因,new Singleton()JVM的实现三个不步骤,如下图:

20200328_singleton_1.jpg

由于指令重排,在多线程环境中易于引起使用未初始化完全的的对象,比如下图:

20200328_singleton_2.jpg

所以使用volatile关键字,其有两个特性:一个是可见性;另外一个是禁止指令重排序优化。而禁止指令重排序优化具体来说,在volatile变量的赋值操作后面会有一个内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前。比如上面的例子,取操作必须在执行完1-2-3 之后或者1-3-2之后,不存在执行到1-3然后取到值的情况。

静态内部类 static nested class

Java 5 以前的版本使用volatile的双检锁还是有问题的。其原因是Java 5以前的JMM(Java 内存模型)是存在缺陷的,即时将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。所以Bill Pugh提供了一种静态内部类实现方式。

public class Singleton {  
    private static class SingletonHelper {  
        private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  

    public static final Singleton getInstance() {  
        return SingletonHelper.INSTANCE;
    }  
}

枚举式

上面单例模式的实现方式都存在两个问题:

  1. 序列化与反序列化创建多个实例;
  2. 反射,创建多个实例;

序列化与反序列化创建多个实例问题

public class DoubleCheckSingleton implements Serializable {
    private static final long serialVersionUID = -7975945444590877513L;

    // 不用volatile 修饰,会出现不完全初始化的状态的实例
    private static volatile DoubleCheckSingleton singleton;

    private DoubleCheckSingleton() {}

    public static DoubleCheckSingleton getInstance() {
        if (null == singleton) { //关键点1: 提高并发效率
            synchronized (DoubleCheckSingleton.class) {
                if (null == singleton) { // 关键点2:防止创建多个实例
                    // 存在三个步骤,顺利不是固定的[编译器的重排序优化],可能是:1,2,3;1,3,2
                    // 1.给singleton分配内存空间
                    // 2.调用Singleton的构造函数等来初始化singleton
                    // 3.将singleton对象指向分配的内存空间(执行完此步singleton就不是null了)
                    singleton = new DoubleCheckSingleton();
                }
            }
        }
        return singleton;
    }

    private int value = 10;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}

测试结果:

20
10

为此在DoubleCheckSingleton需要重写readResolve,它会在反序列化的时候被调用,所以我们可以在此方法中返回已有的对象实例。

protected Object readResolve() {
    return singleton;
}

测试结果

20
20

反射创建多个实例

创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。

public class SingletonReflectionDemo {
    public static void main(String[] args) throws Exception {
        DoubleCheckSingleton singleton = DoubleCheckSingleton.getInstance();
        Constructor constructor = singleton.getClass().getDeclaredConstructor(new Class[0]);
        constructor.setAccessible(true);
        DoubleCheckSingleton singleton2 = (DoubleCheckSingleton) constructor.newInstance();
        if (singleton == singleton2) {
            System.out.println("Two objects are same");
        } else {
            System.out.println("Two objects are not same");
        }
        singleton.setValue(1);
        singleton2.setValue(2);
        System.out.println(singleton.getValue());
        System.out.println(singleton2.getValue());
    }
}

测试结果

Two objects are not same
1
2

我们再来看看枚举方式的单例实现方式,虽然在加载类的时候实例化,并且只有一个实例对象。存在的问题达不到懒加载的作用的。但是绝对解决上述提到的两个问题。首先,我们来看看如何使用枚举的方式来实现单例模式,然后再一一测试。

public enum EnumSingleton {
    INSTANCE;
    int value;

    public int getValue() {
        return value;
    }

    public void setValue(int value) {
        this.value = value;
    }
}
  • 验证序列化与反序列化创建多个实例的问题
public class EnumSingletonSerializeDemo {
    private static EnumSingleton instanceOne = EnumSingleton.INSTANCE;

    public static void main(String[] args) {
        try {
            // Serialize to a file
            ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
                    "filename.ser"));
            out.writeObject(instanceOne);
            out.close();
            instanceOne.setValue(20);

            // deserialize from a file
            ObjectInput in = new ObjectInputStream(new FileInputStream(
                    "filename.ser"));
            EnumSingleton instanceTwo = (EnumSingleton) in.readObject();
            in.close();

            System.out.println(instanceOne.getValue());
            System.out.println(instanceTwo.getValue());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        }
    }
}
  • 验证反射创建多个实例问题

反射进行创建枚举类的会直接报错,无法创建的。原因:枚举被设计成是单例模式,即枚举类型会由JVM在加载的时候,实例化枚举对象,你在枚举类中定义了多少个就会实例化多少个,JVM为了保证每一个枚举类元素的唯一实例,是不会允许外部进行new的,所以会把构造函数设计成private,防止用户生成实例,破坏唯一性。

An enum type has no instances other than those defined by its enum constants. It is a compile-time error to attempt to explicitly instantiate an enum type. The final clone method in Enum ensures that enum constants can never be cloned, and the special treatment by the serialization mechanism ensures that duplicate instances are never created as a result of deserialization. Reflective instantiation of enum types is prohibited. Together, these four things ensure that no instances of an enum type exist beyond those defined by the enum constants.

总结

实现方式 优点 缺点
饿汉式/静态代码模块式 简单,在类加载的时完成实例化;无线程同步问题 不使用此实例,也会在类加载的时候完成实例化,浪费内存;存在序列化、反射创建多个实例问题
懒加载或者线程安全的懒加载式 获取实例的时候才初始化,但只适合单线程情况下使用 线程不安全或者并发度低;存在序列化、反射创建多个实例问题
双重检验锁模式 线程安全;懒加载; 代码复杂度高,易写错;存在序列化、反射创建多个实例问题
枚举式 线程安全;代码简单,流行度不高 不是懒加载,不存在序列化,反射创建多个实例问题

参考资料

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,125评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,293评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,054评论 0 351
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,077评论 1 291
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,096评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,062评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,988评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,817评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,266评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,486评论 2 331
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,646评论 1 347
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,375评论 5 342
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,974评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,621评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,796评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,642评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,538评论 2 352

推荐阅读更多精彩内容