设计模式-单例模式

发布于 — 2019 年 10 月 14 日
#design

概念

一个类只允许创建一个对象(或者实例),那这个类就是一个单例类

为什么要使用单例

有些数据在系统中只应该保存一份,当使用多个对象时会造成一些错误。有一些资源访问冲突时也可以使用单例模式来解决。

实现方式

由于对象只能有一个,所以构造函数的权限必须是private私有的,这样外部就无法通过new来创建实例

还有一些需要关注的点

  • 创建对象的线程安全问题
  • 是否支持延迟加载
  • 获取实例方法的性能是否高

饿汉式

优点:写法简单。在类装载时就完成了实例化。线程安全

缺点:由于在类装载时就完成了实例化,如果从始至终都没有用到这个实例,会造成内存浪费

 class Single {
 /**
  * 设置成私有的,保证无法在其它类中使用 new 关键字进行实例化
  */
 private Single() {}
 
 private final static Single INSTANCE = new Single();
 
     /**
  * 从外界获取实例的唯一方法,从这个方法拿到的实例肯定是全局唯一的
  * @return
  */
 public static Single getInstance() {
 return INSTANCE;
 }
 
 }
 
 /**
  * 与上面的类似,用了静态代码块,不同的代码写法其他完全一样
  */
 class SingleTest2 {
 
 private SingleTest2() {}
 
 private static SingleTest2 SINGLE_TEST_2;
 
 static {
 SINGLE_TEST_2 = new SingleTest2();
 }

 public static SingleTest2 getInstance() {
 return SINGLE_TEST_2;
 }
 }

懒汉式

在需要用到实例时才进行初始化,所以不会造成内存浪费,但多线程情况下会出现多个实例的情况

 /**
  * 懒汉式的单例模式
  * 线程不安全
  */
 class UnSafeSingle {
 
 private UnSafeSingle() {
 }
 
 private static UnSafeSingle INSTANCE;
 
 public static UnSafeSingle getInstance() {
 if (INSTANCE == null) {
       //这个地方,当多个线程同时进入判断后会造成多个实例
 INSTANCE = new UnSafeSingle();
 }
 return INSTANCE;
 }
 
 }
 
 /**
  * 通过加锁的方式实现线程安全的懒汉式单例
  * 由于获取单例的方法每次都需要进行同步,所以效率不高
  */
 class SynchronizedSingle {
 private SynchronizedSingle() {
 }
 
 private static SynchronizedSingle INSTANCE;
 
 public static synchronized SynchronizedSingle getInstance() {
 if (INSTANCE == null) {
      //由于方法加锁,所以每次只能一个线程访问,不会造成多个实例的情况,但是效率不高
 INSTANCE = new SynchronizedSingle();
 }
 return INSTANCE;
 }
 }
 
 /**
  * 双重检查
  * 当使用getInstance()方法时不会进行同步,先进行判断。
  * 由于使用了 volatile 关键字,即便有两个线程同时执行了这个方法,只要有一个线程对变量进行了修改,另外一个线程也能接收到更改的信号
  */
 class SafeSingle{
 
 private SafeSingle(){}
 
 private static volatile SafeSingle INSTANCE;
 
 public static SafeSingle getInstance(){
 if(INSTANCE==null){
        //使用了双重检查
 synchronized (SafeSingle.class){
 if(INSTANCE==null){
 INSTANCE = new SafeSingle();
 }
 }
 }
 return INSTANCE;
 }
 
 }

静态内部类

使用了静态内部类,在外部类方法调用时才会进行初始化也就实现了懒加载,并且在类加载过程中也是线程安全的,不会出现多个实例的情况. 由JVM来保证了类的唯一性.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
/**
 * 线程安全,实现了懒加载
 */
class Single {

	private Single() {
	}

	/**
	 * 内部类在getInstance()方法被调用时才会被加载
	 */
	private static class InSingle {
		private static final Single INSTANCE = new Single();
	}

	public static Single getInstance() {
		return InSingle.INSTANCE;
	}

}

枚举

枚举能避免多线程产生多个实例的问题,还能防止反序列化重新创建新的对象

1
2
3
4
5
6
7
8
9
enum Single {

	INSTANCE;

	public void sayHello(String name) {
		System.out.println("hello," + name);
	}

}

单例存在的问题

大部分情况下, 在项目中使用单例来表示一个全局唯一类, 比如配置信息, ID生成器类.

但是它也存在一些问题:

  1. 单例对OOP特性的支持不友好

    OOP的四大特性是封装, 抽象, 继承, 多态. 单例对抽象, 继承, 多态都支持的不是很好.

    假如我们有一个IdGenerator的类来生成一个全局唯一ID. 如果我们使用单例模式, 就会违背了基于接口而非实现的设计原则, 也就违背了抽象特性. 如果我们需要更改不同业务的ID生成算法, 就需要修改所有用到IdGenerator的地方

  2. 单例会隐藏类之间的依赖关系

    由于单例不需要显示创建, 不需要依赖参数传递, 可以在函数中直接调用. 所以在代码复杂的情况下, 这种调用关系就会非常隐蔽

  3. 单例对代码的扩展性不友好

    单例类只能有一个对象实例, 如果我们需要在代码中创建两个实例或多个实例, 就需要对代码有比较大的改动

  4. 单例对代码的可测试性不友好

  5. 单例不支持有参数的构造函数