1 基本内容
1.1 概念
单例模式,是指在任何时候,该类只能被实例化一次,在任何时候,访问该类的对象,对象都是同一个。只要是程序员都会使用到,甚至都不能算是设计模式。但是在我们使用中也需要了解一下单例特性和使用场景
1.2 模式优缺点
单例模式有以下优点:
使用单例模式可以严格的控制用户怎样以及如何访问它
节约系统资源,提高系统的性能
单例模式有以下缺点:
不易扩展
单例类职责过重,在一定程度上违背了“单一职责原则”
如实例化对象长时间未使用,会GC回收,导致对象状态的丢失
2 单例模式分类
2.1 饿汉模式
public class SingletonEHan {
private SingletonEHan() {}
/**
* 1.单例模式的饿汉式
*/
private static SingletonEHan singletonEHan = new SingletonEHan();
public static SingletonEHan getInstance() {
return singletonEHan;
}
// SingletonEHan instance= SingletonEHan.getInstance();
/**
* 2. 单例模式的饿汉式变换写法
* 基本没区别
*/
private static SingletonEHan singletonEHanTwo = null;
static {
singletonEHanTwo = new SingletonEHan();
}
public static SingletonEHan getSingletonEHan() {
if (singletonEHanTwo == null) {
singletonEHanTwo = new SingletonEHan();
}
return singletonEHanTwo;
}
// SingletonEHan instance= SingletonEHan.getSingletonEHan();
}
- 优点:在类加载的时候就完成了实例化,避免了线程的同步问题
- 缺点:由于在类加载的时候就实例化了,未懒加载,会造成内存的浪费
2.2 懒汉模式
2.2.1 DCL双重校验锁
public class Singleton {
private volatile static Singleton singleton;
public static Singleton getInstance() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
- 优点:线程安全,延迟加载
- 缺点:使用了synchronized关键字,需要同步获取,影响性能
2.2.2 静态内部类
public class SingletonIn implements Serializable {
private static final long serialVersionUID = -2424536714640756316L;
private SingletonIn() {
}
private static class SingletonInHolder {
private static final SingletonIn singletonIn = new SingletonIn();
}
public static SingletonIn getSingletonIn() {
return SingletonInHolder.singletonIn;
}
public static void main(String[] args) {
SingletonIn singleton = getSingletonIn();
}
}
- 优点:静态内部类随着方法调用而被加载,只加载一次,不存在并发问题,所以是线程安全的,延迟加载,效率高,推荐使用
2.3 枚举
public enum SingletonEnum {
INSTANCE(1,"i am singleton");
private int code;
private String message;
SingletonEnum(int code, String message) {
this.code = code;
this.message = message;
}
public final int getCode() {
return code;
}
public final String getMessage() {
return message;
}
}
大佬喜欢的用法,简单粗暴,其实有很多学问在里面,请看下文就会知道
3 克隆-序列化-反射对单例的影响
对象的创建方式有哪几种?
四种:new 、克隆、序列化、反射
上面的单例模式使用的都是new创建对象,那么其他三种方式对单例是否有影响呢?
3.1 克隆对单例的影响
实现Cloneable 接口,尽管构造函数是私有,但还会创建一个对象。因为clone方法不会调用构造函数,会直接从内存中copy内存区域。所以单例模式的类是不可以实现Cloneable接口
以静态内部类的单例方式做测试
public static void main(String[] args) throws CloneNotSupportedException {
SingletonIn singleton = getSingletonIn();
SingletonIn singleton1 = (SingletonIn) singleton.clone();
SingletonIn singleton2 = getSingletonIn();
System.out.println(singleton.hashCode());
System.out.println(singleton1.hashCode());
System.out.println(singleton2.hashCode());
}
测试结果
1173230247
856419764
1173230247
hash值不一样,所以克隆成功了,生成了一个新对象,所以当对象的使用要求单例的时候,切记不可实现cloneable接口
3.2 序列化对单例的影响
3.2.1 静态内部类单例反序列化
如下方式,我们通过序列化反序列化获取对象,观察对象是否还是单例对象
public static void main(String[] args) throws IOException, ClassNotFoundException {
SingletonIn singleton = getSingletonIn();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:\\single.obj"));
oos.writeObject(singleton);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:\\single.obj")));
SingletonIn singleton1 = (SingletonIn) ois.readObject();
System.out.println(singleton.hashCode());
System.out.println(singleton1.hashCode());
System.out.println(singleton == singleton1);
}
控制台打印结果如下:
312714112
692404036
false
hash值不一样,获取的对象非同一对象
3.2.2 枚举序列化反序列化
public static void main(String[] args) throws IOException, ClassNotFoundException {
SingletonEnum singleton = SingletonEnum.INSTANCE;
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:\\single.obj"));
oos.writeObject(singleton);
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("d:\\single.obj")));
SingletonEnum singleton1 = (SingletonEnum) ois.readObject();
System.out.println(singleton.hashCode());
System.out.println(singleton1.hashCode());
System.out.println(singleton == singleton1);
}
打印结果
2125039532
2125039532
true
枚举方式,即使序列化反序列化也不会破坏单例
3.2.3 防止序列化对单例的破坏
自定义实现对象的readResolve()方法可以解决问题
private Object readResolve() {
return getSingletonIn();
}
打印结果
312714112
312714112
true
这样反序列化获取到的就是原有的单例了
3.2.4 原理分析
debug反序列化方法readObject(),调用链如下:
主要分析这部分1767行开始 readOrdinaryObject 下面的源码
//获取SingletonIn的Class雷响
Class<?> cl = desc.forClass();
if (cl == String.class || cl == Class.class
|| cl == ObjectStreamClass.class) {
throw new InvalidClassException("invalid class descriptor");
}
Object obj;
try {//如果所代表的类是可序列化或者自定义序列化并且可以序列化runtime时候被实例化,则返回true
obj = desc.isInstantiable() ? desc.newInstance() : null;
//debug在这里通过反射创建了对象
} catch (Exception ex) {
//省略
}
passHandle = handles.assign(unshared ? unsharedMarker : obj);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(passHandle, resolveEx);
}
//是否实现了自定义序列化接口
if (desc.isExternalizable()) {
readExternalData((Externalizable) obj, desc);
} else {
//读取持久化的数据并写入对象
readSerialData(obj, desc);
}
handles.finish(passHandle);
if (obj != null & handles.lookupException(passHandle) == null &&
desc.hasReadResolveMethod())
//如果所代表对象实现了可序列化或者自定义序列化接口,并且定义了readResolve方法,则返回true
{
//获取readResolve方法中的对象,并替换obj
Object rep = desc.invokeReadResolve(obj);
if (unshared && rep.getClass().isArray()) {
rep = cloneArray(rep);
}
if (rep != obj) {
handles.setObject(passHandle, obj = rep);
}
}
//返回返序列化对象
return obj;
从上面源码可知,反序列化的时候,通过反射会创建新的对象,所以不是单例,如果类实现了serializable or externalizable 接口,定义readResolve方法,可以控制最终反序列对象,所以重写该方法,可以实现单例
而如果是枚举类型的话主要关注checkResolve(readEnum(unshared)) 方法,readEnum(unshared)返回的对象就是INSTANCE对象
//获取枚举类对象
private Enum<?> readEnum(boolean unshared) throws IOException {
//分配给给定对象的下一个可用句柄,并分配赋值处理,
int enumHandle = handles.assign(unshared ? unsharedMarker : null);
ClassNotFoundException resolveEx = desc.getResolveException();
if (resolveEx != null) {
handles.markException(enumHandle, resolveEx);
}
String name = readString(false);
Enum<?> result = null;
//获取枚举类Class
Class<?> cl = desc.forClass();
if (cl != null) {
try{
//方法返回具有指定名称的枚举类型的枚举常量,获取到INSTANCE对象
Enum<?> en = Enum.valueOf((Class)cl, name);
result = en;
} catch (IllegalArgumentException ex) {
// 省略
}
if (!unshared) {
handles.setObject(enumHandle, result);
}
}
handles.finish(enumHandle);
passHandle = enumHandle;
return result;
}
private Object checkResolve(Object obj) throws IOException {
//在这一步就返回对象了
if (!enableResolve || handles.lookupException(passHandle) != null) {
return obj;
}
Object rep = resolveObject(obj);
if (rep != obj) {
handles.setObject(passHandle, rep);
}
return rep;
}
魔法在java.lang.Enum.valueOf()中,最后获取的是常量类
3.3 反射对单例的影响
public static void main(String[] args)
throws ReflectiveOperationException {
Class cls = SingletonIn.class;
Constructor<SingletonIn> constructor = cls.getDeclaredConstructor();
constructor.setAccessible(true);
SingletonIn singleton = constructor.newInstance();
SingletonIn singleton1 = getSingletonIn();
System.out.println(singleton.hashCode());
System.out.println(singleton1.hashCode());
System.out.println(singleton == singleton1);
}
打印结果
1173230247
856419764
false
反射直接破坏单例,解决方法是在构造器使用同步块互斥
private static volatile boolean init = false;
private SingletonIn() {
synchronized (Singleton.class) {
if (init) {
throw new RuntimeException("只能单例获取");
}
init = !init;
}
}
在这种条件下,再次通过反射调用就会报错:
Exception in thread "main" java.lang.ExceptionInInitializerError
at designpattern1.singleton.inclass.SingletonIn.getSingletonIn(SingletonIn.java:43)
at designpattern1.singleton.inclass.SingletonIn.main(SingletonIn.java:57)
Caused by: java.lang.RuntimeException: 只能单例获取
4 总结
枚举类型是绝对单例的,可以无责任使用
其他需根据场景使用不同版本,在有序列化和反射的场景下,选择合适的安全版本
网友评论