前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >单例模式的八种写法

单例模式的八种写法

作者头像
王金龙
发布于 2020-02-24 05:49:30
发布于 2020-02-24 05:49:30
61000
代码可运行
举报
文章被收录于专栏:王金龙的专栏王金龙的专栏
运行总次数:0
代码可运行

单例模式的八种写法

单例模式作为日常开发中最常用的设计模式之一,是最基础的设计模式,也是最需要熟练掌握的设计模式。单例模式的定义是:保证一个类仅有一个实例,并提供一个访问它的全局访问点。那么你知道单例模式有多少种实现方式吗?以及每种实现方式的利弊呢?

  • 饿汉模式
  • 懒汉模式(线程不安全)
  • 懒汉模式(线程安全)
  • 双重检查模式(DCL)
  • 静态内部类单例模式
  • 枚举类单例模式
  • 使用容器实现单例模式
  • CAS实现单例模式


饿汉模式

代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Singleton {  
     private static Singleton instance = new Singleton();  
     private Singleton () {
     }
     public static Singleton getInstance() {  
         return instance;  
     }  
 }  

这种方式在类加载时就完成了实例化,会影响类的加载速度,但获取对象的速度快。 这种方式基于类加载机制保证实例仅有一个,避免了多线程的同步问题,是线程安全的。


懒汉模式(线程不安全)

绝大多数时候,类加载的时机和对象使用的时机都是分开的,所以没有必要在类加载的时候就去实例化单例对象。为了消除单例对象实例化对类加载的影响,引入了延迟加载,就有了懒汉模式的实现方式。代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Singleton {  
    private static Singleton instance;  
    private Singleton () {
    }     
    public static Singleton getInstance() { 
        if (instance == null) {
            instance = new Singleton();  
        }  
        return instance;  
    }  
}  

懒汉模式声明了一个静态对象,在用户第一次调用时完成实例化,属于延迟加载方式。而且这种方式不是线程安全。


懒汉模式(线程安全)

针对线程不安全的懒汉模式,对其中的获取单例对象的方法增加同步关键字。代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Singleton {  
      private static Singleton instance;  
      private Singleton () {
      }
      public static synchronized Singleton getInstance() {  
          if (instance == null) {  
              instance = new Singleton();  
          }  
          return instance;  
      }  
}  

这种写法保证了线程安全,但是每次调用getInstance方法获取单例时都需要进行同步,造成不必要的同步开销,但实际上除了第一次实例化需要同步,其他时候都是不需要同步的。

双重检查模式(DCL)

既然懒汉模式中的实例化只需要在第一次的时候保证同步,那何不只在实例为空的时候加同步关键字呢。代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Singleton {  
      private volatile static Singleton singleton;  // 1
      private Singleton () {
      }   
      public static Singleton getInstance() {  
          if (instance== null) {  // 2
              synchronized (Singleton.class) {  // 3
                  if (instance== null) {  // 4
                      instance= new Singleton();  // 5
                  }  
             }  
         }  
         return singleton;  
    }  
}  

双重检查写法,主要关注以上代码中的5点:

  1. 声明单例对象时加上volatile关键字,保证多线程的内存可见性,也即当在一个线程中单例对象实例化完成之后,其他线程也同时能够看到。同时,还有更为重要的一点,下面会说。
  2. 第一次检查单例对象是否为空,判断是否已经完成了实例化。
  3. 如果第一次检查发现单例对象为空,那么该线程就要对此单例类进行加锁,准备进行实例化,加锁是为了保证该线程进行实例化的时候没有其他线程也同时进行实例化。
  4. 第二次检查单例对象是否为空,则是为了避免这种情况:此时单例对象为空,两个线程,A线程在第2步,B线程在第5步,A线程发现单例对象为空,紧接着B线程就完成了实例化,然后就会导致A线程又会走一次第5步的实例化过程,即重复实例化。那么加上了第二次检查后,当A线程到第4步的时候就会发现单例对象已经实例化完成,自然不会到第5步。
  5. 真正的实例化操作就发生在第5步,且只发生一次。
DCL思考

在以上代码的第一步中,我们提到volatile关键字,volatile关键字除了保证内存可见性,还有一点是禁止指令重排序。那么问题出在哪里呢?对,第5步。实际上,实例化对象的动作并不是一个原子操作,instance= new Singleton();可以分为以下三步完成:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
memory = allocate(); // 5.1:分配对象的内存空间
ctorInstance(memory); // 5.2:初始化对象
instance = memory; // 5.3: 设置instance指向刚分配的内存地址

而上面三行代码,5.2和5.3可能发生重排序。跟着上面代码中的第二次检查的位置进行分析。当线程B执行到5.3之后,5.2之前时,这时候线程A首次判断单例对象是否为空。这时候当然单例对象是不为空的,但是却不能使用,因为单例对象还没有被初始化呢。这既是DCL的缺陷所在,也是为什么要对单例对象家volatile关键字的原因。禁止了指令重排序,自然不会出现线程A拿到一个不可用的单例对象。

静态内部类单例模式

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Singleton { 
    private Singleton() {
    }
    public static Singleton getInstance() {  
        return SingletonHolder.sInstance;  
    }  
    private static class SingletonHolder {  
        private static final Singleton sInstance = new Singleton();  
    }  
} 

第一次加载Singleton类时并不会初始化sInstance,只有第一次调用getInstance方法时虚拟机加载SingletonHolder 并初始化sInstance ,这样不仅能确保线程安全也能保证Singleton类的唯一性,所以推荐使用静态内部类单例模式。

枚举类单例模式

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public enum Singleton {  
     INSTANCE;  
     public void doSomeThing() {  
     }  
 } 

那这个单例如何来填充属性呢,增加构造函数和属性即可啦,请看代码:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public enum Singleton {  
    INSTANCE("name", 18);
    private String name;
    private int age;
    Singleton(String name, int age) {
        this.name = name;
        this.age = age;
    }  
     public void doSomeThing() {  
     }  
 } 

默认枚举实例的创建是线程安全的,并且在任何情况下都是单例,上述讲的几种单例模式实现中,有一种情况下他们会重新创建对象,那就是反序列化,将一个单例实例对象写到磁盘再读回来,从而获得了一个实例。反序列化操作提供了readResolve方法,这个方法可以让开发人员控制对象的反序列化。在上述的几个方法示例中如果要杜绝单例对象被反序列化是重新生成对象,就必须加入如下方法:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
private Object readResolve() throws ObjectStreamException{
    return singleton;
}

使用容器实现单例模式

代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class SingletonManager { 
  private static Map<String, Object> objMap = new HashMap<String,Object>();
  private Singleton() { 
  }
  public static void registerService(String key, Objectinstance) {
    if (!objMap.containsKey(key) ) {
      objMap.put(key, instance) ;
    }
  }
  public static ObjectgetService(String key) {
    return objMap.get(key) ;
  }
}

在程序的初始化,将多个单例类型注入到一个统一管理的类中,使用时通过key来获取对应类型的对象,这种方式使得我们可以管理多种类型的单例,并且在使用时可以通过统一的接口进行操作。这种方式是利用了Map的key唯一性来保证单例。

CAS实现单例模式

以上实现主要用到了两点来保证单例,一是JVM的类加载机制,另一个就是加锁了。那么有没有不加锁的线程安全的单例实现吗?有点,那就是使用CAS。CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。代码如下:

代码语言:javascript
代码运行次数:0
运行
AI代码解释
复制
public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>();
    private Singleton() {}
    public static Singleton getInstance() {
        for (;;) {
            Singleton singleton = INSTANCE.get();
            if (null != singleton) {
                return singleton;
            }
            singleton = new Singleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}

用CAS的好处在于不需要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,可以支持较大的并行度。CAS的一个重要缺点在于如果忙等待一直执行不成功(一直在死循环中),会对CPU造成较大的执行开销。

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-02-18 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

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

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

评论
登录后参与评论
暂无评论
推荐阅读
编辑精选文章
换一批
写了这么久代码,你懂单例模式吗?
这种方式在类加载时就完成了初始化,所以类加载较慢,但获取对象的速度快。这种方式基于类加载机制避免了多线程的同步问题,但是也不能确定有其他的方式(或者其他的静态方法)导致类装载,这时候初始化instance显然没有达到懒加载的效果。
公号:咻咻ing
2019/10/24
3580
设计模式(1)-单例模式
单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中一个类只有一个实例。即一个类只有一个对象实例。
秦子帅
2019/06/06
4070
设计模式(1)-单例模式
单例模式的8种写法
JVM类加载过程中,虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。如果在一个类的<clinit>()方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行<clinit>()方法后,其他线程唤醒之后不会再次进入<clinit>()方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。
有一只柴犬
2024/01/25
1410
单例模式的8种写法
Java单例模式的不同写法(懒汉式、饿汉式、双检锁、静态内部类、枚举)[通俗易懂]
Java中单例(Singleton)模式是一种广泛使用的设计模式。单例模式的主要作用是保证在Java程序中,某个类只有一个实例存在。
全栈程序员站长
2022/09/15
3.6K0
02、人人都会设计模式--单例模式
一个男人只能有一个媳妇「正常情况」,一个人只能有一张嘴,通常一个公司只有一个 CEO ,一个狼群中只有一个狼王等等
TigerChain
2019/07/22
5240
02、人人都会设计模式--单例模式
浅析单例模式的8中写法
说明这种写法看似在线程安全的基础上减少了锁的代码量,其实是达不到“永远”单例的目的的。
行百里er
2020/12/02
4410
浅析单例模式的8中写法
单例模式的六种写法
确保某个类只有一个对象的场景,比如一个对象需要消耗的资源过多,访问io、数据库,需要提供全局配置的场景
Rouse
2019/08/13
3.9K0
剑指Offer(一)--手写单例模式
单例模式,是一种比较简单的设计模式,也是属于创建型模式(提供一种创建对象的模式或者方式)。 要点: 1.涉及一个单一的类,这个类来创建自己的对象(不能在其他地方重写创建方法,初始化类的时候创建或者提供私有的方法进行访问或者创建,必须确保只有单个的对象被创建)。 2.单例模式不一定是线程不安全的。 3.单例模式可以分为两种:
秦怀杂货店
2022/02/15
5820
单例模式详解
单例模式,是一种常用的软件设计模式。在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,使用单例模式的类只有一个对象实例。
Java架构师必看
2021/05/18
4400
单例模式详解
Java实现单例模式的9种方法
因进程需要,有时我们只需要某个类同时保留一个对象,不希望有更多对象,此时,我们则应考虑单例模式的设计。
全栈程序员站长
2022/09/08
4410
Java设计模式 | 单例模式解析与实战
确保某个类有且只有一个对象的场景, 避免产生多个对象消耗过多的资源, 或者 某种类型的对象只应该有且只有一个。 例如, 创建一个对象需要消耗的资源过多, 如要访问IO和数据库等资源,这时就要考虑使用单例模式。
凌川江雪
2020/04/14
7080
Java设计模式 | 单例模式解析与实战
02.创建型:单例设计模式2
创建型:单例设计模式2目录介绍01.如何实现一个单例02.饿汉式实现方式03.懒汉式实现方式04.双重DCL校验模式05.静态内部类方式06.枚举方式单例07.容器实现单例模式01.如何实现一个单例介绍如何实现一个单例模式的文章已经有很多了,但为了保证内容的完整性,这里还是简单介绍一下几种经典实现方式。概括起来,要实现一个单例,我们需要关注的点无外乎下面几个:构造函数需要是 private 访问权限的,这样才能避免外部通过 new 创建实例;考虑对象创建时的线程安全问题;考虑是否支持延迟加载;考虑 getI
杨充
2022/09/08
2940
Java单例模式的写法及优缺点
优点:实现简单,不存在多线程问题,直接声明一个私有对象,然后对外提供一个获取对象的方法。
老马的编程之旅
2022/06/22
8150
设计模式之单例模式(创建型)
本博客介绍一种创建型模式:单例模式 这是一种比较容易理解的设计模式,可以理解为创建对象的一种很好的做法。可以尽量避免创建过多的对象,给JVM造成很大的负载。
SmileNicky
2019/01/17
4340
Java 实现单例模式的 9 种方法
因进程需要,有时我们只需要某个类同时保留一个对象,不希望有更多对象,此时,我们则应考虑单例模式的设计。
芋道源码
2019/01/09
1.2K0
Java版的7种单例模式
今天看到某一篇文章的一句话 单例DCL 前面加 V 。就这句话让我把 单例模式 又仔细看了一遍。
静默加载
2020/05/29
4410
面试突击50:单例模式有几种写法?
单例模式是面试中的常客了,它的常见写法有 4 种:饿汉模式、懒汉模式、静态内部类和枚举,接下来我们一一来看。
磊哥
2022/05/26
3340
再谈单例模式
此前写过设计模式的文章:《单例模式》,谈过单例模式,但对背后的底层知识阐述的还不够到位,比如下面几个问题剖析的不够仔细:
后台技术汇
2024/09/18
1480
再谈单例模式
设计模式二十四章经之单例设计模式
概述 单例模式是应用最广的设计模式之一。也可能是很多初级工程师唯一会使用的设计模式。从字面意思,单例模式就是单例对象的类必须保证只有一个实例的存在,而且自行实例化并向整个系统提供这个实例。例如,创建一个对象需要消耗太多的资源,如要访问IO和数据库等资源,这时需要考虑单例模式。 懒汉单例模式 线程不安全 当提到单例模式的时候,我们第一反应就是如下代码: public class Singleton { private static Singleton instance; private S
我就是马云飞
2018/06/22
4700
设计模式 -创建型模式之单例模式的五种实现
许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
未读代码
2019/11/08
3210
相关推荐
写了这么久代码,你懂单例模式吗?
更多 >
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档