DesignPattern - 单例模式【创建型】

580 阅读4分钟

欢迎关注微信公众号:FSA全栈行动 👋

一、单例模式介绍

通过单例模式可以保证系统中,应用该模式的类只有一个对象实例。

  • 好处:

    • 节省内存(没必要存在 多个功能相同的对象)
    • 提高性能(有些对象的创建很耗系统资源)
  • 分类:

    • 饿汉:提前创建对象(class 一加载就创建)
    • 懒汉:延迟创建对象(调用 getInstance() 时再创建)
  • 实现步骤:

    • 私有化构造函数
    • 提供获取单例的方法

二、单例模式代码实现

1、饿汉式

  • 饿汉方式:提前创建好对象
  • 优点:实现简单,没有多线程同步问题
  • 缺点:类一加载就会创建,不管有没有使用,instance 对象一直占着内存
/**
 * 单式模式:饿汉式
 *
 * @author GitLqr
 */
public class SingletonHungry {

	private static SingletonHungry instance = new SingletonHungry();

	private SingletonHungry() {
	}

	public static SingletonHungry getInstanceHungry() {
		return instance;
	}
}

2、懒汉式

  • 懒汉方式:延迟创建对象
  • 优点:需要用到时才会创建对象,规避不必要的内存浪费
  • 缺点:可能需要考虑有多线程同步问题

1)DCL (+ volatile) 方式

DCL,即双重检查锁定 (Double-Checked-Locking)

  • synchronized 前第一次判空:避免不必要的加锁同步,提高性能
  • synchronized 后第二次判空:避免出现多线程安全问题
  • volatile 修饰 instance:避免指令重排序问题
/**
 * 单例模式:懒汉式 (DCL +  volatile)
 *
 * @author GitLqr
 */
public class SingletonLazy {

	private static volatile SingletonLazy instance;

	private SingletonLazy() {
	}

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

2)静态内部类 方式

  • Holder 静态内部类:外部类加载时,并不会直接导致静态内部类被加载,在调用 getInstance() 时才会触发该静态内部类被加载,所以,可以延时执行。
  • Holder.instance static 字段:同一个加载器下,一个类型只会初始化一次,故天然的线程安全。
/**
 * 单例模式:懒汉式 (静态内部类)
 *
 * @author GitLqr
 */
public class SingletonLazyClass {

	private static class Holder {
		private static SingletonLazyClass instance = new SingletonLazyClass();
	}

	public static SingletonLazyClass getInstance() {
		return Holder.instance;
	}

	private SingletonLazyClass() {
	}
}

注意:静态内部类 相比 DCL 代码简洁很多,即有饿汉式的优势,又可以做到延时初始化,看似很完美,但其有一个致命问题,即无法传参,所以,实际开发中,要根据实际情况来选择其中一种实现方式。

3)枚举 方式

  • 枚举无法 new,也就无须私有化构造函数,相比 静态内部类 方式要简洁的多
  • 枚举实例创建是线程安全的
/**
 * 单例模式:懒汉式 (枚举)
 *
 * @author GitLqr
 */
public enum SingletonEnum {
	INSTANCE;

    // 枚举与普通类一样,可以拥有字段和方法
	public void method() {
		// TODO
	}
}

注意:缺点跟 静态内部类 方式一样,外部无法传参。

三、DCL 单例模式的演进

阶段一:简单的对象判空

  • 缺点:线程不安全,多线程下存在安全问题
  • 解决办法:加锁
public class SingletonLazy {

	private static SingletonLazy instance;

	private SingletonLazy() {
	}

	public static SingletonLazy getInstance() {
		if (instance == null) {
			instance = new SingletonLazy();
		}
		return instance;
	}
}

阶段二:通过加锁 synchronized 保证单例

  • 缺点:采用 synchronized 对方法加锁有很大的性能开销
  • 解决办法:锁粒度不要这么大
public class SingletonLazy {

	private static SingletonLazy instance;

	private SingletonLazy() {
	}

	public static synchronized SingletonLazy getInstance() {
		if (instance == null) {
			instance = new SingletonLazy();
		}
		return instance;
	}
}

阶段三:DCL 双重检查锁定 (Double-Checked-Locking)

  • 优点:在多线程情况下保持高性能
  • 缺点:不安全,instance = new SingletonLazy(); 不是原子性操作,这行代码为如下 3 步:
    1. 分配内存空间
    2. 在空间内创建对象
    3. 对对象赋值给引用
  • 原因:因为 JVM 指令重排序问题,可能导致线程中会按 1->3->2 的顺序执行(JVM 认为 2 和 3 没有先后顺序),其中步骤 3 会把 线程副内存中 的值写入主内存,其他线程就会读取到 instance 最新的值,但因为步骤 2 还没有执行,此时这个 instance 是不完全的对象,这时其他线程中使用这个不完全的 instance 就会出错。
  • 解决办法:使用 volatile 关键字
public class SingletonLazy {

	private static SingletonLazy instance;

	private SingletonLazy() {
	}

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

阶段四:DCL + volatile

  • volatile:是 java 提供的关键字,可以禁止指令重排序
public class SingletonLazy {

	private static volatile SingletonLazy instance;

	private SingletonLazy() {
	}

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

如果文章对您有所帮助, 请不吝点击关注一下我的微信公众号:FSA全栈行动, 这将是对我最大的激励. 公众号不仅有Android技术, 还有iOS, Python等文章, 可能有你想要了解的技能知识点哦~