单例模式是什么
单例模式保证一个类只有一个实例,并且提供一个全局可以访问的入口。取个例子:比如孙悟空的分身术,每一个分身都对应同一个真身
为什么要用单例模式
- 节省内存
- 节省计算,确保计算结果的正确性
- 方便管理
单例模式使用场景
无状态的工具类
- 日志工具类
- 字符串工具类
- ...
全局信息类
- 全局技术
- 环境变量
- ...
常见单例模式写法
饿汉式
用 static
修饰实例,private
修饰构造函数
- 饿汉式
public class Singleton {
//加载时就完成的实例,避免线程同步的问题
private static Singleton singleton = new Singleton();
private Singleton() {
}
//返回实例
public static Singleton getInstance(){
return singleton;
}
}
- 饿汉式的变种
写法和饿汉式差不多,只不过是把类实例化的过程放在了静态代码块中。
public class Singleton {
private static Singleton singleton;
static {
singleton = new Singleton();
}
private Singleton() {
}
public static Singleton getInstance() {
return singleton;
}
}
优缺点: 在类装载(JVM ClassLoader)
的时候就完成了实例化,所以只有这一次,线程是安全的。没有达到懒加载的效果,如果这个实例没有得到使用,就会造成内存的浪费,影响性能,不能保证实例对象的唯一性。
懒汉式
在 getInstance()
方法被调用的时候才去实例化对象,起到了懒加载
的效果,但只能在单线程下使用。单线程进入if判断还没来得及往下执行,另一个线程也通过了这个判断,就会多次创建实例。所以多线程环境下不能使用懒汉式的写法。
- 线程不安全写法
public class Singleton {
private static Singleton singleton;
private Singleton() { }
public static Singleton getInstance() {
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
- 线程安全写法
public static synchronized getInstance() {
...
}
就在 getInstance()
方法上加上 synchronized
关键字就可以解决之前的线程安全问题。缺点是由于synchronized
关键字会导致退化到串行执行,效率低,大多数情况下是没有必要的。
- 线程安全进一步优化的写法
采用代码块的形式保护线程安全,synchronized
放到代码块,但是这种写法也是有问题的。加入一个线程进入了第一个if,还没往下执行,而另一个线程也通过了这个判断就会产生多个实例。
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
singleton = new Singleton();
}
}
return singleton;
}
双重检查式
DCL(Double-Check-Locking) 其实也是在 getInstance()
方法的操作,还给对象加了 volatile
关键字。这里进行了两次if判断保证线程安全,实例化代码只调用一次。后面再访问只会判断第一次的if,跳过整个if快,直接return实例化对象。
public class Singleton {
private static volatile Singleton singleton;
private Singleton() { }
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
优缺点 线程安全,延迟加载。因指令重排引起空指针异常。
面试问题:为什么要double-check,去电第二次行不行?
需考虑的情况:有两个线程同时调用getInstance()
方法,并且由于singleton是空的,所以两个线程都可以通过第一个if,由于锁机制的存在,会有一个线程先进入同步语句,并进入第二次if判断,而另一个还在等待,当第一个线程执行完 new Singleton()
语句后,就会推出 synchronized
保护的区域,这时如果没有第一次if的话,那么第二个线程也会创建一个实例。这就破坏了单例模式。对于第一个check而言,去过去掉了,那么所有线程就会串行执行。
- 为什么要用voletile关键字?
用voletile关键字的作用主要在于singleton = new Singleton();这并非是一个原子操作,在JVM中,singleton = new Singleton()这语句至少了做了三件事:
- 给 singleton 分配内存空间
- 调用Singleton的构造函数来初始化singleton
- 将singleton对象指向分配的内存空间(此时singleton就不是null了)
这里存在重排序的优化,2,3 的顺序是不能保证。有可能是123,也可能是132,如果是后者,那么对象初始化就没有完全初始化,用对象的时候就会报错。
使用voletile的意义就在于防止重排序的发生。
静态内部类式
跟饿汉式的方法才用的机制类似,都采用类装载的方式来保证初始化时只有一个线程。
public class Singleton {
private static class SingletonInstance {
private static final Singleton singleton = new Singleton();
}
private Singleton() { }
public static Singleton getInstance() {
return SingletonInstance.singleton;
}
}
优缺点 饿汉式在装载时立刻实例化,静态内部类并不会立刻实例化对象,而是在需要实例化(调用getInstance()
)时再实例化。优点跟DCL写法是一样的,都避免了线程不安全的问题,并且延迟加载,性能高。但是他们不能防止被反序列化,生成多个实例。
枚举式
(最佳选择)
借助JDK 1.5 枚举类实现单例模式,不仅能够避免多线程同步,还能防止反序列化和反射创建新的对象。
public enum Singleton {
INSTANCE;
public void whateverMethod(){
}
}
优缺点 :写法简洁、线程安全、避免反序列化
注释 Java专门对枚举类的序列化做了规定:在序列化时,仅仅是将枚举对象的name属性输出到结果中,在反序列化时,通过java.lang.Enum的valueOf方法来根据名字查找对象,而不是创建一个新的对象。反射通过newInstance创建对象时,会检查这个类是否是枚举类。如果是抛出IllgalArgumenException("Cannot reflecively create enum objects")异常,反射创建失败,通过这两种方式来防止反序列化破坏单例模式。