单例模式的八种写法

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

  • 饿汉模式

  • 懒汉模式(线程不安全)

  • 懒汉模式(线程安全)

  • 双重检查模式(DCL)

  • 静态内部类单例模式

  • 枚举类单例模式

  • 使用容器实现单例模式

  • CAS实现单例模式


饿汉模式

代码如下:

public class Singleton {   private static Singleton instance = new Singleton();   private Singleton () { } public static Singleton getInstance() {   return instance;   }
 }

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


懒汉模式(线程不安全)

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

public class Singleton {  private static Singleton instance;  private Singleton () {}     public static Singleton getInstance() { if (instance == null) {instance = new Singleton();  }  return instance;  }  }

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


懒汉模式(线程安全)

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

public class Singleton {    private static Singleton instance;    private Singleton () {  }  public static synchronized Singleton getInstance() {    if (instance == null) {    instance = new Singleton();    }    return instance;    }  }

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

双重检查模式(DCL)

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

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();可以分为以下三步完成:

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拿到一个不可用的单例对象。

静态内部类单例模式

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类的唯一性,所以推荐使用静态内部类单例模式。

枚举类单例模式

public enum Singleton {   INSTANCE;   public void doSomeThing() {   }
 }

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

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方法,这个方法可以让开发人员控制对象的反序列化。在上述的几个方法示例中如果要杜绝单例对象被反序列化是重新生成对象,就必须加入如下方法:

private Object readResolve() throws ObjectStreamException{return singleton;}

使用容器实现单例模式

代码如下:

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同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。代码如下:

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造成较大的执行开销。

(0)

相关推荐

  • 设计模式(2) 单例模式

    单例模式 线程安全的Singleton 会破坏Singleton的情况 线程级Singleton 单例模式是几个创建型模式中最独立的一个,它的主要目标不是根据客户程序调用生成一个新的实例,而是控制某个 ...

  • java常见设计模式之---单例模式

    java常见设计模式之---单例模式 1.单例模式简介 应用场景举例 2.单例模式的特点 3.单例模式和静态类 4.单例模式的经典实现 饿汉式单例(典型实现) 饿汉式-静态代码块 懒汉式单例创建,五种 ...

  • 图解Java设计模式之单例设计模式

    图解Java设计模式之单例设计模式 设计模式介绍 设计模式类型 单例设计模式介绍 饿汉式(静态常量) 饿汉式(静态代码块) 懒汉式(线程不安全) 懒汉式(线程安全,同步方法) 懒汉式(线程安全,同步代 ...

  • 【转】C#设计模式-单例模式(Singleton Pattern)

    目录 介绍 第一个版本 --不是线程安全的 第二个版本 -- 简单的线程安全 第三个版本 - 使用双重检查锁定尝试线程安全 第四个版本 - 不太懒,不使用锁且线程安全 第五版 - 完全懒惰的实例化 第 ...

  • 单例模式的实现

    记一下学习单例模式的笔记: 单例就是要保证该类仅有一个实例.实现完全封闭的单例(外部不能new)其实就要两点要求: 全局访问:需要一个该类型的全局静态变量,每次获取实例时都要判断它是否null,不存在 ...

  • 设计模式 - Java中单例模式的6种写法及优缺点对比

    目录 1 为什么要用单例模式 1.1 什么是单例模式 1.2 单例模式的思路和优势 2 写法① - 饥饿模式 2.1 代码示例 2.2 优缺点比较 3 写法② - 懒惰模式 3.1 代码示例 3.2 ...

  • 这行书八种基本笔画的写法,你必须知道!

    晋,王献之 <地黄汤帖> 行书的用笔方法很多,具有灵活性.伸缩性.多样性等特点.因此,掌握行书的用笔方法是至关重要的,用笔的熟练与否,直接关系到书写质量.在此介绍行草书八大笔法,后四个你绝 ...

  • 行书八种基本笔画的写法

    行书介于草书与楷书之间,在行书的书写中,我们一方面要注意其字形,另一方面更要注意笔画的写法,因为笔画是字的基本构成元素. 王献之<余杭帖>拓本,入刻<淳化阁帖>卷九 今天小编为 ...

  • 行书八种基本笔画的写法(图解)

    行书介于草书与楷书之间,在行书的书写中,我们一方面要注意其字形,另一方面更要注意笔画的写法,因为笔画是字的基本构成元素. 王献之<余杭帖>拓本,入刻<淳化阁帖>卷九 今天小编为 ...

  • 行书八种基本笔画的写法教程

    岐黄名家经方 1篇原创内容 公众号 收藏岐黄名家经方学习 行书介于草书与楷书之间,在行书的书写中,我们一方面要注意其字形,另一方面更要注意笔画的写法,因为笔画是字的基本构成元素. 王献之<余杭帖 ...

  • 行书书法:智永行书的八种竖笔写法

    行书书法:智永行书的八种竖笔写法

  • 「方圆居文化」书海拾贝:楷书的八种笔画在隶书中的对应写法

    中国的书法文化经过三千多年的发展演变,形成了篆书.隶书.草书.楷书.行书五种成熟的基本书体,也有的书家将行楷和行草两种过渡书体作为两种独立的书体.从这个意义上就可以说,中国书法有七种书体.为了区分,我 ...

  • 技法指导丨行书八种基本笔画的写法(图解)

    行书介于草书与楷书之间,在行书的书写中,我们一方面要注意其字形,另一方面更要注意笔画的写法,因为笔画是字的基本构成元素. 王献之<余杭帖>拓本,入刻<淳化阁帖>卷九 今天小编为 ...

  • 楷书八种基本笔画的写法

    对于初学者而言,写好硬笔字,最基本的要求是:结构规范.字体清楚整洁.行笔流畅,有提按顿转的基本表现.而基本笔画的写法则是重中之重,是写好书法的根本. 构成汉字的基本笔画是:点.横.竖.撇.捺.提.折. ...