简介
单例模式(Singleton Pattern)是 Java 中最简单的设计模式之一。这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。
单例模式具有3个特点:
- 单例类只能有一个实例。
- 单例类必须自己创建自己的唯一实例。
- 单例类必须给所有其他对象提供这一实例。
实现单例模式的思路是:一个类能返回对象一个引用 (永远是同一个) 和一个获得该实例的方法 (必须是静态方法,通常使用getInstance这个名称) ;当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
饿汉式
可以想象一下一个很饿的人,一到了吃饭的时候就立刻吃到饱,一旦系统开始运行的时候,就立刻把类的实例构建出来,供方法调用。
这个方式有两个特点:
- 不能延时加载 顾名思义,在系统运行开始的时候就把实例构建好,而不是要用的时候才开始构建
- 线程安全 即使在多线程的环境下,每个线程都只能拿到同一个实例,因为在多线程还没开始启动的时候,实例就已经构建好 ```java package com.mori.design.pattern.singleton;
/**
- 单例:饿汉式 *
@author mori */ public class SingletonHungry {
private static final SingletonHungry INSTANCE = new SingletonHungry();
private SingletonHungry() { }
public static SingletonHungry getInstance() {
return INSTANCE;
} }
<a name="Xlg6W"></a>
# 懒汉式
再想象一个很懒的人,等到要做事的时候才开始做事,从来不会提前准备,这就是懒汉式,等到了要使用这个实例的时候才开始构建实例,果然是很懒。
```java
package com.mori.design.pattern.singleton;
/**
* 单例:懒汉式
*
* @author mori
*/
public class SingletonLazy {
private static SingletonLazy INSTANCE = null;
private SingletonLazy() {
}
public static synchronized SingletonLazy getInstance() {
if (INSTANCE == null) {
INSTANCE = new SingletonLazy();
}
return INSTANCE;
}
}
双重检测锁式
双重检验锁式单例模式的写法主要也是实现两个目的
- 延迟加载
- 线程安全
双重检测的说法是,在判断对象为null的时候,检测了两次,第一次是在同步块外面,第二次是在同步块里面。为什么要在同步块里面还要判断null呢?这是因为在多线程并发的情况下,可能有多个线程同时进入了同步块外面的判断,如果同步块里面不再次判断为null,那么后进来的线程就会把前面创建的实例覆盖,所以需要在同步块里面再判断一次。
package com.mori.design.pattern.singleton;
/**
* 单例:双重检测锁式
* @author mori
*/
public class SingletonDoubleLockCheck {
private static volatile SingletonDoubleLockCheck INSTANCE = null;
private SingletonDoubleLockCheck() {
}
public static SingletonDoubleLockCheck getInstance() {
/* 如果不加volatile,当另一个线程执行到此处,
可能会返回一个未完全初始化的对象,此时INSTANCE!=null,但是未完全初始化 */
if (INSTANCE == null) {
synchronized (SingletonDoubleLockCheck.class) {
if (INSTANCE == null) {
// 分为三步,1.分配内存空间,2.初始化对象的内存空间 3.INSTANCE指向内存空间
// 经过指令重排序,可能是132顺序调用,当13执行完,2还没来得及执行,此时的对象是一个未完成初始化的半成品
// 若此时发生线程调度切换,此时INSTANCE!=null,但是未完全初始化
// 加上volatile,则23两步不会发生重排序,也就不会返回未完全初始化的对象
INSTANCE = new SingletonDoubleLockCheck();
}
}
}
return INSTANCE;
}
}
注意双检锁的INSTANCE的声明要加上volatile关键字,保证内存可见性。
主要在于INSTANCE = new SingletonDoubleLockCheck()这句,这不是一个原子操作,事实上在 JVM 中这句话大概做了下面 3 件事情:
- 给INSTANCE分配内存空间
- 调用构造函数来初始化成员变量
- 将INSTANCE对象指向分配的内存空间(执行完这步INSTANCE就为非null了)
但是在JVM的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时instance已经是非null了(但却没有初始化),所以线程二会直接返回INSTANCE,然后使用,然后顺理成章地报错。
所以这里的INSTANCE 一定要声明为volatile。
静态内部类式
这种写法使用JVM本身classloder的机制保证了线程安全问题;由于 inner 是私有的,除了 getInstance() 之外没有办法访问它,因此它是懒汉式的;同时读取实例的时候不会进行同步,没有性能缺陷;也不依赖 JDK 版本。 实际工作中,这种写法比较推荐。
package com.mori.design.pattern.singleton;
/**
* 单例:静态内部类式
* @author mori
*/
public class SingletonStaticInnerClass {
private SingletonStaticInnerClass() {
}
/**
* 外部类初始化的时候不会初始化内部类,只有当调用getInstance方法的时候才会初始化内部类
* 1、使用内部类,规避了 JVM 加载外部类的时候就单例进行初始化
* 2、在外部类被调用的时候内部类才会被加载(类的懒加载)
* 3、内部类必须在getInstance方法调用之前初始化(对象的懒加载)
* 4、由 static 对外部的可见性
* 5、final 保证了 INSTANCE 不被重写
*/
private static class SingletonHolder {
private static final SingletonStaticInnerClass INSTANCE = new SingletonStaticInnerClass();
}
public static SingletonStaticInnerClass getInstance() {
return SingletonHolder.INSTANCE;
}
}
枚举式
枚举类型是默认线程安全的,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象,绝对防止多次实例化。
package com.mori.design.pattern.singleton;
/**
* 单例:枚举式
* @author mori
*/
public enum SingletonEnum {
/* 枚举单例 */
INSTANCE
}
防止反射和反序列化破解单例
对于一般的项目我们无需考虑会有人特地来破解自己的系统,但是如果有朝一日需要接触到高安全性的需求,也能有所准备。
反射是Java里面非常强大的一个功能,甚至可以拿到类的私有方法,所以即使构造器私有也不能阻止通过反射拿到对象。
之前懒汉式的构造函数为空,下面在懒汉式的构造函数增加两行代码,如果实例已经创建,调用构造函数直接抛出异常,防止反射调用构造函数。
/**
* 单例:懒汉式
*
* @author mori
*/
public class SingletonLazy {
private static SingletonLazy INSTANCE = null;
private SingletonLazy() {
// 如果实例已经创建,通过反射调用构造函数,抛出异常
if (INSTANCE != null) {
throw new RuntimeException();
}
}
public static synchronized SingletonLazy getInstance() {
if (INSTANCE == null) {
INSTANCE = new SingletonLazy();
}
return INSTANCE;
}
}
如果反射发生在单例初始化之前,很遗憾,那么我们就无法通过抛异常来阻止了,这种情况下仍然可通过反射获取到不同的示例。
反序列化破解,需要我们的目标类实现了可序列化接口,通过调用ObjectInputStream对象的readObject()可以反序列化出多个单例的对象。
这种情况下,我们只需要重写反序列化的接口就可以了,在readResolve中返回单例的对象。
package com.mori.design.pattern.singleton;
import java.io.ObjectStreamException;
import java.io.Serializable;
/**
* 单例:懒汉式,实现了序列化接口
*
* @author mori
*/
public class SingletonLazy implements Serializable {
private static SingletonLazy INSTANCE = null;
private SingletonLazy() {
// 如果实例已经创建,通过反射调用构造函数,抛出异常
if (INSTANCE != null) {
throw new RuntimeException();
}
}
public static synchronized SingletonLazy getInstance() {
if (INSTANCE == null) {
INSTANCE = new SingletonLazy();
}
return INSTANCE;
}
/** 反序列化中指定了这个方法,就会覆盖反序列化的操作,直接返回实例 */
private Object readResolve() throws ObjectStreamException {
return getInstance();
}
}
总结
如果追求性能,枚举式显然是最好的,虽然枚举类型可能不方便扩展。
在项目规模不大,单例对象消耗资源不多的情况下,饿汉式绝对的完全可以胜任。
如果项目规模大,单例对象消耗的资源过多,系统资源不足的情况下,需要懒汉式创建对象,静态内部类是一个合适的好的选择。
双重检测锁由于加了volatile,性能比不上静态内部类,写法也比较复杂,它在面试中比较常见,这个也需要掌握。