设计模式(5) : 单例模式

定义:

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

类型:

创建型

使用场景

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

coding

单例模式需要注意的点
  1. 私有构造器
  2. 线程安全
  3. 延迟加载
  4. 序列化和反序列化安全
  5. 防止反射机制破坏单例模式

单例模式的N种写法

1. 饿汉式
  • 实现简单
  • 线程安全
public class HungrySingleton {
    private static HungrySingleton INSTANCE = new HungrySingleton();
    private HungrySingleton(){}
    public static HungrySingleton getInstance() {
        return INSTANCE;
    }
}

初始化类时就加载, 如果不使用就会浪费内存

2. 懒汉式
  • 实现简单
  • 延迟加载
public class LazySingleton {
    private static LazySingleton INSTANCE;
    private LazySingleton(){}
    public static LazySingleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new LazySingleton();
        }
        return INSTANCE;
    }
}

懒汉式的优点是延迟加载,等到需要的时候才会创建实例, 但他是线程不安全的, 当两个线程同时进入getInstance方法时, 线程1和2都执行到
INSTANCE == null, 此时INSTANCE如果还未创建, 将会创建两个实例

线程不安全可以通过多线程调试来复现
IDEA多线程调试

3. 懒汉式 + 同步锁
  • 延迟加载
  • 线程安全
public class LazySyncSingleton {
    private static LazySyncSingleton INSTANCE;
    private LazySyncSingleton(){}
    public static synchronized LazySyncSingleton getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new LazySyncSingleton();
        }
        return INSTANCE;
    }
}

这样子线程就安全了,但是消耗了不必要的同步资源,不推荐这样使用。

4. DCL模式(Double CheckLock) - 双重检查
  • 延迟加载
  • 线程安全
  • 相对懒汉式 + 同步锁的方式只在初始化时才会加锁, 提高了效率
public class LazyDoubleCheckSingleton {
    private static LazyDoubleCheckSingleton INSTANCE;
    private LazyDoubleCheckSingleton(){}
    public static synchronized LazyDoubleCheckSingleton getInstance() {
        if (INSTANCE == null) {
            synchronized (LazyDoubleCheckSingleton.class) {
                if (INSTANCE == null) {
                    INSTANCE = new LazyDoubleCheckSingleton();
                }
            }
        }
        return INSTANCE;
    }
}

通过两个判断,第一层是避免不必要的同步,第二层判断是否为null。
可能会出现DCL模式失效的情况。

DCL模式失效:
singleton=new Singleton()这句话执行的时候,会进行下列三个过程:

  1. 分配内存。
  2. 初始化构造函数和成员变量。
  3. 将对象指向分配的空间。

由于JMM(Java Memory Model)的规定,可能会对单线程情况下不影响程序运行结果的指令进行重排序, 因此可能会出现1-2-3和1-3-2两种情况。
所以,就会出现线程A进行到1-3时,就被线程B取走,此时B线程拿到的是一个还未初始化完成的对象, 这时就出现了异常, DCL模式就失效了。

可以使用 volatile 来解决重排序问题

volatile 有禁止指令重排序的功能. volatile详解

private volatile static LazyDoubleCheckSingleton INSTANCE;
5.内部类实现单例
  • 线程安全
  • 实现简单
  • 延迟加载
public class StaticInnerClassSingleton {
    private static class SingletonHolder {
        private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
    }
    private StaticInnerClassSingleton(){}
    public static StaticInnerClassSingleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

利用了class的初始化锁保证只有一个线程能加载内部类
只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance (只有拿到初始化锁的线程才会初始化对象)

6.枚举
  • 实现简单
  • 线程安全
  • 避免反序列化破坏单例
  • 避免反射攻击
public enum EnumSingleton {
    INSTANCE(new Object());
    EnumSingleton(Object data) {
        this.data = data;
    }
    /**
     * 单例实体
     */
    private Object data;
    public Object getData() {
        return data;
    }
    public static EnumSingleton getInstance(){
        return INSTANCE;
    }
}

Spring管理单例bean就是容器管理

7.容器
  • 统一管理单例
public class ContainerSingleton {
    private ContainerSingleton(){}
    private static Map<String,Object> CONTAINER = new HashMap<String,Object>();
    public static void putInstance(String key,Object instance){
        if(StringUtils.isNotBlank(key) && instance != null){
            if(!CONTAINER.containsKey(key)){
                CONTAINER.put(key,instance);
            }
        }
    }
    public static Object getInstance(String key){
        return CONTAINER.get(key);
    }
}
8.特殊的单例模式 ThreadLocal 实现线程单例
public class ThreadLocalInstance {
    private static final ThreadLocal<ThreadLocalInstance> INSTANCE
            = ThreadLocal.withInitial(ThreadLocalInstance::new);
    private ThreadLocalInstance(){
        System.out.println("init");
    }
    public static ThreadLocalInstance getInstance(){
        return INSTANCE.get();
    }
}

测试

public class ThreadLocalInstance {
    private static final ThreadLocal<ThreadLocalInstance> INSTANCE
            = ThreadLocal.withInitial(ThreadLocalInstance::new);
    private ThreadLocalInstance(){
        System.out.println("init");
    }
    public static ThreadLocalInstance getInstance(){
        return INSTANCE.get();
    }
}

运行, 在输出结果中可以看到, 用一个线程获取到的实例都是相同的, 即每个线程中只有一个实例存在, 在很多情况下是非常有用的,篇幅原因就不详细展开了,想详细了解的可以看一下这边文章 => Java并发编程:深入剖析ThreadLocal

源码中的单例

单例在源码中是广泛使用的
比如常用的工具类 java.lang.Math#random方法

public static double random() {
        return RandomNumberGeneratorHolder.randomNumberGenerator.nextDouble();
    }

这个RandomNumberGeneratorHolder.randomNumberGenerator是什么呢?

   private static final class RandomNumberGeneratorHolder {
        static final Random randomNumberGenerator = new Random();
    }

这正是上面提到的内部类实现单例的模式.
其他的比如java.lang.Runtime

    private static Runtime currentRuntime = new Runtime();

    public static Runtime getRuntime() {
        return currentRuntime;
    }

一个非常明显的饿汉式单例

序列化对单例模式的破坏

java中提供了对象的序列化与反序列化功能, 对象实现了Serializable接口之后就可以对对象的实例进行序列化与反序列化, 下面以HungrySingleton 为例看一下 反序列化破坏单例模式的实例

public class SerializationBrokenSingletonTest {
    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);
        System.out.println(instance == newInstance);
    }
}

运行结果:

com.hhx.design.pattern.creational.singleton.HungrySingleton@135fbaa4
com.hhx.design.pattern.creational.singleton.HungrySingleton@568db2f2
false

明显看到 反序列化之后, 得到了一个不同的对象实例.

在HungrySingleton中添加readResolve()方法

    private Object readResolve(){
        return hungrySingleton;
    }

再次运行代码

com.hhx.design.pattern.creational.singleton.HungrySingleton@135fbaa4
com.hhx.design.pattern.creational.singleton.HungrySingleton@135fbaa4
true

神奇的发现返回true, 这是怎么回事呢,
有兴趣的朋友可以debug跟踪一下

  1. ObjectInputStream#readObject
  2. ObjectInputStream#readObject0
  3. ObjectInputStream#readOrdinaryObject

重点关注ObjectInputStream#readOrdinaryObject中的
obj = desc.isInstantiable() ? desc.newInstance() : null

Object rep = desc.invokeReadResolve(obj)
就可以知道 readResolve 的调用原理了.

反射对单例模式的破坏

public class ReflectBrokenSingletonTest {
    public static void main(String[] args) throws IOException, ClassNotFoundException, NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        HungrySingleton instance = HungrySingleton.getInstance();
        Class<HungrySingleton> clazz = HungrySingleton.class;
        Constructor<HungrySingleton> constructor = clazz.getDeclaredConstructor();
        constructor.setAccessible(true);
        HungrySingleton newInstance = constructor.newInstance();
        System.out.println(instance);
        System.out.println(newInstance);
        System.out.println(instance == newInstance);
    }
}
  • 对于在类加载阶段就初始化的实例的单例模式(饿汉式, 内部类)可以通过在构造器中抛出异常的方式防止反射攻击
    private HungrySingleton(){
        if(INSTANCE != null){
            throw new RuntimeException("单例构造器禁止反射调用");
        }
    }

对于懒加载的单例模式(懒汉式, 懒汉式+同步锁, DCL模式), 如果在构造器中抛出异常的话, 当实例在反射调用constructor.newInstance()执行之前就已经实例化时, 是可以按照预期抛出异常的, 但是如果单例模式中的实例还未被实例化, 执行constructor.newInstance()不会抛出异常, 因为此时INSTANCE == null.

优点:

  • 内存只有一个实例, 减少内存开销
  • 设置全局访问点, 严格控制访问

缺点:

  • 没有接口, 扩展困难

github源码

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

推荐阅读更多精彩内容