一、什么是单例模式?
顾名思义,就是某个类在全局中只有一个实例。如果一个类可以在外部随意通过new方法来实例化,那么它一定不是单例的。无论在哪里获取该类的实例,都应该是唯一的实例,即在不同地方获取到的实例实际上是同一个对象,哪怕是在不同的线程里获取到的依然是唯一实例。
通常我们用getInstance()方法来对外提供该类的单例。
二、单例模式的实现方式:
2.1 简单的饿汉式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {
// 在私有构造方法里进行一些变量的初始化操作
}
public static getInstance() {
return instance;
}
}
所谓饿汉,即提前加载,在第一次引用该类的时候就直接进行实例化(static关键字的作用)—即使并没有调用getInstance()方法。(可以理解为一个汉子饿怕了,所以不管他到底需不需要吃东西,都提前把吃的已经准备好了)
这种形式的缺点:资源浪费!假如我们只是引用该类的其他变量,也依然会实例化,造成了资源浪费。
2.2 简单的懒汉式
public class Singleton {
private static Singleton instance = null;
private Singleton() {}
public static Singleton getInstance() {
if(instance == null) {
instance = new Singleton();
}
return instance;
}
}
所谓懒汉,即实现懒加载,只有在调用到getInstance()方法的时候才会实例化。
这种形式的缺点:线程不安全!如果有多个线程同时调用getInstance()方法,就完全无法保证单例了,很可能导致new了多个对象,尤其是在私有构造方法里,如果里面的初始化操作比较多,就特别容易出现new了多个对象。
2.3 DCL型懒汉式
public class Singleton {
private static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
所谓DCL,就是Double-Checked Locking,也就是双重检查+锁的方式。
这种形式看起来似乎没问题了:10个线程在调用getInstance()时,instance为null,一个线程拿到锁,在锁里再经过一次检查,instance依然为null,则进行初始化操作。其他线程则在进行等待,在第一个线程完成初始化并拿到了instance实例后,再依次获取实例。但实际上这种形式依然存在一个坑,有一定的可能导致获取到的instance为null!问题就出在instance = new Singleton();这里了,实际上它并不是原子性的(atomic),一个简单的初始化实际上分成3个步骤:
- 1、分配内存
- 2、将分配的内存指向实例的引用
- 3、将对象Singleton初始化给分配的内存
如果第一个线程的还卡在“将对象Singleton初始化给分配的内存”这里,第二个线程就有可能获取到锁,然后开心的以为拿到了Non-Null的instance,用的时候直接就挂了……所以这段代码还是需要更改的。
2.4 DCL增强型懒汉式
public class Singleton {
private volatile static Singleton instance = null;
private Singleton(){}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
所谓增强,跟之前的一段代码相比,只多了一个volatile关键字。
当volatile用于一个作用域时,Java保证如下:
- 1、(适用于Java所有版本)读和写一个volatile变量有全局的排序。也就是说每个线程访问一个volatile作用域时会在继续执行之前读取它的当前值,而不是(可能)使用一个缓存的值。(但是并不保证经常读写volatile作用域时读和写的相对顺序,也就是说通常这并不是有用的线程构建)。
- 2、(适用于Java5及其之后的版本)volatile的读和写建立了一个happens-before关系,类似于申请和释放一个互斥锁。
显然第二点是我们所需要的。在使用了volatile之后,第一个线程的初始化未完成时,其他线程是得不到instance的。
至此,问题已经圆满解决了!但是,还有没有更好的办法?当然有!
2.5 静态内部类单例模式
public class Singleton {
private Singleton() {}
private static class SingletonHolder {
private static Singleton singletonHolder = new Singleton();
}
public static Singleton getInstance() {
return SingletonHolder.singletonHolder;
}
}
把Singleton实例放到一个静态内部类SingletonHolder中,这样就避免了静态实例在Singleton类加载的时候就创建对象,并且由于静态内部类只会被加载一次,所以这种写法也是线程安全的,可以说这种方法已经比较完善了,写起来也不复杂。
至此,我们通过各种尝试,已经实现了既保证单例又能实现延迟加载的实现了,基本上快要达到圆满了。但是,凡是就怕但是,它们依然不完美:
如果不自行实现序列化,那么每次去反序列化一个序列化的对象都会创建一个新的实例。
虽然我们的构造方法是private的,但在其他地方如果通过反射的方式还是可以调用构造方法,当然我们可以通过在构造方法里进行判断,创建第二个实例的时候抛出异常。
那么到底有没有,完美的、一劳永逸的、简洁明了的单例呢?有!
2.6 枚举实现单例模式
public enum Singleton {
instance;
public static Singleton getInstance() {
return instance;
}
}
是的,enum的构造方法天生就是私有的。也直接提供了线程安全,还可以防止反序列化时创建新对象,说是完美并不夸张。
也许有人会有疑问,以前Android官网明确写着尽量不要用enum,因为会增加内存开销:
毫无疑问,枚举一定会比直接定义常量多占用内存(因为枚举是一个完整的类,而常量只占用了最基本的内存),但我们是在实现单例啊!本身就要写一个类,所以枚举本来的那点问题就不算是问题了。再说,以Android设备现在的硬件配置,尤其是内存容量,我们完全不需要避讳枚举了。如果通过利用枚举能显著提升程序的可读性和可维护性,那么就不需要纠结这点内存消耗了。
最后,此文借鉴于某作者的原文:https://www.kaelli.com/24.html ,特此感谢!