【设计模式】04 单例模式

4.1 使用单例模式来解决问题

  1. 单例模式的定义

保证一个类仅有一个实例,并提供一个访问它的全局访问点。

  1. 应用单例模式来解决问题的思路

一个类能够创建多个实例,问题的根源在与类的构造方法是公开的,可以通过构造方法创建多个实例。

只要将构造方法私有化,然后提供一个外部可以访问的创造实例的方法。

4.2 单例模式实例代码

在 Java 中,单例模式的实现又分为两种,一种称为懒汉式,一种称为饿汉式。

(1)懒汉式

/**
 * 懒汉式单例实现
 */
public class Singleton {
	/**
	 * 定义一个变量来存储创建好的类实例
	 */
	private static Singleton uniqueInstance = null;
	
	/**
	 * 私有构造方法,可以在内部控制创建实例的数目
	 */
	private Singleton() {
		//
	}
	
	/**
	 * 定义一个方法来为客户端提供类实例
	 * @return 一个 Singleton 的实例
	 */
	public static synchronized Singleton getInstance() {
		// 判断存储实例的变量是否有值
		if (uniqueInstance == null) {
			// 如果没有,就创建一个类实例,并把值赋值给存储类实例的变量
			uniqueInstance = new Singleton();
		}
		
		// 如果有值,那就直接使用
		return uniqueInstance;
	}
}

(2)饿汉式

/**
 * 饿汉式单例实现
 */
public class Singleton {
	/**
	 * 定义一个变量来存储创建好的类实例,直接在这里创建类实例,只能创建一次
	 */
	private static Singleton uniqueInstance = new Singleton();
	
	/**
	 * 私有构造方法,可以在内部控制创建实例的数目
	 */
	private Singleton() {
		//
	}
	
	/**
	 * 定义一个方法来为客户端提供类实例
	 * @return 一个 Singleton 实例
	 */
	public static Singleton getInstance() {
		// 直接使用已经创建好的实例
		return uniqueInstance;
	}
	
}

饿汉式:在装载类的时候就创建对象实例

懒汉式:需要使用对象的时候才创建

4.3 模式讲解

4.3.1 认识单例模式

  1. 单例模式的功能

保证这个类在运行期间只会创建一个类实例。

  1. 单例模式的范围

目前 Java 里面实现的单例是一个虚拟机的范围。因为装载类的功能是虚拟机的,所以一个虚拟机在通过自己的 ClassLoader 装载饿汉式实现单例类的时候会创建一个实例。

如果一个虚拟机里面有很多个 ClassLoader,而且这些 ClassLoader 都装某个类的话,就算这个类是单例,它也会产生很多个实例。

如果一个计算机有多个虚拟机,每个虚拟机至少有一个这个类的实例。

4.3.2 延迟加载的思想

单例模式的懒汉式实现方式体现了延迟加载的思想。

简单的说,延迟加载就是一开始不要加载资源或者数据,等到马上就要使用这个资源或者数据了,才加载,所以也称 Lazy Load。

4.3.3 缓存的思想

单例模式的懒汉式实现还体现了缓存的思想。

简单的说,当某些资源或者数据被频繁的使用,而这些资源或者数据存储在系统外部,比如数据库,硬盘文件等,那么每次操作这些数据都要从数据库或硬盘上获取,速度会很慢,将造成性能问题。

一个简单解决方法就是:把这些数据缓存到内存里面,每次操作的时候,先到内存里面找,看有没有这些数据,如果有,就直接使用,如果没有就获取它,并设置到缓存中,下一次访问的时候就可以直接从内存中获取,从而节省大量的时间。

以下就是缓存的实现:

if (instance == null) {
	instance = new Singleton();
}

4.3.4 Java 中缓存的基本实现

在 Java 开发中最常见的一种实现缓存的方式就是使用 Map,基本步骤如下。

(1)先到缓存里面查找,看看是否有需要使用的数据。
(2)如果没有找到,那么就创建一个满足要求的数据,然后把这个数据设置到缓存中,以备下次使用。

/**
 * Java 中缓存的基本示例
 */
public class JavaCache {
	/**
	 * 缓存数据的容器,定义成 Map 是方便访问,直接根据 Key 就可以获取 Value 了
	 */
	private Map<String, Object> map = new HashMap<String, Object>();
		
	/** 
	 * 从缓存中获取值
	 * @param key 设置时候的 key
	 * @return key 对应的 Value
	 */
	public Object getValue(String key) {
		// 先从缓存里面取值
		Object obj = map.get(key);
		// 判断缓存里面是否有值
		if (obj == null) {
			// 如果没有,那么就去获取相应的数据,比如读取数据库或者文件
			obj = key + ", value";
			// 把获取到的值设置回到缓存中
			map.put(key, obj);
		}
		// 如果有值了,就直接返回使用
		return obj;
	}
}

以上只是缓存的基本实现。

4.3.5 利用缓存来实现单例模式

/**
 * 使用缓存来模拟实现单例
 */
public class Singleton {
	/**
	 * 定义一个默认的 key 值,用来标识在缓存中的存放
	 */
	private final static String DEFAULT_KEY = "One";
	
	/**
	 * 缓存实例的容器
	 */
	private static Map<String, Singleton> map = new HashMap<String, Singleton>();
		
	/**
	 * 私有化构造方法
	 */
	private Singleton() {
		//
	}
	
	public static Singleton getInstance() {
		// 先从缓存中获取
		Singleton instance = (Singleton)map.get(DEFAULT_KEY));
		// 如果没有,就新建一个 ,然后设置回缓存中
		if (instance == null) {
			instance = new Singleton();
			map.put(DEFAULT_KEY, instace);
		}
	}
}

4.3.6 单例模式的优缺点

  1. 时间和空间

懒汉式是典型的时间换空间,也就是每次获取实例都会进行判断,看是否需要创建实例,浪费判断的时间。

饿汉式是典型的空间换时间,当类装载的时候就会创建类实例,不需要再判断,节省了运行时间。

  1. 线程安全

(1) 从线程安全上讲,不加同步的懒汉式是线程不安全的,比如,有两个线程,同时调用 getInstance 方法,那就有可能导致并发问题。

(2)饿汉式是线程安全的,因为虚拟机保证只会装载一次,在装载类的时候是不会发生并发的。

(3)懒汉式如何实现线程安全呢?

加上同步锁

public static synchronized Singleton getInstance() {}

这样会降低访问的速度,可以使用 双重检查加锁 来实现

(4)双重检查加锁

所谓双重检查加锁,指的是:并不是每次进入 getInstance 方法都需要同步,而是先不同步,进入方法后,先检查实例是否存在,如果不存在才进入下面的同步块,这是第一重检查。进入同步块过后,再次检查实例是否存在,如果不存在,就在同步的情况下创建一个实例,这是第二重检查。

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

public class Singleton {
	/**
	 * 对保存实例的变量添加 volatile 的修饰
	 */
	private volatile static Singleton instance = null;
	
	private Singleton() {
	
	}
	
	public static Singleton getInstance() {
		// 先检查实例是否存在,如果不存在才进入下面的同步块
		if (instance == null) {
			// 同步块,线程安全地创建实例
			synchronized(Singleton.class) {
				// 再次检查实例是否存在,如果不存在才真正的创建实例
				if (instance == null) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}

4.3.7 在 Java 中一种更好的单例实现方式

Lazy initialization holder class 模式,这个模式综合使用了 Java 的类级内部类和多线程缺省同步琐的知识,很巧妙地同步实现了延迟加载和线程安全。

  1. 类级内部类:
  • 简单点说,类级内部类指的是,有 static 修饰的成员式内部类。
  • 类级内部类相当于其外部类的 static 成分。
  • 类级内部类中,可以定义静态的方法。在静态方法中只能够引用外部类中的静态成员方法或者成员变量。
  • 类级内部类相当于其外部类的成员,只有在第一次被使用的时候才会被装载。
  1. 多线程缺省同步琐的知识

在某些情况中,JVM 已经隐含地为您执行了同步,这些情况下就不用自己再来进行同步控制。

  • 由静态初始化器(在静态字段上或 static{} 块中的初始化器)初始化数据时
  • 访问 final 字段时
  • 在创建线程之前创建对象时
  • 线程可以看见它将要处理的对象时
  1. 解决方案的思路

要想实现线程安全,可以采用静态初始化器的方式,它可以由 JVM 来保证线程的安全性,但是在类装载的时候就会初始化,所以会浪费一定的空间。

采用类级内部类可以解决延迟加载。

public class Singleton {
	/**
	 * 类级的内部类,也就是静态的成员式内部类,该内部类的实例与外部类的实例
	 * 没有绑定关系,而且只有被调用的时候才会装载,从而实现了延迟加载
	 */
	private static class SingletonHolder {
		/**
		 * 静态初始化器,由 JVM 来保证线程安全
		 */
		private static Singleton instance = new Singleton();
	}
	
	/**
	 * 私有构造方法
	 */
	private Singleton() {
	
	}
	
	public static Singleton getInstance() {
		return SingletonHolder.instance;
	}
}

4.3.8 单例和枚举

单元素的枚举类型已经成为实现 Singleton 的最佳方法。

  • Java 的枚举类型实质上是功能齐全的类,因此可以有自己的属性和方法。
  • Java 的枚举类型的基本思想是通过公有的静态 final 域为枚举变量导出实例的类。
  • 从某个角度讲,枚举是单例的泛型化,本质上是单元素的枚举。
public enum Singleton {
	/**
	 * 定义一个枚举的元素,它就代表了 Singleton 的一个实例
	 */
	uniqueInstance;
	
}

4.3.9 思考单例模式

  1. 单例模式的本质

单例模式的本质:控制实例数目。

如何控制实例数目为多个?

/**
 * 简单演示如何扩展单例模式,控制实例数目为 3 个
 */
public class OneExtend {
	/**
	 * 定义一个缺省的 key 值的前缀
	 */
	private final static String DEFAULT_PREKEY = "Cache";
	
	/**
	 * 缓存实例的容器
	 */
	private static Map<String, OneExtend> map = new HashMap<String, OneExtend>();
		
	/**
	 * 用来记录当前正在使用第几个实例,到了控制的最大数目,就返回从 1 开始
	 */
	private static int num = 1;
	
	/**
	 * 定义控制实例的最大数目
	 */
	private final static int NUM_MAX = 3;
	
	private OneExtend() {
		
	}
	
	public static OneExtend getInstance() {
		Strine key = DEFAULT_PREKEY + num;
		OneExtend oneExtend = map.get(key);
		if (oneExtend == null) {
			oneExtend = new OneExtend();
			map.put(key, oneExtend);
		}
		// 把当前的序号加 1 
		num++;
		if (num > NUM_MAX) {
			// 如果实例的序号已经达到最大数目了,那就重复从 1 开始获取
			num = 1;
		}
		return oneExtend;
	}
	
}

  1. 何时选用单例模式

当需要控制一个类的实例只能有一个,而且客户只能从一个全局访问点访问它时,可以选用单例模式。

猜你喜欢

转载自blog.csdn.net/qq_37581282/article/details/82991913