阎宏博士在《JAVA与模式》中是这样描述单例模式的:作为对象的创建模式,单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例。这个类称为单例类。     

单例模式可以说是最常使用的设计模式了,它的作用是确保某个类只有一个实例,自行实例化并向整个系统提供这个实例。在实际应用中,线程池、缓存、日志对象和对话框对象等常被设计成单例。

应聘的时候,面试官经常会问,怎么保证某个类在程序运行过程中只有一个对象存在?此问题实际就是面试官在了解面试者对单例模式的了解。

总之,选择单例模式就是为了走出多头管理、政出多门的怪圈。本文介绍单例模式的六种实现方式,在《Java与设计模式之单例模式(下)》中分析如何使用Java实现线程安全的单例模式。


单例模式的结构

  单例模式的特点:

  • 单例类只能有一个实例;
  • 单例类必须自己创建自己的唯一实例;
  • 单例类必须给所有其他对象提供这一实例。

单例模式有多种写法,各有利弊,现在我们来看看各种模式写法。

1. 饿汉式单例

结构图:

实现代码:

/**
* 饿汉式单例,线程安全
*/
public class EagerSingleton {
// 自行实例化,并用 static 和 final 修饰
   private static final EagerSingleton instance = new EgerSingleton();
// 私有化构造方法
   private EagerSingleton() {
}
// 对外发布,并用static修饰。静态公有工厂方法,返回唯一实例
public static EagerSingleton getInstance() {
return instance;
}
}

Singleton通过将构造方法限定为private避免了类在外部被实例化。在同一个虚拟机范围内,想调用其中的方法getInstance就必须使用static修饰,这样就可以通过类名.方法名访问EagerSingleton的唯一实例了;又因为静态方法里只能用静态成员,所以instance必须static化。

成员变量instance前可以不加final,因为静态方法只在编译期间执行一次初始化,也就是只会有一个对象。

饿汉式单例是线程安全的。当类被加载时,静态变量instance会被初始化;此时类的私有构造函数会被调用,从而单例类的唯一实例将被创建,以后不再改变,无需关注多线程问题,写法简单明了。

饿汉式是典型的空间换时间,当类装载的时候就会创建类的实例,然后每次调用的时候,就不需要再判断,节省了运行时间。如果是一个工厂模式、缓存了很多实例、那么就得考虑效率问题,因为这个类一加载则把所有实例不管用不用都创建。

2. 懒汉式单例

        将上面的饿汉式改为懒汉式: 
/**
* 懒汉式单例模式(线程安全)
*/
public class LazySingleton { private static volatile LazySingleton instance; private LazySingleton() {
} public static synchronized LazySingleton getInstance() {
if (null == instance) {
instance = new LazySingleton();
}
return instance;
}
}

之所以叫做懒汉式单例模式,主要是因为此种方法lazy loading,简单说就是什么时候调用,什么时候创建。它是典型的时间换空间。如果从源码中删除关键字synchronized,则是线程不安全的懒汉式单例模式。并发其实是一种特殊情况,同步锁锁的是对象,每次取对象的时候都加锁会浪费资源,因此,这种方式写出来的结构效率很低,不推荐。

3. 双重检查加锁单例

使用“双重检查加锁”的方式来实现单例可以既实现线程安全,又使性能不受很大的影响。那么什么是“双重检查加锁”机制呢?

所谓“双重检查加锁”机制,指的是:并不是每次进入getInstance方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查;进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。这样一来,就只需要同步一次了,从而减少了多次在同步情况下进行判断所浪费的时间。

“双重检查加锁”机制的实现会使用关键字volatile,它的意思是:被volatile修饰的变量的值,将不会被本地线程缓存,所有对该变量的读写都是直接操作共享内存,从而确保多个线程能正确的处理该变量。

注意:在java1.4及以前版本中,很多JVM对于volatile关键字的实现的问题,会导致“双重检查加锁”的失败,因此“双重检查加锁”机制只只能用在java5及以上的版本。

因为上面的懒汉式单例模式每次请求时都会添加同步锁,很浪费性能,所以在加锁之前先进行非空校验。

/**
* 双重检查加锁单例
*/
public class DoubleCheckSingleton {
private static volatile DoubleCheckSingleton singleton = null;
private DoubleCheckSingleton() {
} public static DoubleCheckSingleton getSingleton() {
if (null == singleton) {
synchronized (DoubleCheckSingleton.class) {
if (null == singleton) {
singleton = new DoubleCheckSingleton();
}
}
}
return singleton;
}
}

先分析去掉volatile关键字的双重检查加锁。看似简单的一段赋值语句:

 instance = new Singleton(); // 其实JVM内部已经转换为多条指令:
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址

但是经过重排序后如下:

 memory = allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址,此时对象还没被初始化
ctorInstance(memory); //2:初始化对象

可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面,在线程A初始化完成这段内存之前,线程B虽然进不去同步代码块,但是在同步代码块之前的判断就会发现instance不为空,此时线程B获得instance对象进行使用就可能发生错误。

Volatile关键字的作用是禁止进行指令的重排序。相比于去掉volatile关键字的双重校验锁, 加上之后保证了线程安全,但是,性能降低了。这种实现方式既可以实现线程安全地创建实例,而又不会对性能造成太大的影响。它只是第一次创建实例的时候同步,以后就不需要同步了,从而加快了运行速度。

提示:由于volatile关键字可能会屏蔽掉虚拟机中一些必要的代码优化,所以运行效率并不是很高。因此一般建议,没有特别的需要,不要使用。也就是说,虽然可以使用“双重检查加锁”机制来实现线程安全的单例,但并不建议大量采用,可以根据情况来选用。

4. 静态内部类单例

这个模式综合使用了Java的类级内部类和多线程缺省同步锁的知识,很巧妙地同时实现了线程安全和类似懒汉式单例模式的延迟加载。

       什么是类级内部类?

简单点说,类级内部类指的是,有static修饰的成员式内部类。如果没有static修饰的成员式内部类被称为对象级内部类。

类级内部类相当于其外部类的static成分,它的对象与外部类对象间不存在依赖关系,因此可直接创建。而对象级内部类的实例,是绑定在外部对象实例中的。

类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。

类级内部类相当于其外部类的成员,只有在第一次被使用的时候才被会装载。

      多线程缺省同步锁的知识

大家都知道,在多线程开发中,为了解决并发问题,主要是通过使用synchronized来加互斥锁进行同步控制。但是在某些情况中,JVM已经隐含地为您执行了同步,这些情况下就不用自己再来进行同步控制了。这些情况包括:

  1. 由静态初始化器(在静态字段上或static{}块中的初始化器)初始化数据时
  2. 访问final字段时
  3. 在创建线程之前创建对象时
  4. 线程可以看见它将要处理的对象时
/**
* 静态内部类
*/
public class InnerStaticSingleton { // 私有的静态内部类
private static class Holder {
private static InnerStaticSingleton instance = new InnerStaticSingleton();
} private InnerStaticSingleton() {
System.out.println("Singleton has been loaded.");
} public static InnerStaticSingleton getInstance() {
return Holder.instance;
}
}

当getInstance方法第一次被调用的时候,它第一次读取Holder.instance,导致Holder类得到初始化;而这个类在装载并被初始化的时候,会初始化它的静态域,从而创建InnerStaticSingleton的实例,由于是静态的域,因此只会在虚拟机装载类的时候初始化一次,并由虚拟机来保证它的线程安全性。

这个模式的优势在于,getInstance方法并没有被同步,并且只是执行一个域的访问,因此延迟初始化并没有增加任何访问成本。

5. 枚举单例

/**
* 枚举单例
*/
public enum EnumSingleton {
INSTANCE;
private String name;
public String getName() {
return name;
} public void setName(String name) {
this.name = name;
} public void otherMethod(){
System.out.println("Do something.");
}
}

这种方式是《Effective Java》作者Josh Bloch 提倡的方式。它不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象,可谓是很坚强的壁垒啊,推荐!是更简洁、高效、安全的实现单例的方式。

测试用例:

   public static void main(String[] args) {
EnumSingleton.INSTANCE.otherMethod();
System.out.println("-----------------------");
}

6. 登记式单例

登记式单例实际上维护的是一组单例类的实例,将这些实例存储到一个Map(登记簿)中,对于已经登记过的单例,则从工厂直接返回,对于没有登记的,则先登记,而后返回。

import java.util.HashMap;
import java.util.Map; /**
* 登记式单例类.<br/>
* 类似Spring里面的方法,将类名注册,下次从里面直接获取。
* @author east7
*/
public class RegisterSingleton {
//使用一个map来当注册表
private static Map<String, RegisterSingleton> map = new HashMap<String, RegisterSingleton>(); //静态块,在类被加载时自动执行,把 RegisterSingleton 自己也纳入容器管理
static {
RegisterSingleton single = new RegisterSingleton();
map.put(single.getClass().getName(), single);
} //受保护的默认构造函数,如果为继承关系,则可以调用,克服了单例类不能为继承的缺点
protected RegisterSingleton() {
} //静态工厂方法,返回此类惟一的实例
public static RegisterSingleton getInstance(String name) {
if (name == null) {
name = RegisterSingleton.class.getName();
System.out.println("name == null" + "--->name=" + name);
}
if (map.get(name) == null) {
try {
map.put(name, (RegisterSingleton) Class.forName(name).newInstance());
} catch (Exception e) {
e.printStackTrace();
}
}
return map.get(name);
} //一个示意性的商业方法
public String about() {
return "Hello, I am RegisterSingleton.";
} public static void main(String[] args) {
RegisterSingleton single3 = RegisterSingle-ton.getInstance(null);
System.out.println(single3.about());
}
}

Reference

  1. https://www.cnblogs.com/alter888/p/9163612.html
  2. https://blog.51cto.com/13477015/2177185
  3. https://www.cnblogs.com/java-my-life/archive/2012/03/31/2425631.html
  4. https://www.cnblogs.com/twoheads/p/9723543.html
  5. JAVA与模式

Java与设计模式之单例模式(上)六种实现方式的更多相关文章

  1. Java与设计模式之单例模式(下) 安全的单例模式

          关于单例设计模式,<Java与设计模式之单例模式(上)六种实现方式>介绍了6种不同的单例模式,线程安全,本文介绍该如何保证单例模式最核心的作用——“实现该模式的类有且只有一个实 ...

  2. 使用apache daemon让java程序在unix系统上以服务方式运行

    通过使用apache_commons_daemon,可以让Java程序在unix系统上以服务器的方式运行. 当然,通过wrapper也是可以达到这样的目的,wrapper还可以指定java应用中用到的 ...

  3. Java常见设计模式之单例模式

         1.何为单例模式? 单例模式是一种常用的软件设计模式.在它的核心结构中只包含一个被称为单例类的特殊类.通过单例模式可以保证系统中一个类只有一个实例而且该实例易于外界访问,从而方便对实例个数的 ...

  4. 《java常用设计模式之----单例模式》

    一.简介 单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一.这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式. 这种模式涉及到一个单一的类,该类负责创 ...

  5. java基础设计模式1——单例模式

    概念:在应用这个模式时,单例对象的类必须保证只有一个实例存在.许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为. 单例模式从实现上可以分为饿汉式单例和懒汉式单例两种,前者天生 ...

  6. 基于JAVA的设计模式之单例模式

    概念 于大二上学期面向对象C++期中考试中有这么道题:一个Computer有多个USB插口,那么意味着这台电脑可以插多个鼠标,但是无论你如何拔插多少个鼠标,桌面上的鼠标一直只显示一个,且多个硬件鼠标都 ...

  7. java设计模式之单例模式(七种方法)

    单例模式:个人认为这个是最简单的一种设计模式,而且也是在我们开发中最常用的一个设计模式. 单例模式的意思就是只有一个实例.单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例.这个 ...

  8. [java设计模式]之单例模式

    -------------------此部分比較深入地解说了单例模式,原文链接已给出.兴许将涉及一些常见面试问题--------------------------- 原文地址:http://www. ...

  9. java设计模式之单例模式(几种写法及比较)

    概念: Java中单例模式是一种常见的设计模式,单例模式的写法有好几种,这里主要介绍三种:懒汉式单例.饿汉式单例.登记式单例. 单例模式有以下特点: 1.单例类只能有一个实例. 2.单例类必须自己创建 ...

随机推荐

  1. mybatis-plus代码生成,实体类不生成父类属性

    一.参考文档: 官方文档其实说的很清楚了,可能有个别地方有点不太清楚. mybatis-plus官方: https://mp.baomidou.com/guide/generator.html 模版引 ...

  2. 并发编程之Disruptor并发框架

    一.什么是Disruptor Martin Fowler在自己网站上写了一篇LMAX架构的文章,在文章中他介绍了LMAX是一种新型零售金融交易平台,它能够以很低的延迟产生大量交易.这个系统是建立在JV ...

  3. 利用PHPExcel快速导出excel

    <?php $objPHPExcel = new PHPExcel(); $objSheet = $objPHPExcel->getActiveSheet(); $array = arra ...

  4. Ubuntu-18.04 LTS UEFI 安装U盘制作

    要把U盘作为UEFI启动盘,第一个分区要为FAT32分区,EFI程序放在/EFI/Boot/bootx64.efi.为了制作Ubuntu-18.04 LTS安装U盘,可以把一个U盘格式化为FAT32格 ...

  5. Vue props用法详解

    Vue props用法详解 组件接受的选项之一 props 是 Vue 中非常重要的一个选项.父子组件的关系可以总结为: props down, events up 父组件通过 props 向下传递数 ...

  6. c#读写apk的 comment

    写入: ZipFile zipFile = new ZipFile("C:\\Users\\Administrator\\Desktop\\2.apk"); zipFile.Beg ...

  7. js 数组的深度拷贝 的四种实现方法

    首先声明本人资质尚浅,本文只用于个人总结.如有错误,欢迎指正.共同提高. --------------------------------------------------------------- ...

  8. 尚硅谷韩顺平Linux教程学习笔记

    目录 尚硅谷韩顺平Linux教程学习笔记 写在前面 虚拟机 Linux目录结构 远程登录Linux系统 vi和vim编辑器 关机.重启和用户登录注销 用户管理 实用指令 组管理和权限管理 定时任务调度 ...

  9. Android GOT Hook

    最后介绍的这种hook方式原理比较简单,只需要将GOT表中的目标函数地址替换为我们自己的函数地址即可,但它的缺点是只能对导入函数进行hook,还需要对elf文件的结构有所了解. 一.获取到GOT表在内 ...

  10. 批处理引擎MapReduce程序设计

    批处理引擎MapReduce程序设计 作者:尹正杰 版权声明:原创作品,谢绝转载!否则将追究法律责任. 一.MapReduce API Hadoop同时提供了新旧两套MapReduce API,新AP ...