Singleton
知识点:
- 模式定义、应用场景、类图分析
- 字节码知识、字节码指令重排序
- 类加载机制
- JVM序列化机制
- 单例模式在Spring框架 & JDK源码中的应用
1. 模式定义
保证一个类只有一个实例,并且提供一个全局访问点
2. 场景
重量级的对象,不需要多个实例(如线程池,数据库连接池)
3. 类图
4. 实现方式
(1)懒汉模式
(1.1)基本概念
- 延迟加载,只有在真正使用的时候,才开始实例化
- 线程安全
- double check、加锁优化
synchronized - 编译器(JIT),CPU有可能对指令进行重排序,导致使用到尚未初始化的实例,可以通过添加
volatile关键字进行修饰,对于volatile修饰字段,可以防止指令重排。
(1.2)代码说明
- 创建单例模式
class LazySingleton {
private static LazySingleton instance;
/**
* 私有构造函数
* 避免直接从外部进行对应实例的创建
*/
private LazySingleton() {
}
/**
* 公有的全局访问点
* @return
*/
public static LazySingleton getInstance() {
if (instance == null) {
instance = new LazySingleton();
}
return instance;
}
}
测试:
LazySingleton instance1 = LazySingleton.getInstance();
LazySingleton instance2 = LazySingleton.getInstance();
System.out.println(instance1 == instance2);
返回结果为
true,说明两个对象使用同一个实例
- 多线程引发的问题 以上实现在单线程上没有问题,但是当情景放在多线程的时候,会引发问题。
测试:
new Thread(()-> {
LazySingleton instance = LazySingleton.getInstance();
System.out.println(instance);
}).start();
new Thread(()-> {
LazySingleton instance = LazySingleton.getInstance();
System.out.println(instance);
}).start();
// 为了体现效果,修改全局访问点,然它延迟一会
public static LazySingleton getInstance() {
if (instance == null) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new LazySingleton();
}
return instance;
}
返回结果:
com.nick.lazysingleton.LazySingleton@90472a2 com.nick.lazysingleton.LazySingleton@1e057600 可以看出在多线程情况下单例会失败
- 多线程单例问题解决方案
使用
synchronized进行修饰,引入锁可以避免以上问题。
public synchronized static LazySingleton getInstance() {
if (instance == null) {
try {
Thread.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
instance = new LazySingleton();
}
return instance;
}
测试结果:
com.nick.lazysingleton.LazySingleton@4388eabf com.nick.lazysingleton.LazySingleton@4388eabf
- 进一步方案优化 但是以上方案并不是最完美的,由于对全局访问点进行了锁,粒度有点大,在高并发的时候可能会存在问题。因此,想到的进一步优化方案是降低锁粒度。
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
instance = new LazySingleton();
}
}
return instance;
}
将synchronized放到new对象的时候,但是在高并发的时候同样会有问题:
为了解决这个问题,需要进行double check:
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
// double check
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
- 字节码层面优化
new一个对象,在底层会有几步指令操作,例如有一个Demo类,当该类new对象时,通过反汇编查看指令如下:步骤可以简单归结为:
-
- 分配空间
-
- 初始化
-
- 引用赋值
其中步骤1必须先执行,而步骤2、3并没有先后顺序的要求,因此JIT(即时编译)或者CPU可能会对指令进行优化,将指令进行重排序。
- 引用赋值
其中步骤1必须先执行,而步骤2、3并没有先后顺序的要求,因此JIT(即时编译)或者CPU可能会对指令进行优化,将指令进行重排序。
为了解决此问题,java会有一个修饰符volatile,被修饰的私有变量不会对引用对应空间进行指令排序。因此用该修饰符修饰instance即可。
完成代码如下:
class LazySingleton {
/**
* volatile修饰不会对引用对应的空间进行指令重排
*/
private volatile static LazySingleton instance;
/**
* 私有构造函数
* 避免直接从外部进行对应实例的创建
*/
private LazySingleton() {
}
/**
* 公有的全局访问点
* 在调用该方法的时候才对私有变量进行实例化
* 不添加synchronized的话在多线程会有问题
* @return
*/
public static LazySingleton getInstance() {
if (instance == null) {
synchronized (LazySingleton.class) {
// double check
if (instance == null) {
instance = new LazySingleton();
}
}
}
return instance;
}
}
(2)饿汉模式
- 类加载的初始化阶段就完成实例,本质上就是借助于JVM类加载机制,保证实例的唯一性。
- 类加载过程:
- 加载二进制数据到内存中,生成对应的Class数据结构
- 连接:(1)验证(2)准备,给类的静态成员变量赋默认值(3)解析
- 初始化:给类的静态变量赋初值
只有在真正使用对应的类时,才会触发初始化(如当前类的启动类main函数所在类,直接new操作,访问静态属性、访问静态方法、用反射访问类、初始化一个类的子类等)
/**
* 饿汉模式编写
* 基于JVM类加载机制,保证线程安全
*/
class HungrySingleton {
private static HungrySingleton instance = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return instance;
}
}
静态变量instance是在类加载的最后一步进行初始化(new对象),而类加载的时机是在真正使用对应类时。
(3)静态内部类
- 本质上是利用类的加载机制来保证线程安全
- 只有在实际使用的时候,才会触发初始化,所以也是懒加载的一种形式
/**
* 静态内部类是在具体调用getInstance方法并返回的时候才去加载
* 静态内部加载时会进行初始化,最终导致instance的初始化
* 依赖于JVM类加载机制保证线程安全
*/
class InnerClassSingleton {
private static class InnerClassHolder {
private static InnerClassSingleton instance = new InnerClassSingleton();
}
private InnerClassSingleton() {
}
public static InnerClassSingleton getInstance() {
return InnerClassHolder.instance;
}
}
(4)通过反射来破坏单例
在使用懒汉或者内部类时,在调用getInstance时才去进行类加载并初始化。可以通过反射来打破单例:
// 获取构造函数
Constructor<InnerClassSingleton> declaredConstructor = InnerClassSingleton.class.getDeclaredConstructor();
// 获取权限
declaredConstructor.setAccessible(true);
// 创建实例
InnerClassSingleton innerClassSingleton = declaredConstructor.newInstance();
// 通过getInstance再次创建实例
InnerClassSingleton instance = InnerClassSingleton.getInstance();
System.out.println(innerClassSingleton == instance);
为了避免这种情况,可以在实现的私有构造函数中进行判断:
private InnerClassSingleton() {
if (InnerClassHolder.instance != null) {
throw new RuntimeException("单例不允许多个实例");
}
}
(5)通过反序列化破坏单例
使用内部静态类作为例子,让该类继承自Serializable,然后该类就可以进行序列化。
class InnerClassSingleton implements Serializable {
// ...
}
通过getInstance方法获取一个实例,将其通过序列化保存到硬盘:
ObjectOutputStream oos = new ObjectOutputStream( new FileOutputStream("testSerializable"));
oos.writeObject(instance);
oos.close();
此时序列化后的instance就会保存至硬盘:
然后通过反序列化将去加载回内存,并强转为InnerClassSingleton:
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("testSerializable"));
InnerClassSingleton object = ((InnerClassSingleton) ois.readObject());
System.out.println(object == instance);
此时强转回来的实例object与之前创建的实例instance不相同。
因为通过反序列化的方式创建对象时,并不会调用自己定义的构造函数,而是直接从字节流中拿数据。
解决办法:在Serializable类中的注释里写的很清楚,为了避免这种情况,可以重写方法readResolve并返回实例。
Object readResolve() throws ObjectStreamException {
return InnerClassHolder.instance;
}
此时重新进行反序列化并判断,其结果为保存:
Exception in thread "main" java.io.InvalidClassException: com.nick.innerclasssingleton.InnerClassSingleton; local class incompatible: stream classdesc serialVersionUID = 2562363086033517702, local class serialVersionUID = 5509890476487421386
at java.io.ObjectStreamClass.initNonProxy(ObjectStreamClass.java:699)
at java.io.ObjectInputStream.readNonProxyDesc(ObjectInputStream.java:1885)
at java.io.ObjectInputStream.readClassDesc(ObjectInputStream.java:1751)
at java.io.ObjectInputStream.readOrdinaryObject(ObjectInputStream.java:2042)
at java.io.ObjectInputStream.readObject0(ObjectInputStream.java:1573)
at java.io.ObjectInputStream.readObject(ObjectInputStream.java:431)
at com.nick.innerclasssingleton.InnerClassSingletonTest.main(InnerClassSingletonTest.java:39)
提示版本号不同,是因为序列化时,会根据数据进行一个版本号的创建,存到序列化后的文件中;反序列化时,首先在JVM中根据class生成序列号,然后与文件中的序列号进行比对,如果一致则可以反序列化;不一致则认为class已经改过了。所以需要在内部静态类中添加版本号(具体方法在Serializable的接口注释中有,添加根本号后即便类成员变化,也是可以进行兼容)
static final long serialVersionUID = 42L;
此时重新进行序列化和反序列化,输出结果为true。
(6)枚举单例
public enum EmunSingleton {
INSTANCE;
public void print() {
System.out.println(this.hashCode());
}
}
创建一个枚举,然后通过反汇编可以看出,编译器会为枚举创建一个类com.nick.emunsingleton.EmunSingleton,该类继承自抽象类java.lang.Enum
测试:
EmunSingleton instance1 = EmunSingleton.INSTANCE;
EmunSingleton instance2 = EmunSingleton.INSTANCE;
System.out.println(instance1 == instance2);
结果为true,看这段代码的字节码可以看出:第一次调用EmunSingleton.INSTANCE时先去new一个com/nick/emunsingleton/EmunSingleton对象,然后保存到栈中,获取构造函数参数(一个String,一个int),然后调用构造函数。当第二次调用时,是从静态区获取对象而不是重新new一个对象。