目录
1. 定义
程序运行过程中,某类在内存中仅存在一个实例对象。
2. 使用场景
在程序中需要多次创建某类对象且使用对象目的又完全相同时,可以使用考虑使用单例模式来实现此类,避免在内存中频繁创建和销毁此类对象。
3. 实现方式
3.1 饿汉式
饿汉式:在类加载时就预先创建实例对象,在程序调用时就直接返回创建好的实例对象。
3.1.1 静态成员变量
public class Singleton {
// 类加载时就会在内存中创建实例对象,如果不被调用,会造成内存浪费
private static final Singleton SINGLETON = new Singleton();
// 私有化构造函数,防止外界创建实例对象
private Singleton() {
}
// 对外提供获取唯一实例对象的方法
public Singleton getInstance() {
return SINGLETON;
}
}
通过静态成员变量实现单例模式,可能造成内存浪费,但不存多线程并发访问问题。
3.1.2 静态代码块
public class Singleton {
// 声明实例对象
private static Singleton singleton;
// 类加载时执行静态代码块,初始化实例对象
static {
singleton = new Singleton();
}
// 私有化构造函数,防止外界创建实例对象
private Singleton() {
}
// 对外提供获取唯一实例对象的方法
public Singleton getInstance() {
return singleton;
}
}
通过静态代码块实现单例模式,可能造成内存浪费,但不存多线程并发访问问题。
3.2 懒汉式
懒汉式:在需要使用实例对象时才创建实例对象。
3.2.1 线程不安全
public class Singleton {
// 声明实例对象
private static Singleton singleton;
// 私有化构造函数,防止外界创建实例对象
private Singleton() {
}
// 对外提供获取唯一实例对象的方法
public Singleton getInstance() {
// 为null时才创建实例对象;不为null时直接返回已创建好的实例对象
if (singleton == null) {
singleton = new Singleton();
}
return singleton;
}
}
通过普通懒汉式实现单例模式,虽然解决浪费内存问题,但是引入了多线程并发访问问题,多线程场景下无法保证只创建唯一实例对象。
3.2.2 线程安全
public class Singleton {
// 声明实例对象
private static Singleton singleton;
// 私有化构造函数,防止外界创建实例对象
private Singleton() {
}
// 对外提供获取唯一实例对象的方法
public Singleton getInstance() {
// 添加锁,保证线程安全
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
return singleton;
}
}
上面代码虽然通过代码块锁保证了线程安全,但是存在性能问题,因为每次获取实例对象时都需要获取锁,并发访问时性能差。
3.2.3 双重校验加锁
在代码块锁外层添加判断,如果已创建好实例对象,就无需获取锁,而是直接返回实例对象
public class Singleton {
// 声明实例对象
private static Singleton singleton;
// 私有化构造函数,防止外界创建实例对象
private Singleton() {
}
// 对外提供获取唯一实例对象的方法
public Singleton getInstance() {
// 添加判断,实例对象已创建就无须获取锁,提高多线程并发访问性能
if (singleton == null) {
// 添加锁,保证线程安全
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
3.2.4 volatile防止指令重排
指令重排:JVM在保证最终结果正确的情况下,可以不按照程序编码的顺序执行语句,尽可能提高程序的性能。
创建一个对象,在JVM中会经过三步(以singleton对象为例):
1. 为singleton分配内存空间;
2. 初始化singleton对象;
3. 将singleton指向分配好的内存空间;
在上述步骤中,2和3可能会发生指令重排现象,导致创建对象的顺序变为1、3、2,当多个线程获取singleton对象时,线程A先执行了1、3步骤,然后线程B判断singleton已经不为空,获取到未初始化的singleton对象,就会报NPE异常。所以在3.3.2双重校验加锁的基础上,还需添加volatite关键字防止指令重排,代码如下:
public class Singleton {
// 声明实例对象; volatile 防止指令重排
private static volatile Singleton singleton;
// 私有化构造函数,防止外界创建实例对象
private Singleton() {
}
// 对外提供获取唯一实例对象的方法
public Singleton getInstance() {
// 添加判断,实例对象已创建就无须获取锁,提高多线程并发访问性能
if (singleton == null) {
// 添加锁,保证线程安全
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
3.2.5 静态内部类
public class Singleton {
// 私有化构造函数,防止外界创建实例对象
private Singleton() {
}
// 外部类加载时,不会加载静态内部类
private class SingletonHolder {
// 外部类实例对象由静态内部类创建
private static final Singleton SINGLETON = new Singleton();
}
// 对外提供获取唯一实例对象的方法
public Singleton getInstance() {
// 静态内部类被使用时才会被加载,从而创建静态成员变量
return SingletonHolder.SINGLETON;
}
}
通过静态内部类方式实现单例模式,在使用实例对象时才创建实例对象,避免浪费内存,而且也不会出现多线程并发访问问题。
3.3 破坏单例模式
3.1和3.2中所有单例模式实现方式,均可以被反射机制和序列化/反序列化破坏,让单例模式创建的实例对象不再唯一。以3.1.1静态成员变量实现单例模式为例,破坏单例模式代码如下所示:
public class Singleton implements Serializable {
// 类加载时就会在内存中创建实例对象,如果不被调用,会造成内存浪费
private static final Singleton SINGLETON = new Singleton();
// 私有化构造函数,防止外界创建实例对象
private Singleton() {
}
// 对外提供获取唯一实例对象的方法
public Singleton getInstance() {
return SINGLETON;
}
public static void main(String[] args) {
destroySingletonByReflect();
destroySingletonBySerialize();
}
// 通过反射破坏单例模式
private static void destroySingletonByReflect() {
try {
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
constructor.setAccessible(true);
Singleton singleton2 = constructor.newInstance();
System.out.println(singleton2); // com.zh.pattern.Singleton@16b98e56
System.out.println(SINGLETON); // com.zh.pattern.Singleton@7ef20235
} catch (Exception e) {
System.out.println("destroySingletonByReflect occur exception : " + e);
}
}
// 通过序列化/反序列化破坏单例模式
private static void destroySingletonBySerialize() {
try {
// 序列化
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SingletonFile"));
oos.writeObject(SINGLETON);
// 反序列化
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("SingletonFile"));
Singleton singleton3 = (Singleton) ois.readObject();
System.out.println(singleton3); // com.zh.pattern.Singleton@66cd51c3
System.out.println(SINGLETON); // com.zh.pattern.Singleton@7ef20235
} catch (Exception e) {
System.out.println("destroySingletonBySerialize occur exception : " + e);
}
}
}
3.4 Enum实现单例模式
(1)通过反射机制强制创建枚举对象时,Enum内部会校验枚举类型,防止反射创建多个对象。
(2)枚举对象序列化时会向文件写入枚举类型和枚举对象名称,反序列化时通过valueOf() 方法匹配枚举类型和枚举对象名称,找到内存中的唯一的实例对象,防止通过反序列化创建多个对象。
public enum Singleton {
// 枚举类Singleton被加载时,通过无参构造函数Singleton()创建实例对象SINGLETON
SINGLETON;
private Singleton() {
System.out.println("Constructor Singleton");
}
}
class Test {
public static void main(String[] args) {
Singleton singleton1 = Singleton.SINGLETON;
Singleton singleton2 = Singleton.SINGLETON;
System.out.println(singleton1 == singleton2);
// Constructor Singleton
// true
}
}
Enum实现单例模式,既是线程安全的,又可避免反射机制和序列化/反序列化破坏。