Java单例模式详解,包括各种方式的实现

单例对象(Singleton)是一种常用的设计模式。在 Java 应用中,单例对象能保证在一个 JVM中,该对象只有一个实例存在。这样的模式有几个好处:

  1. 某些类创建比较频繁,对于一些大型的对象,这是一笔很大的系统开销。
  2. 省去了 new 操作符,降低了系统内存的使用频率,减轻 GC 压力。
  3. 有些类如交易所的核心交易引擎,控制着交易流程,如果该类可以创建多个的话,系统完全乱了。(比如一个军队出现了多个司令员同时指挥,肯定会乱成一团),所以只有使用单例模式,才能保证核心交易服务器独立控制整个流程。

单例模式有好几种实现方式,其中包括线程安全的和线程不安全的,接下来就实现一遍。

饿汉式的单例模式

public class Singleton {
	//构造器私有化,防止被实例化
	private Singleton() {
		
	}
	//持有私有静态实例,防止被引用,饿汉式先创建对象
	private static Singleton Instance = new Singleton();
	
	//提供一个对外的方法调用
	public static Singleton getInstance() {
		return Instance;
	}
	
	//多线程验证是否安全
	public static void main(String[] args) {
		for(int i = 1; i <= 10; i++) {
			new Thread(()->{
				Singleton instance1 = Singleton.getInstance();
				System.out.println(instance1.hashCode());
			}).start();
		}
	}
}

java 单类实现方式 java中单_java 单类实现方式


可以看到,饿汉式的单例模式在多线程下是安全的,每个线程获得的都是同一个实例的哈希地址。但是饿汉式也是有缺点,不能实现延迟加载的策略,当数据过多时,提前加载,占据内存,显然不是我们想要的,我们大多数需要使用到的时候再加载实例,这就用到了懒汉式单例模式

懒汉式单例模式(不安全)

方式一:普通懒汉式,不安全

public class LazySingleton {
	private LazySingleton() {
		//测试输出代码(不必要)
		System.out.println(Thread.currentThread().getName());
	}
	//初始不实例化,需要使用的时候才实例化
	private static LazySingleton Instance;
	//对外提供调用的方法,实现懒加载的效果
	public static LazySingleton getInstance() {
		if(Instance == null) {
			Instance = new LazySingleton();
		}
		return Instance;
	}
	
	public static void main(String[] args) {
		for(int i = 1; i <= 1000; i++) {
			new Thread(()->{
				LazySingleton.getInstance();
			}).start();
		}
	}
}

java 单类实现方式 java中单_多线程_02


可以看到,在多线程下哈希值不一样,说明不同的线程创建了多个实例。

方式二、改进懒汉式
首先会想到对 getInstance 方法加synchronized 关键字,其它的都不变。如下:

public static synchronized LazySingleton getInstance() {
		if(Instance == null) {
			Instance = new LazySingleton();
		}
		return Instance;
	}

这样虽然线程安全了,但是每一次线程进入该方法中时都是阻塞式进入,每次都只允许一个线程进入,synchronized 关键字锁住的是这个对象,这样的用法,在性能上会有所下降,因为每次调用 getInstance(),都要对对象上锁(每进来一次都要上锁,就算已经实例化Instance,也要上锁),事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了,所以,这个地方需要改进。

方式三、双重检测,效率快许多

public static LazyMan getInstance() {
		if(Instance == null) {
			synchronized(LazyMan.class) {
				if(Instance == null) {
					Instance = new LazySingleton();
				}
			}
		}
		return Instance;
	}

将 synchronized 关键字加在了内部,也就是说当调用的时候是不需要加锁的,只有在 instance 为 null,并创建对象的时候才需要加锁,性能有一定的提升。

但是,这样的情况,还是有可能有问题的,看下面的情况:在 Java 指令中创建对象和赋值操作是分开进行的,也就是说 instance = new LazySingleton();语句是分两步执行的。但是JVM 并不保证这两个操作的先后顺序,也就是说有可能 JVM 会为新的 Singleton 实例分配空间,然后直接赋值给 instance 成员,然后再去初始化这个 Singleton 实例。这样就可能出错了。

以 A、B 两个线程为例:

  1. A、B 线程同时进入了第一个 if 判断
  2. A 首先进入 synchronized 块,由于 instance 为 null,所以它执行 instance = new LazySingleton();
  3. 由于 JVM 内部的优化机制(指令重排),JVM 先画出了一些分配给 Singleton 实例的空白内存,并赋值给 instance 成员(注意此时 JVM 没有开始初始化这个实例),然后 A 离开了 synchronized块。
  4. B 进入 synchronized 块,由于 instance 此时不是 null,因此它马上离开了 synchronized
    块并将结果返回给调用该方法的程序。
  5. 此时 B 线程打算使用 Singleton 实例,却发现它没有被初始化,于是错误发生了。

方式四、使用volatile关键字,防止指令重排,其它不变

public class LazyMan {
	private LazyMan() {
		System.out.println(Thread.currentThread().getName());
	}

	private static volatile LazyMan Instance;
	
	public static LazyMan getInstance() {
		if(Instance == null) {
			synchronized(LazyMan.class) {
				if(Instance == null) {
					Instance = new LazyMan();
				}
			}
		}
		return Instance;
	}
	
	public static void main(String[] args) {
		for(int i = 1; i <= 1000; i++) {
			new Thread(()->{
				LazyMan.getInstance();
			}).start();
		}
	}
}

方式五、改进双重检测,效率一样,防止以上问题

private static synchronized void InitIns() {
		if(Instance == null) {
			Instance = new LazySingleton();
		}
	}
	
	public static LazyMan2 getInstance() {
		if(Instance == null) {
			InitIns();
		}
		return Instance;
	}

此方式也只会创建一次实例,线程安全,效率高

方式六、使用静态内部类的方式

public class LazySingleton {
	private LazySingleton() {
		
	}
	//静态内部类
	private static class SingletonFactory{
		private static LazySingleton Instance = new LazySingleton();
	}
	
	public static synchronized LazySingleton getInstance() {
		return SingletonFactory.Instance;
	}
	
	public static void main(String[] args) {
		for(int i = 1; i <= 10; i++) {
			new Thread(()->{
				LazySingleton lazySingleton = LazySingleton.getInstance();
				System.out.println(lazySingleton.hashCode());
			}).start();
		}
	}
}

将原来的一个方法中的双重检测改为使用内部类方式。JVM 内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用 getInstance 的时候,JVM 能够帮我们保证 instance 只被创建一次,并且会保证把赋值给 instance 的内存初始化完毕,这样我们就不用担心上面的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。

还没完

看了这么多的单例的实现,以为就OK了?no no no!上述单例模式看起来是安全的,但是一旦用到了反射,就会破坏单例模式的安全性。

public class LazySingleton2 {
	private LazySingleton2() {
		
	}

	private static volatile LazySingleton2 Instance = null;
	
	public static LazySingleton2 getInstance() {
		if(Instance == null) {
			synchronized (LazySingleton2.class) {
				if(Instance == null) {
					Instance = new LazySingleton2();
				}
			}
		}
		return Instance;
	}
	
	public static void main(String[] args) throws Exception{
		LazySingleton2 instance1 = LazySingleton2.getInstance();
		Constructor<LazySingleton2> declaredConstructor = LazySingleton2.class.getDeclaredConstructor(null);
		//破坏private
		declaredConstructor.setAccessible(true);
		LazySingleton2 instance2 = declaredConstructor.newInstance();
		System.out.println(instance1.hashCode());
		System.out.println(instance2.hashCode());
	}
}

java 单类实现方式 java中单_多线程_03


可以看到,两个实例的哈希值不同,破坏了单例模式。

此时我们可以在加一个检测,检测是否使用反射创建对象
只需要在构造方法中添加

private LazySingleton2() {
		if(Instance != null) {
			throw new RuntimeException("请不要使用反射破坏单例");
		}
	}

我们一个实例用普通方式创建,一个用反射创建,如果使用反射创建对象,破坏了单例之后,就会抛出异常,到时处理异常即可。

但是这就安全了吗?
现在尝试使用多个反射创建对象

public class LazySingleton2 {
	private LazySingleton2() {
		if(Instance != null) {
			throw new RuntimeException("请不要使用反射破坏单例");
		}
	}

	private static volatile LazySingleton2 Instance = null;
	
	public static LazySingleton2 getInstance() {
		if(Instance == null) {
			synchronized (LazySingleton2.class) {
				if(Instance == null) {
					Instance = new LazySingleton2();
				}
			}
		}
		return Instance;
	}
	
	public static void main(String[] args) throws Exception{
		Constructor<LazySingleton2> declaredConstructor = LazySingleton2.class.getDeclaredConstructor(null);
		//破坏private
		declaredConstructor.setAccessible(true);
		//这里创建对象发生改变
		LazySingleton2 instance1 = declaredConstructor.newInstance();
		LazySingleton2 instance2 = declaredConstructor.newInstance();
		System.out.println(instance1.hashCode());
		System.out.println(instance2.hashCode());
	}
}

java 单类实现方式 java中单_设计模式_04


还是创建了两个不同的对象,破坏了单例。(⊙﹏⊙)(⊙﹏⊙)(⊙﹏⊙),好烦啊

设置标志位防止反射破坏

我们设置一个假设为qwdx的标志位

public class LazySingleton2 {
	//在此处设置一个标志位
	private static boolean qwdx = false;
	
	private LazySingleton2() {
		if(qwdx = false) {
			qwdx = true;
		}else {
			throw new RuntimeException("请不要使用反射破坏单例");
		}
	}

	private static volatile LazySingleton2 Instance = null;
	
	public static LazySingleton2 getInstance() {
		if(Instance == null) {
			synchronized (LazySingleton2.class) {
				if(Instance == null) {
					Instance = new LazySingleton2();
				}
			}
		}
		return Instance;
	}
	
	public static void main(String[] args) throws Exception{
		Constructor<LazySingleton2> declaredConstructor = LazySingleton2.class.getDeclaredConstructor(null);
		//破坏private
		declaredConstructor.setAccessible(true);
		LazySingleton2 instance1 = declaredConstructor.newInstance();
		LazySingleton2 instance2 = declaredConstructor.newInstance();
		System.out.println(instance1.hashCode());
		System.out.println(instance2.hashCode());
	}
}

java 单类实现方式 java中单_java 单类实现方式_05


处理OK,通过一个标志位,解决反射创建对象破坏单例的问题。

结束了吗?没有,当我们知道该标志位的变量,我们就能用反射破坏它。如下:

public class LazySingleton2 {
	
	private static boolean qwdx = false;
	
	private LazySingleton2() {
		if(qwdx == false) {
			qwdx = true;
		}else {
			throw new RuntimeException("请不要使用反射破坏单例");
		}
	}

	private static volatile LazySingleton2 Instance = null;
	
	public static LazySingleton2 getInstance() {
		if(Instance == null) {
			synchronized (LazySingleton2.class) {
				if(Instance == null) {
					Instance = new LazySingleton2();
				}
			}
		}
		return Instance;
	}
	
	public static void main(String[] args) throws Exception{
		Constructor<LazySingleton2> declaredConstructor = LazySingleton2.class.getDeclaredConstructor(null);
		//破坏private
		declaredConstructor.setAccessible(true);
		//假设我们知道了标志位变量qwdx,我们就可以破坏它
		Field field = LazySingleton2.class.getDeclaredField("qwdx");
		field.setAccessible(true);
		
		LazySingleton2 instance1 = declaredConstructor.newInstance();
		
		//将第一个对象的标志位重置
		field.set(instance1, false);
		
		LazySingleton2 instance2 = declaredConstructor.newInstance();
		System.out.println(instance1.hashCode());
		System.out.println(instance2.hashCode());
	}
}

java 单类实现方式 java中单_java 单类实现方式_06


此时,通过反射又得到了两个实例。单例又被破坏。我XXX

怎么能避免这些情况呢?JDK5提出了枚举方式

public class Test{
	public static void main(String[] args) {
		EnumSingleton instance1 = EnumSingleton.getInstance();
		EnumSingleton instance2 = EnumSingleton.getInstance();
		System.out.println(instance1 == instance2);
	}
}

enum  EnumSingleton{

	INSTANCE;
	
	public static EnumSingleton getInstance() {
		return INSTANCE;
	}
}

结果返回 true,证明是同一个对象。

使用反射创建对象

public class Test{
	public static void main(String[] args) throws Exception{
		Constructor<EnumSingleton> declaredConstructor = EnumSingleton.class.getDeclaredConstructor(null);
		declaredConstructor.setAccessible(true);
		EnumSingleton instance1 = declaredConstructor.newInstance();
		EnumSingleton instance2 = declaredConstructor.newInstance();
		System.out.println(instance1 == instance2);
	}
}

enum  EnumSingleton{

	INSTANCE;
	
	public static EnumSingleton getInstance() {
		return INSTANCE;
	}
}

java 单类实现方式 java中单_多线程_07


结果抛出异常,没有找到该无参构造器的异常,但是枚举类中明明有无参构造器,为什么抛出该异常?

我们反编译一下看看

java 单类实现方式 java中单_单例模式_08


通过反编译发现其中有一个空参的构造方法(倒数第三行),但是我们在使用反射时,通过空参构造器得到对象发生异常,异常显示无该构造器,说明枚举类在编译时期是一个“欺骗性”的类,可以防止反射创建对象和破坏单例。

我勒个去,单例涉及到多线程时这么复杂啊!加油(ง •_•)ง💪!!!