说明:笔者采用JAVA语言对《剑指Offer(第2版)》的题目求解。
单例模式—JAVA版本
剑指Offer第2版 P32页:面试题2:实现Singleton模式
我们先来看看什么是单例模式?
单例模式的核心是保证一个类只有一个实例,并且提供一个访问实例的全局访问点。
单例模式有以下特点:
1、单例类只能有一个实例。
2、单例类必须自己创建自己的唯一实例。
3、单例类必须给所有其他对象提供这一实例。
单例模式确保某个类只有一个实例,而且自行实例化并向整个系统提供这个实例。在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。每台计算机可以有若干个打印机,但只能有一个Printer Spooler,以避免两个打印作业同时输出到打印机中。每台计算机可以有若干通信端口,系统应当集中管理这些通信端口,以避免一个通信端口同时被两个请求同时调用。
通常单例模式的实现有两种模式
- 懒汉式:指全局的单例实例在第一次被使用时构建。
- 饿汉式:指全局的单例实例在类装载时构建。
单例模式的使用场景
- Spring中bean对象的模式实现方式
- servlet中每个servlet的实例
- spring mvc框架中,控制器对象是单例模式
- 应用程序的日志应用,一般都用单例模式实现(由于共享的日志文件一直处于打开状态,因为只能有一个实例去操作,否则内容不好追加)
- 操作系统的文件系统,也是大的单例模式实现的具体例子,一个操作系统只能有一个文件系统
- 项目中,读取配置文件的类,一般也只有一个对象。没有必要每次使用配置文件数据,每次new一个对象去读取。
- 数据库连接池的设计一般也是采用单例模式,因为数据库连接是一种数据库资源
单例模式优点
- 由于单例模式只生成一个实例,减少了系统性能开销,当一个对象的产生需要比较多的资源时,如读取配置、产生其他依赖对象时,则可以通过在应用启动时直接产生一个单例对象,然后永久驻留内存的方式来解决。
- 单例模式可以在系统设置全局的访问点,优化环共享资源访问,例如可以设计一个单例类,负责所有数据表的映射处理。
Singleton模式实现—1 非线程安全的实现(简单版本)
这可能是最简单的实现方法了。每次获取instance之前先进行判断,如果instance为空就new一个出来,否则就直接返回已存在的instance。这种写法在大多数的时候也是没问题的。
但是仔细阅读代码我们就会发现,这个写法是非线程安全的写法。换句话说,只适合于单线程应用。这是非常不好的写法。
问题在于,当多线程工作的时候,如果有多个线程同时运行到 if (singleton == null),都判断为null,那么两个线程就各自会创建一个实例——这样一来,就不是单例了。
package com.bean.singleton;
public class SingletonDemo_1 {
//声明单例对象singleton
private static SingletonDemo_1 singleton = null;
//构造方法
private SingletonDemo_1() {
}
//获得类对象的方法 getInstance()
public static SingletonDemo_1 getInstance() {
//基本思想是如果singleton对象为null,那么就new一个对象,并返回它。
if (singleton == null) {
singleton = new SingletonDemo_1();
}
return singleton;
}
}
Singleton模式实现—2 加线程锁(synchronized)的版本
加上synchronized关键字之后,getInstance方法就会锁上了。如果有两个线程(T1、T2)同时执行到这个方法时,会有其中一个线程T1获得同步锁,得以继续执行,而另一个线程T2则需要等待,当第T1执行完毕getInstance之后(完成了null判断、对象创建、获得返回值之后),T2线程才会执行执行。——所以这段代码也就避免了上一个简单版本中可能出现因为多线程导致多个实例的情况。
但是,这种写法也有一个问题:给getInstance()方法加锁,虽然会避免了可能会出现的多个实例问题,但是会强制除T1之外的所有线程等待,实际上会对程序的执行效率造成负面影响。
package com.bean.singleton;
public class SingletonDemo_2 {
/**
* 定义一个变量来存储创建好的类实例
*/
private static SingletonDemo_2 singleton = null;
/**
* 私有化构造方法,好在内部控制创建实例的数目
*/
private SingletonDemo_2() {
}
/**
* 定义一个方法来为客户端提供类实例
*
* @return 一个Singleton的实例
*/
public static synchronized SingletonDemo_2 getInstance() {
// 判断存储实例的变量是否有值
if (singleton == null) {
// 如果没有,就创建一个类实例,并把值赋值给存储类实例的变量
singleton = new SingletonDemo_2();
}
// 如果有值,那就直接使用
return singleton;
}
/**
* 示意方法,单例可以有自己的操作
*/
public void singletonOperation() {
//功能处理
}
/**
* 示意属性,单例可以有自己的属性
*/
private String singletonData;
/**
* 示意方法,让外部通过这些方法来访问属性的值
*
* @return 属性的值
*/
public String getSingletonData() {
return singletonData;
}
}
Singleton模式实现—3 加了volatile关键字的double-check版本
注意其中有两次if (instance == null)的判断,这个叫做双重检查( Double-Check)。
这个版本代码虽然比较简单,但是实现的思想比较复杂,解释起来更加复杂。涉及原则操作和中间状态。
先不做解释了,后续补充。
package com.bean.singleton;
public class SingletonDemo_3 {
/**
* 对保存实例的变量添加volatile的修饰
*/
private volatile static SingletonDemo_3 instance = null;
private SingletonDemo_3() {
}
public static SingletonDemo_3 getInstance() {
// 先检查实例是否存在,如果不存在才进入下面的同步块
if (instance == null) {
// 同步块,线程安全的创建实例
synchronized (SingletonDemo_3.class) {
// 再次检查实例是否存在,如果不存在才真的创建实例
if (instance == null) {
instance = new SingletonDemo_3();
}
}
}
return instance;
}
}
Singleton模式实现—4 饿汉模式
声明了一个private static类型的实例,并且直接创建它。
注意构造方法是private类型的。
package com.bean.singleton;
public class SingletonDemo_4 {
/*
* 定义一个静态变量来存储创建好的类实例
* 直接在这里创建类实例,只会创建一次
*/
private static SingletonDemo_4 instance = new SingletonDemo_4();
//私有化构造方法,好在内部控制创建实例的数目
private SingletonDemo_4(){
}
/*
* 定义一个类级别的方法来提供类实例,即该方法采用static修饰
* 这个方法里面就不需要控制代码了
*
*/
public static SingletonDemo_4 getInstance(){
//直接使用已经创建好的实例
return instance;
}
}
Singleton模式实现—5 Holder模式
该模式是将懒加载模式和线程安全模式完美结合的一种模式
package com.bean.singleton;
public class SingletonDemo_5 {
/**
* 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例 没有绑定关系,而且只有被调用到才会装载,从而实现了延迟加载
*/
private static class SingletonHolder {
/**
* 静态初始化器,由JVM来保证线程安全
*/
private static SingletonDemo_5 instance = new SingletonDemo_5();
}
/**
* 私有化构造方法
*/
private SingletonDemo_5() {
}
public static SingletonDemo_5 getInstance() {
return SingletonHolder.instance;
}
}
以上总结了单例模式的不同实现方法。