前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >一文搞懂单例模式

一文搞懂单例模式

作者头像
全菜工程师小辉
发布2020-12-22 11:22:51
6290
发布2020-12-22 11:22:51
举报

单例模式(Singleton Pattern)是Java中最简单的设计模式之一,属于创建型模式。它提供了一种创建对象的最佳方式。

这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建,排除线程不安全的风险。这个类提供了一种访问其唯一的对象的方式。

相关阅读:

Spring的设计模式快速入门干货

快速理解设计模式之创建型模式

懒汉式,线程安全

这种方式具备很好的 lazy loading,能够在多线程中很好的工作,但是,效率很低,99% 情况下不需要同步。

  • 优点:第一次调用才初始化,避免内存浪费。
  • 缺点:必须加锁 synchronized 才能保证单例,但加锁会影响效率。getInstance() 的性能对应用程序不是很关键(该方法使用不太频繁)。
代码语言:javascript
复制
public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}

饿汉式

这种方式比较常用,但容易产生垃圾对象。

  • 优点:没有加锁,执行效率会提高。
  • 缺点:类加载时就初始化,浪费内存。

它基于 classloader 机制避免了多线程的同步问题,不过,instance 在类装载时就实例化,虽然导致类装载的原因有很多种,在单例模式中大多数都是调用 getInstance 方法, 但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化 instance 显然没有达到 lazy loading 的效果。

代码语言:javascript
复制
public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return instance;  
    }  
}

笔者最喜欢用的方式,源码里使用这种形式也很多

静态内部类

这种方式能达到双检锁方式一样的功效,但实现更简单。对静态域使用延迟初始化,应使用这种方式而不是双检锁方式。这种方式只适用于静态域的情况,双检锁方式可在实例域需要延迟初始化时使用。这种方式同样利用了 classloader 机制来保证初始化 instance 时只有一个线程,它跟第 3 种方式不同的是:第 3 种方式只要 Singleton 类被装载了,那么 instance 就会被实例化(没有达到 lazy loading 效果),而这种方式是 Singleton 类被装载了,instance 不一定被初始化。因为 SingletonHolder 类没有被主动使用,只有通过显式调用 getInstance 方法时,才会显式装载 SingletonHolder 类,从而实例化 instance。想象一下,如果实例化 instance 很消耗资源,所以想让它延迟加载,另外一方面,又不希望在 Singleton 类加载时就实例化,因为不能确保 Singleton 类还可能在其他的地方被主动使用从而被加载,那么这个时候实例化 instance 显然是不合适的。这个时候,这种方式相比第 3 种方式就显得很合理。

代码语言:javascript
复制
public class Singleton {  
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
}

枚举

这种实现方式还没有被广泛采用,但这是实现单例模式的最佳方法。它更简洁,自动支持序列化机制,绝对防止多次实例化。这种方式是 Effective Java 作者 Josh Bloch 提倡的方式,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。不能通过 reflection attack 来调用私有构造方法。

代码语言:javascript
复制
public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}

双重校验锁

这种方式采用双锁机制,安全且在多线程情况下能保持高性能。

代码语言:javascript
复制
public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}

双重检测的FAQ

1. 为何要同步?

多线程情况下,若是A线程调用getInstance,发现instance为null,那么它会开始创建实例,如果此时CPU发生时间片切换,线程B开始执行,调用getInstance,发现instance也null(因为A并没有创建对象),然后B创建对象,然后切换到A,A因为已经检测过了,不会再检测了,A也会去创建对象,两个对象,单例失败。因此要同步。

2. 同步为何不用public synchronized static SingletonClass getInstance(),也就是说为何不同步这个方法,而要同步下面的语句?

因为synchronized修饰的同步块的运行要比一般的代码段慢,如果经常调用getInstance,那么性能问题就得考虑了。

有关synchronized的更详细讲解,请看详解Java多线程锁之synchronized

3. 最外层为何要有if (instance == null)判断?

减少同步代码块的运行次数,从而减少不必要的加锁操作。

4. instance为何要有volatile修饰?

结论:volatile能够防止指令的重排序。

有关volatile的更多知识,请看Java多线程的可见性与有序性

JVM实现可以自由的进行编译器优化。而我们创建变量的步骤:

  1. 申请一块内存,调用构造方法进行初始化。
  2. 分配一个指针指向这块内存。

而这两个操作,JVM并没有规定谁在前谁在后,那么就存在这种情况:线程A开始创建SingletonClass的实例,此时线程B调用了getInstance()方法,首先判断instance是否为null。按照我们上面所说的内存模型,A已经把instance指向了那块内存,只是还没有调用构造方法,因此B检测到instance不为null,于是直接把instance返回了——问题出现了,尽管instance不为null,但它并没有构造完成,就像一套房子已经给了你钥匙,但你并不能住进去,因为里面还没有收拾。此时,如果B在A将instance构造完成之前就是用了这个实例,程序就会出现错误了。

在JDK 5之后,Java使用了新的内存模型。volatile关键字有了明确的语义——在JDK1.5之前,volatile是个关键字,但是并没有明确的规定其用途——被volatile修饰的写变量不能和之前的读写代码调整,读变量不能和之后的读写代码调整!因此,只要我们简单的把instance加上volatile关键字就可以了。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2020-12-18,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 全菜工程师小辉 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 懒汉式,线程安全
  • 饿汉式
  • 静态内部类
  • 枚举
  • 双重校验锁
  • 双重检测的FAQ
    • 1. 为何要同步?
      • 2. 同步为何不用public synchronized static SingletonClass getInstance(),也就是说为何不同步这个方法,而要同步下面的语句?
        • 3. 最外层为何要有if (instance == null)判断?
          • 4. instance为何要有volatile修饰?
          相关产品与服务
          文件存储
          文件存储(Cloud File Storage,CFS)为您提供安全可靠、可扩展的共享文件存储服务。文件存储可与腾讯云服务器、容器服务、批量计算等服务搭配使用,为多个计算节点提供容量和性能可弹性扩展的高性能共享存储。腾讯云文件存储的管理界面简单、易使用,可实现对现有应用的无缝集成;按实际用量付费,为您节约成本,简化 IT 运维工作。
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档