文章目录
设计模式之单例模式
1.什么是单例模式?
单例模式,是一种常用的软件设计模式。属于创建型模式的一种,在它的核心结构中只包含一个被称为单例的特殊类。通过单例模式可以保证系统中,应用该模式的一个类只有一个实例。即一个类只有一个对象实例。
数学与逻辑学中,Singleton定义为“有且仅有一个元素的集合”。单例模式最初的定义出现于《设计模式》(艾迪生维斯理, 1994):“保证一个类仅有一个实例,并提供一个访问它的全局访问点。”
Java中单例模式定义:“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”
2.为什么用单例模式?
单例模式听名字都大致明白,那就是单个的意思,既然是单个,哪说明不能存在多个,对于java来讲,一个类对象如果频繁用到,但是创建的类全部都是一模一样,哪么在堆里边占用的空间资源是一个很大的问题,说严重点,一直频繁的创建同一个对象,虽然用起来挺爽,但是万一把堆占满了,岂不是要一直OOM了,既然每次创建的类都一样,哪为什么不创建一个来复用呢,资源省下来干点其他的,岂不是更完美,这个就是单例想表达的意义和用途了,在那些需要单一任务的业务中(比如ServletContext,ApplicationContext),来保证实例的全局唯一性,来达到功能服用减少冗余设计的目的,是一个很好的优化策略,所有的设计模式,其实都是优化项目代码的一种手段,单例自然也不例外.
3.单例模式怎么用?
既然知道了单例其实就是想达到复用的目的,用来优化程序和设计,那么一个设计有好处自然也有坏处,万事万物都逃不开因果循环嘛,所以单例虽然好用,但是怎么用是一个很有讲究的话题,咱们就分析一下常见的一些单例实现方式,咱这里只从实际使用的角度出发,那些线程不安全的就不展开论述了,因为用了也没啥意义,也不建议那么用
3.1 饿汉式单例
/**
* @description: 饿汉式单例1
* @author: virtiL
*/
public class HungrySingleton {
private static final HungrySingleton INSTANCE = new HungrySingleton();
private HungrySingleton(){
//防止反射破坏
if(INSTANCE != null){
throw new RuntimeException("Multiple instances are not allowed");
}
}
public static HungrySingleton getInstance(){
return INSTANCE;
}
//防止反序列化破坏
private Object readResolve() {
return INSTANCE;
}
}
/**
* @description: 饿汉式单例2
* @author: virtiL
*/
public class HungrySingleton {
private static final HungrySingleton INSTANCE;
static {
INSTANCE = new HungrySingleton();
}
private HungrySingleton() {
if(INSTANCE != null){
throw new RuntimeException("Multiple instances are not allowed");
}
}
public static HungrySingleton getInstance() {
return INSTANCE;
}
//防止反序列化破坏
private Object readResolve() {
return INSTANCE;
}
}
说明:
饿汉式单例其实就是在类加载的过程中就将类进行初始化了,就像大家都说的饿汉嘛,比较着急,饿死鬼投胎嘛,看到吃的还不得疯抢,在JVM的类加载过程中,因为饿汉式单例是静态的,所以在加载时就已经初始化到内存中了,所以这个时候多线程是啥都还不知道呢它就已经实例化好并且存在了,而且还是static final的,所以自然而然的就不存在线程安全问题,JVM已经替我们规避了这个问题.
优点:
执行效率高,没有锁来消耗性能,依靠JVM的机制来保证线程安全
缺点:
类加载的时候就实例化,不管后边用到了还是没用到,都浪费了内存
3.2 懒汉式单例
/**
* @description: 懒汉式单例-双重校验锁
* @author: virtiL
*/
public class LazySimpleSingleton {
//用volatile来保证线程可见性和指令重排序
private static volatile LazySimpleSingleton INSTANCE = null;
private LazySimpleSingleton() {
//防止反射破坏
if(INSTANCE != null){
throw new RuntimeException("Multiple instances are not allowed");
}
}
//双重校验锁
public static LazySimpleSingleton getInstance() {
//此处N个线程都可以同时进来
if (INSTANCE == null) {
//此处N个线程都可以同时进来,但是这里加了锁,串行化执行
synchronized (LazySimpleSingleton.class) {
//虽然保证了同步,从不能每次进来都new吧,所以还是要再检查一次的
if (INSTANCE == null) {
INSTANCE = new LazySimpleSingleton();
}
}
}
return INSTANCE;
}
//防止反序列化破坏
private Object readResolve() {
return INSTANCE;
}
}
/**
* @description: 懒汉式单例-匿名内部类
* @author: virtiL
*/
public class LazySimpleSingleton {
private LazySimpleSingleton() {
//防止反射破坏
if(AnonymousInnerClass.INSTANCE != null){
throw new RuntimeException("Multiple instances are not allowed");
}
}
//保证空间共享和方法不被重载
public static final LazySimpleSingleton getInstance() {
//返回结果之前会加载内部类
return AnonymousInnerClass.INSTANCE;
}
//默认是不加载的,当使用LazySimpleSingleton的时候才会加载初始化内部类,也就是达成了延迟加载的目的
private static class AnonymousInnerClass{
private static final LazySimpleSingleton INSTANCE = new LazySimpleSingleton();
}
//防止反序列化破坏
private Object readResolve() {
return AnonymousInnerClass.INSTANCE;
}
}
说明:
懒汉式相当于对饿汉式做的一个优化,在饿汉式的基础上,来解决资源浪费的问题,懒汉懒汉,自然就是懒嘛,所以双重校验锁诞生了,这种方式开始的时候不实例化,等用的时候才实例化,也就是在get的时候我们才实例化,这样不用的时候就可以避免不必要的资源浪费了,但是这里也加上了锁,自然而然的也就带来了一些性能上的下降,因为大量的并发会导致锁等待,线程出现阻塞,然后就想到了更好的方式,那就是借用类初始化的辅助,实现了静态内部类的方式,这种方式即抛弃了锁同时又优化了饿汉式资源浪费的问题,同时内部类也是静态的成员变量,当调用get的时候内部类才会加载,也保证了线程安全,算是上边几种的最优解.
优点:
双重校验锁: 线程安全,减少了资源的浪费,锁细化,保证性能下降最小化
匿名内部类: 线程安全,减少了资源的浪费,执行效率高,没有锁来消耗性能,依靠JVM的机制来保证线程安全
缺点:
双重校验锁: 加入了锁,带来了性能上的问题
3.3 注册式单例
/**
* @Description: 枚举式单例(最完美的线程安全单例写法)
* @Author: virtiL
*/
public enum EnumSingleton {
INSTANCE;
private Object data;
public static EnumSingleton getInstance(){
return INSTANCE;
}
public Object getData() {
return data;
}
//注册
public void setData(Object data) {
this.data = data;
}
}
/**
* @Description: 注册式容器单例
* @Author: virtiL
*/
public class RegisterContainerSingleton {
//容器
private static Map<String, Object> ioc = new ConcurrentHashMap<>();
private RegisterContainerSingleton() {
}
public static Object getBean(String className) {
if (!ioc.containsKey(className)) {
synchronized (ioc) {
Object obj = null;
try {
// obj = Class.forName(className).newInstance();//jdk9之后就过时了
obj = Class.forName(className).getDeclaredConstructor().newInstance();
ioc.put(className, obj);
} catch (InvocationTargetException | NoSuchMethodException | ClassNotFoundException | InstantiationException | IllegalAccessException e) {
e.printStackTrace();
}
return obj;
}
} else {
return ioc.get(className);
}
}
// public static Object getBean(String className) {
// synchronized (ioc) {
// if (!ioc.containsKey(className)) {
// Object obj = null;
// try {
obj = Class.forName(className).newInstance();//jdk9之后就过时了
// obj = Class.forName(className).getDeclaredConstructor().newInstance();
// ioc.put(className, obj);
// } catch (InvocationTargetException | NoSuchMethodException | ClassNotFoundException | InstantiationException | IllegalAccessException e) {
// e.printStackTrace();
// }
// return obj;
// } else {
// return ioc.get(className);
// }
// }
// }
}
说明:
先来说说怎么理解注册式单例,其实你可以把它想象成一个key-value散列表,用key来保证唯一性,比如说IOC容器,HashMap,ThreadLocal等等,但是在这些容器中单例的体现也仅限于容器内部,而并非使用的时候,所以要想保证线程安全,我们还是需要保证使用容器的方法是线程安全的才可,比如Spring的getBean方法.
优点:
枚举: jvm去保证线程安全和攻击,没有锁,性能高效,写法简单优雅
容器: 容器本身线程安全,适合批量创建单例对象,管理实例便捷
缺点:
枚举: 黑盒,内部实现只能反编译去看,其实原理还是饿汉式的方式
容器: 容器本身线程安全,但是使用容器还是非线程安全的
3.4 破坏单例
单例模式相信大家已经听过很多次了,但是破坏单例的方式方法估计就没有那么普遍了,这里我就说一下单例怎么被破坏以及怎么防止被破坏的方法,破坏单例也有很多方式,这里说说常见的三种,一种是通过反射调用构造方法的方式去破坏,另外一种就是通过反序列化的方式去破坏.还有一种就是通过深克隆的方式
反射的方式
这里例子很明显,通过反射newInstance()出来的两个对象明显不是同一个,所以怎么解决这个问题呢,也很简单,既然知道是通过构造方法实现的,那我们就在构造方法中加点逻辑阻止它即可,也就是在上边例子中无参构造中加入的if判断,在调用构造方法之前闲判断是否存在过,存在了就不让它继续创建即可.
加了这个以后再跑一次,结果就是下边这样了
反序列化的方式
这个例子也很明显了,出来了两个实例,单例也被破坏了,现在是不是有点后背发凉的感觉,是不是有种平时一直在写bug的感觉,但是为什么会出现两个呢?
大家肯定也很疑惑,那咱就去翻翻源码看一看,到底是因为什么原因,导致出现了两个对象实例,第一个instance自然就不用说了,这个是我们自己创建的,所以咱们主要研究一下instance2是怎么出来的,整个例子用唯一用到的也就是readObject()方法了,那自然就是它搞出来的事情,具体的源码咱们就不跟了,那算是序列化和反序列化的问题了,脱离了咱们说设计模式的路线了,有兴趣的可以打个断点自己进去调试一下,凡事还得多动手嘛,重点其实就是readObject0方法,大致逻辑就是readObject0方法会先读取二进制内容对象有没有构造方法,如果有无参构造就会进行实例化,然后这个逻辑之后还有一层逻辑就是hasReadResolveMethod()方法,这个方法的作用就是判断有没有readResolveMethod,如果这个属性不是空,那就调用invokeReadResolve(),然后去调用对象中被重写的readResolve()方法,那这个就很明显了,我们可以重写这个方法在里边搞点事情,这样就不能出来两个了,所以重写readResolve()方法后再试一次,结果就是这样了
可能你还有个疑问,那就是readResolveMethod为啥就是true了,它是什么时候赋值的,赋值嘛,搜一下readResolveMethod =找找看不就知道了,然后会发现这个方法
readResolveMethod = getInheritableMethod(cl, "readResolve", null, Object.class);
然后进去getInheritableMethod看一下就会发现,其实就是通过反射获取了一下对象是不是重写过readResolve()方法,如果重写过,那就是true了,到此,反序列化被攻击怎么防御就解释通了
深克隆的方式
估计大家都知道克隆对象有浅克隆和深克隆,浅克隆其实就是复制了一份引用,深克隆就是完全复制一份内存空间,这个时候会产生新的引用地址,这样自然也就破坏了单例模式了,所以如果我想避免这个问题,只需要重写对象的clone()方法即可,让clone()方法返回当前对象,这样就可以避免破坏单例了
天生的宠儿枚举
枚举的单例是不是也要重写readResolve和加if逻辑呢,答案就是:NO ,所以说枚举是一个天生的宠儿,人家才是jvm的亲儿子啊,其他都是领养的,JVM对枚举做了特殊的语法处理和规范,还是沿用上边找源码的方式,有兴趣的可以去看看枚举是怎么处理的,而且还要jad反编译一下枚举的类,看看真实的枚举到底长什么样子,那就完全明白了,为什么枚举天生就可以阻止反射和序列化的破坏,咱这里不展开论述了,内容有点多,话题也不小,以后有机会另写一篇文章来说说枚举,这里就简单说一下,枚举其实用jad反编译之后原理和饿汉式的原理是类似的,jad下载地址: https://varaneckas.com/jad/ 下载之后把jad.exe放到你jdk的bin目录下边就行了,然后执行一下jad就可以显示安装成功了,跟执行java,javac类似,反编译(jad xxx.class即可,最后生成一个jad的文件)后的代码如下:
public final class EnumSingleton extends Enum
{
public static EnumSingleton[] values()
{
return (EnumSingleton[])$VALUES.clone();
}
//看这里
public static EnumSingleton valueOf(String name)
{
//两个参数确定唯一,所以说压根就不能通过无参构造去创建实例
return (EnumSingleton)Enum.valueOf(singleton/demo/EnumSingleton, name);
}
private EnumSingleton(String s, int i)
{
super(s, i);
}
public static EnumSingleton getInstance()
{
return INSTANCE;
}
public Object getData()
{
return data;
}
public void setData(Object data)
{
this.data = data;
}
public static final EnumSingleton INSTANCE;
private Object data;
private static final EnumSingleton $VALUES[];
static
{
//饿汉式
INSTANCE = new EnumSingleton("INSTANCE", 0);
$VALUES = (new EnumSingleton[] {
INSTANCE
});
}
}
代码很明显,调用无参构造函数压根就没有,再说说反射,看这个
要不说亲儿子,发现是枚举,直接抛异常,根本不给你机会.
4.总结
单例的写法可能还有很多种,我这里也只是说了说我们用的比较频繁的方式,而且也比较多见,这篇单例也纯属于个人见解,毕竟每个人对待设计模式的理解都不太相同,毕竟是一种很灵活的设计思想,所以如果文中有错误的地方欢迎指正,大家一起进步,好了,归根结底,说了那么多,最后得到一个结论,那就是如果想用单例,最稳妥的方式还是枚举或者匿名内部类的方式,但是也并不是所有的单例都要求做到那么严格,具体还是要依赖于我们的业务,总之都是一个目的,以最小的代价换取最大的作用嘛,希望大家也可以从小内容去勘破大格局.