前言:
在我们学习的过程中肯定会遇到很多问题,有时候当时找资料看的明白了,但是过一段时间对其模糊甚至忘记,那怎么办?我觉得就是整理下来,形成文章,无论是你参考其他人的还是自己手写的,都能为你以后随时翻看做下铺垫,还有就是在你整理的过程中更能发现问题加深印象及理解,不过这只是我的理解,还是希望大家找到自己快速积累知识的方式。 言归正传,下面我们一起来看下面试的时候经常遇到的单例模式到底是什么鬼。
一、什么是单例模式?
单例模式是设计模式中最简单的模式了,看前两个字单例估计会有些认知,其实单例模式就是在java中一个类只能有一个实例,通过单例模式我们可以保证系统中应用该模式的一个类中只有一个实例。
二、为什么要用单例模式?它带来了什么好处
由上面我们了解到了单例模式的概念了,那大家想一想,为什么会有这种设计模式呢,它的好处什么? 当然单例模式带来好处挺大的,我觉得GC应该感谢他,或者用户也应该感谢它,
-
单例模式保证了类的实例只有一个,在java中避免了一个类被实例化过多次,导致时间耗费过多,对于用户来时,时间就是生命,而单例模式节省了类创建多次实例的时间,所以用户是不是应该感激它~
-
单例模式由于一个类只有一个实例,节省了内存空间,因为它限制了实例的个数,所以有助于垃圾回收,给垃圾回收节省了工作量
三、单例模式的使用场景
单例模式只允许创建一个对象,因此节省内存,加快对象访问速度,因此对象需要被公用的场合适合使用,如多个模块使用同一个数据源连接对象等等。如:
- 1.需要频繁实例化然后销毁的对象。
- 2.创建对象时耗时过多或者耗资源过多,但又经常用到的对象。
- 3.有状态的工具类对象
- 4.频繁访问数据库或文件的对象。
以下都是单例模式的经典使用场景:
- 资源共享的情况下,避免由于资源操作时导致的性能或损耗等。如上述中的日志文件,应用配置
- 控制资源的情况下,方便资源之间的互相通信。如线程池等。
四、单例模式实现方式?
经过上面我们大概了解到单例模式的概念,好处,及使用场景,然后我们再来看看重中之重,单例模式的实现方式
- 1、懒汉模式


- 2、饿汉模式

3、双重检测锁模式第一版

下面我们来解释下几个关键点:
首先我们将synchronized移到了方法内部,防止锁住整个对象,做到了更加精细化,提升了效率,然后看下两次判断
第一次判断大家都能理解,就是在进入synchronized临界区之前判断对象是否实例化,如果有的话直接返回
第二次判断是进入synchronized临界区以后的,因为当两个线程同时访问的时候,线程A构完对象的时候,线程B也已经通过了第一次判空验证,如果不做第二次判空的话,线程B还是会再次构建一次实例,像这样两次判空机制叫做双重检测机制。
但是这这种写法就是线程安全的嘛?不是,这里涉及到了JVM编译器的指令重排问题。 因为singletonDemo1=new SingletonDemo1()会被编译器生成如下JVM指令
memory=allocate();//1:分配对象的内存空间
ctorLnstance(memory);//2: 初始化对象
instance=memory;//3:设置instance指向刚分配的内存地址
但是这些指令经过JVMde CPU 优化,顺序可能会发生变化
有可能变成以下顺序 memory=allocate();//1:分配对象的内存空间
instance=memory;//3:设置instance指向刚分配的内存地址
ctorLnstance(memory);//2: 初始化对象
在这种顺序下 比方说当 线程1 执行完 1 3时,此时instance对象还未完成初始化,但已经不再指向null了,此时如果另外一个线程 2抢到了CPU资源,执行了 if(singletonDemo1==null)的结果会是false,然后走到 return singletonDemo1;这步返回了一个没有初始化的对象,从而报错
其实java也发现了这种情况,在jdk1.5之后有了volatile关键字,它可以防止指令重排序
所以如果用volatile修饰的话,那singletonDemo1=new SingletonDemo1()肯定会保证正常的顺序
即上面的 1 2 3 顺序

用volatile修饰后的双重检测写法如下

- 4、静态内部类

静态内部类实现的代理模式是线程安全,而且可以延迟加载,在加载SingletonDemo1类的时候不会加载
静态内部类,只有在第一次调用getInstance才会加载,相比于懒汉以及饿汉模式,静态内部类模式(一般也被称为 Holder)是许多人推荐的一种单例的实现方式,因为相比懒汉模式,它用更少的代码量达到了延迟加载的目的。
其实对于以上四种实现方式,也不是真正的安全的,因为可以通过反射和序列化破坏单例,我们来
看下通过反射是否能实现单例

看下序列化破坏单例
import java.io.*;
public class SingletonDemo1 implements Serializable {
private String content;
//静态内部类
private static class LazySingleton{
private static volatile SingletonDemo1 singletonDemo1=new SingletonDemo1();
}
//构造器私有化
private SingletonDemo1(){};
public static SingletonDemo1 getInstance(){
return LazySingleton.singletonDemo1;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) throws IOException, ClassNotFoundException {
SingletonDemo1 s = SingletonDemo1.getInstance();
s.setContent("单例序列化");
System.out.println("序列化前读取其中的内容:"+s.getContent());
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));
oos.writeObject(s);
oos.flush();
oos.close();
FileInputStream fis = new FileInputStream("SerSingleton.obj");
ObjectInputStream ois = new ObjectInputStream(fis);
SingletonDemo1 s1 = (SingletonDemo1)ois.readObject();
ois.close();
System.out.println(s+"\n"+s1);
System.out.println("序列化后读取其中的内容:"+s1.getContent());
System.out.println("序列化前后两个是否同一个:"+(s==s1));
}
}
看下结果,反射也同样破坏了单例

通过上面的测试我们可以看到用反射得到对象相比较是不一致的,不过可以写一些代码去防止反射攻击
这个是我找的一个链接有兴趣的可以看看
5、枚举实现单例
public enum EnumSingleton {
INSTANCE;
public EnumSingleton getInstance(){
return INSTANCE;
}
}
是不是觉得很简单,极少的代码量,不过编译后相当于,代码量也是挺多的
public final class EnumSingleton extends Enum< EnumSingleton> {
public static final EnumSingleton ENUMSINGLETON;
public static EnumSingleton[] values();
public static EnumSingleton valueOf(String s);
static {};
}
然后我们可以自己验证下通过枚举,反射和序列化是否可以破坏,大家自己验证哈,下面直接给出结论
如果枚举实现单例模式没通过反射时会报异常
反射在通过newInstance创建对象时,会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。
而序列化如果是枚举类型实现的单例模式,序列化的对象也是同一个,大家可以自己验证下
四、总结
通过上面几种方式的比较,我们会发现枚举是实现单例模式的最好方式,代码量少,写法简单可以避免 反射和序列化带来的问题
题外话:
整理文章其实挺累的,但是在整理的过程中真的能学到很多,痛苦并快乐着吧,希望能给大家带来帮助,谢谢~