单例模式的7种实现总结


单例模式介绍

  单例模式(Singleton Pattern)是 Java 中最简单、最常用的设计模式之一。单例模式提供了一种在多线程环境下保证实例唯一性的解决方案。即:对象一经初始化,后需可以直接访问,不需要再次实例化该类的对象。属于设计模式三大类中的创建型模式。在Java中,一般常用在工具类的实现、对象创建开销较大的情景下。
  单例模式具有典型的三个特点:

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例。(自我实例化)
  3. 单例类必须给所有其他对象提供这一实例。(提供全局访问点)

  单例模式虽然比较简单,但实现方式却多种多样,本篇列举了7种实现方式,并从线程安全高性能懒加载三个维度对其进行评估,比较其优劣。


一、饿汉式

  饿汉模式,顾名思义,就是采用静态初始化的方式在类被加载时就将自身实例化,所以被形象地称之为饿汉式单例模式。

// final不允许被继承
public final class HungrySingleton {
	//类的成员变量(一般类都有成员变量)
	private byte[] data = new byte[1024];
	//第一步:私有化构造器,不允许外部new操作
	private HungrySingleton(){	
	}
	//第二步:在类初始化时立即实例化该对象,从而保证线程安全
	private static HungrySingleton instance = new HungrySingleton();
	//第三步:提供一个获取全局访问点的方法
	public static HungrySingleton getInstance() {
		return instance;
	}
	//other methods
}

  饿汉式把instance作为"类变量"并且直接初始化,当主动使用Singleton 类时会完成instance的创建,包括其中的实例变量都会得到初始化,比如上例中的data数组将被创建并占用1K的空间。如果instance被ClassLoader加载后很长一段时间才被使用,那就意味着instance实例所开辟的堆内存会驻留更久的时间,如果一个类的成员占用的内存资源较多,那么采用饿汉式就有些不妥。

总结

  • 线程安全。
  • getInstance方法的性能比较高。
  • 无法进行懒加载。

注意:上面代码中有使用final关键字,强制该类不允许被继承。若父类所有的构造器都是私有的(private修饰),那么JVM规定该父类不允许被继承,因为子类的构造器都必须显示或隐式调用父类的构造器。此时可以不使用final关键字声明。

JDK饿汉式单例举例:
在这里插入图片描述


二、懒汉式

  懒汉式就是在第一次使用类实例的时候再去创建,和饿汉式在类初始化时就提前创建实例不同,所以就被称为懒汉式单例模式。

//final不允许被继承
public final class LazySingleton {
	//类的成员变量(一般类都有成员变量)
	private byte[] data = new byte[1024];
	// 未实例化的类变量
	private static LazySingleton instance = null;
	// 私有化构造器
	private LazySingleton() {
    }
	// 运行时加载对象
	public static LazySingleton getInstance() {
		//判断是否已经初始化过(没有同步机制控制,多线程不安全)
		if (instance == null) {
			instance = new LazySingleton();
		}
		return instance;
	}
	//other methods
}

总结

  • 线程不安全。instance是共享资源,当多个线程对其访问时需要保证共享资源的同步性,因此线程不安全,无法保证单例的唯一性。(更加具体的原因不摊开说明了)
  • 性能和懒加载,就不讨论了,因为这种方法本身就不正确。

三、懒汉式+synchronized同步

  上述的懒汉式保证了实例的懒加载,但无法保证实例的唯一性,需要增加对共享资源instance的同步访问机制,可以采用synchronized关键字实现。

//final不允许被继承
public final class LazySingleton {
	//类的成员变量(一般类都有成员变量)
	private byte[] data = new byte[1024];
	// 未实例化的类变量
	private static LazySingleton instance = null;
	// 私有化构造器
	private LazySingleton() {
    }
	// 运行时加载对象(增加了synchronized,每次只能有一个线程能够进入)
	public static synchronized LazySingleton getInstance() {
		//判断是否已经初始化过(没有同步机制控制,多线程不安全)
		if (instance == null) {
			instance = new LazySingleton();
		}
		return instance;
	}
	//other methods
}

总结

  • 线程安全,能够保证实例的唯一性;
  • getInstance方法采用synchronized关键字所以性能较低
  • 懒加载。

四、Double-Check式(注意有坑)

  Double-Check的方式是一种更加高效的数据同步策略,只有首次初始化时才加锁,之后多个线程获取实例时都无需同步控制。

// final不允许被继承
public final class LazySingleton {
	//类的成员变量(一般类都有成员变量)
	private byte[] data = new byte[1024];
	// 未实例化的类变量
	private static LazySingleton instance = null;
	// 私有化构造器
	private LazySingleton() {
	}

	public static LazySingleton getSingleton() {
		// 若instance不为null,则不用获取锁,提升了效率
		if (instance == null) {
			//同步加锁是为了线程安全,确保只有一个线程创建实例
			synchronized (LazySingleton.class) {
				//再次判空是为了保证单例对象的唯一性,只有没被创建才去创建
				if (instance == null) {
					instance = new LazySingleton();
				}
			}
		}
		return instance;
	}
	//other methods
}

  当两个线程同时发现instance == null成立时,只有一个线程有资格进入synchronized同步代码块完成instance的实例化,随后的线程进入synchronized同步代码块后发现instance == null不成立则无需再次实例化,以后对getSingleton方法的访问也不需要执行synchronized同步代码块,大大提升了性能。满足懒加载、线程安全、高性能这三个标准,一切看起来很完美。但这种方式在多线程环境下可能会导致空指针异常,原因如下。

首先,我们要理解new LazySingleton()做了什么,详细的介绍请查看《java new一个对象的过程中发生了什么》。本篇简单介绍new一个对象需要的4个步骤,如下:

  1. 看class对象是否加载,如果没有就先加载class对象。(加载)
  2. 为类的静态变量分配内存空间并为其初始化默认值(连接阶段),为静态变量赋予正确的初始值(初始化阶段)。
  3. 调用构造函数。(单例比较复杂时,有很多的成员变量需要初始化)
  4. 返回地址给引用。

然后,cpu为了优化程序,可能会进行指令重排序,打乱这3,4这几个步骤,导致实例内存还没分配(或是只实例化了部分成员变量),就被使用了,导致空指针。下面举例:

线程A执行到new LazySingleton(),开始初始化实例对象,由于存在指令重排序,先执行步骤4,先把引用instance赋值了,此时还没有执行构造函数(或执行还未完成,只实例化了部分成员变量),这时CPU时间片耗尽,切换到线程B执行,线程B调用new LazySingleton()方法,发现instance == null不成立,就直接返回引用地址了,然后线程B执行了一些操作,就可能导致线程B使用了还没有被初始化的变量,报空指针错误。


五、Volatile + Double-Check式(最终版)

  Double-Check是一种巧妙的设计,但由于JVM在运行new LazySingleton()时可能对指令重排序,导致空指针异常。而volatile关键字可以防止重排序,因此还需要对Double-Check方式稍加修改。关于volatile关键字的用法,可参考另一篇博文《深入理解volatile关键字》

// 加volatile 修饰
private static volatile LazySingleton instance = null;

  至此,就有了一个线程安全、高性能、懒加载版本的双重检查加锁式单例模式。但这种写法对于初学者很棘手,一下子很难理解。


六、Holder式

直接上代码,然后给出说明。

// 不允许被继承
public final class HolderSingleton {
	// 类的成员变量
	private byte[] data = new byte[1024];
	// 私有化构造器
	private HolderSingleton() {
	}
	// 静态内部类 Holder中持有实例instance
	private static class Holder{
		private static HolderSingleton instance = new HolderSingleton();
	}
	// 调用getSingleton,返回Holder的instance类属性
	public static HolderSingleton getSingleton() {
		return Holder.instance;
	}
	//other methods
}

这种方式利用了类加载的特点。HolderSingleton 类中没有持有静态的instance实例,而是放在静态内部类Holder中,该方式仍然需要私有化构造器。当Holder被主动引用时(懒加载)会创建HolderSingleton 的实例,JVM保证实例的唯一性,性能高。是目前广泛采用的一种单例设计。

七、枚举式

八、防止反射/反序列化攻击单例类

  上面的单例实现方式中,除了枚举类型外,其他的实现方式是可以被JAVA的反射机制攻击的。享有特权的客户端可以借助AccessibleObject.setAccessible方法通过反射机制调用私有构造器,如果需要抵御这种攻击,可以修改构造器,让它的被要求创建第二个实例的时候抛出一个异常。具体方式请参考 《如何防止JAVA反射对单例类的攻击?》 《设计模式——单例模式》


参考资料

  1. 《java高并发编程详解》 汪文君
  2. 《Effective Java 中文版 第2版》
  3. 如何防止JAVA反射对单例类的攻击?
  4. 单例模式为什么要用Volatile关键字
  5. java new一个对象的过程中发生了什么
  6. 设计模式——单例模式
  7. 深入理解Java枚举类型(enum)
GNG
发布了118 篇原创文章 · 获赞 389 · 访问量 68万+

猜你喜欢

转载自blog.csdn.net/so_geili/article/details/101354344