单例模式 - 饿汉式与懒汉式详解

什么是单例模式?

对于一个软件系统中的某些类而言,只有一个实例很重要,就像Windows中的任务管理器一样,只能打开一个。如果不适用机制对窗口对象进行唯一化,必定会弹出多个窗口。如果这些窗口显示的内容完全一致,则是重复对象,浪费内存资源。如果内容不一致,则意味着某一瞬间系统有多个状态,与实际不符,也会给用户带来误解,不知道哪一个是真实的状态。因此有时确保系统某个对象的唯一性非常重要。

单例对象的类必须保证只有一个实例存在,许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。

Spring框架中就用到了单例模式,比如说配置的一个Bean,如果不做配置,则默认就是单例的。

 

优点:

1、可以确保所有的对象都访问同一个实例

2、可以节约系统资源,避免了频繁创建和销毁的对象

3、避免对共享资源的多重占用

缺点:

1、不适合变化的对象,如果同一类型的对象总是要在不同场景发生变化,单例就会造成数据错误

2、职责过重,一定程度上违背了”单一职责原则“

3、如果实例化的对象长时间不被利用,系统可能会认为是垃圾而被挥手

 

 

单例模式分为两种,一种是饿汉式,一种是懒汉式

一、饿汉式单例类

public class HungrySingleton {
	// 类加载,初始化
	private static final HungrySingleton instance = new HungrySingleton();
	
	private HungrySingleton() {}
	
	public static HungrySingleton getInstance() {
		return instance;
	}
}

饿汉式的特点就是当类被加载是,静态变量instance就会被初始化,此时类的私有构造方法会被调用,单例类的唯一实例将被创建。

二、懒汉式单例类

public class LazySingleton {
	private static LazySingleton instance = null;
	
	private LazySingleton() {}
	
	public static LazySingleton getInstance() {
		// 如果实例没被创建,就创建
		if(instance == null) {
			instance = new LazySingleton();
		}
		return instance;
	}
}

这是极简版的懒汉式单例类,它的构造方法同样是私有的。但是,它与饿汉式不同的是,它在第一次被引用的时候才会实例化,即在类加载的时候不会将自己实例化。它在第一次调用getInstance方法的时候实例化,这种技术又称为延迟加载技术,即在需要的时候才加载实例。这段代码存在线程问题,下面开始解决这个问题,优化代码。

为了避免多个线程同时调用getInstance方法,可以使用关键字synchronized包裹。

// 确保任意时刻只有一个线程可以执行该方法
synchronized public static LazySingleton getInstance() {
	// 如果实例没被创建,就创建
	if(instance == null) {
		instance = new LazySingleton();
	}
	return instance;
}

上诉代码虽然加了synchronized锁定线程,但是每次调用getInstance都需要进行线程锁定判断,在多线程高并发访问环境下会导致系统性能大大降低。分析发现,核心代码是instance = new LazySingleton(),所以我们仅需锁定这段代码即可。

public static LazySingleton getInstance() {
	// 如果实例没被创建,就创建
	if(instance == null) {
		synchronized(LazySingleton.class) {
			instance = new LazySingleton();
		}
	}
	return instance;
}

这段代码看似解决了性能问题,实际上,还存在问题,可能存在单例对象不唯一的情况。比如说,在某一时刻,线程A和线程B同时调用getInstance方法。因为加锁的原因,有一个线程(A)先执行代码,另一个(B)需要等待。因为锁定的是实例化的那块代码,没有锁定If(instance == null)这块代码,也就是说两个线程都通过了这个判断语句。那么线程A执行完,创建了一个实例,该线程B去执行了,那么B又会创建一个实例。这两个实例肯定不是同一个,他们都是单独new出来的。

因此,需要加一个双重判断检查锁定来解决这个问题。

public class LazySingleton {
	// 被volatile修饰的变量可以确保多个线程能正常处理
	private volatile static LazySingleton instance = null;
	
	private LazySingleton() {}
	
	public static LazySingleton getInstance() {
		// 第一层判断,如果实例已经创建,跳过
		if(instance == null) {
			synchronized(LazySingleton.class) {
				// 第二层判断,如果实例创建,跳过
				if(instance == null) {
					instance = new LazySingleton();
				}
			}
		}
		return instance;
	}
}

通过两层判断,解决了实例对象可能不唯一的问题。这里可能有人会疑惑,第一层可以不要。实际这里也是一个性能问题。第一层我们要解决的是实例是否已经创建的问题,创建了,就可以不用去判断线程锁定问题。第二层触发条件就是第一层没通过,即没有创建实例,是为了解决多个线程同一时刻调用创建对象的方法时可能造成对象不唯一的问题的。

如果使用双重检查锁定来实现懒汉式,需要在静态成员instance前加上volatile修饰,该修饰符在JDK1.5以上才能用,它会屏蔽Java虚拟机所做的一些代码优化,可能会导致系统运行效率降低,所以这种办法似乎也不是最完美的。

3.饿汉式和懒汉式对比

饿汉式的优点是无需考虑多个线程同时访问的问题,可以确保实例的唯一性。从调用速度和反应时间来说,因为它一开始就创建了对象,它是优于懒汉式的。但是从资源利用角度来说,它不如懒汉式,因为饿汉式无论你是否能用到这个单例对象,它都会给你创建,懒汉式是在你用到才会给你创建,而且系统加载时就需要创建饿汉式单例对象,可能会造成加载时间变长。

懒汉式,需要去解决多线程的问题,特别是当单例类作为资源控制器,在实例化时必然涉及到资源初始化的问题,可能会耗费大量时间,意味着多线程首次引用此类几率变大,所以要通过双重判断来进行控制,就会导致性能下降。

4.完美方案 - 使用静态内部类实现单例模式

在Java中,可以通过Initialization on Demand Holder(IoDH)来实现单例模式,该方法可以实现延迟加载,又可以保证线程安全,不影响系统性能,但是很多语言是不支持IoDH的。

public class Singleton {
	private Singleton() {}
	
	// 静态内部类
	private static class HolderClass {
		private final static Singleton instance = new Singleton();
	}
	
	public static Singleton getInstance() {
		return HolderClass.instance;
	}
}
public class Test {
	public static void main(String[] args) {
		Singleton s1 = Singleton.getInstance();
		Singleton s2 = Singleton.getInstance();
		System.out.println(s1 == s2);
	}
}

 结果为true,表明通过这种方式创建的对象是唯一的。

因为静态单例对象没有作为SIngleton的成员变量,所以在类加载的时候不会实例化Singleton,第一次调用getInstance时就会加载内部类HolderClass,内部类中定义了一个静态变量instance,这个时候才会初始化这个成员变量,由Java虚拟机来保证线程安全,确保该成员变量只能初始化一次。这里因为没有用synchronized锁定,所以不会造成任何性能影响。

 

你可能感兴趣的:(设计模式,单例模式,饿汉式,懒汉式,IoDH单例模式)