单例模式是设计模式中最常见的,也是最简单的一种,所谓单例,是需要在任何时候只存在一个对象实例,故显然需要私有化构造器,构造器私有了,要想获得这个实例,故必须在类内部创建对象实例,同时必须提供静态方法来获取,静态方法只能操作静态属性,故内部对象实例需要被static修饰,由于单例,可用final修饰;
单例存在多种写法,有各自不同的特点,下面介绍常用的写法,并且这些写法有些存在漏洞,如发射、发序列化可以破坏该单例;
一、单例模式的五种写法
1、饿汉式
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){
}
public static Singleton getInstance(){
return instance;
}
}
饿汉式在类加载后,直接创建了对象,从文章开头的解释出发,可以理解为什么用private、static这些关键字
2、懒汉式
class Singleton2{
private static Singleton2 instance;
private Singleton2(){}
public static Singleton2 getInstance(){
if(instance==null){ //A位置
instance = new Singleton2();
}
return instance;
}
}
懒汉式具有延迟加载的特点,即在需要用该对象的时候才会创建实例,在一定程度上可以节约点资源;
懒汉式vs饿汉式:饿汉式在类加载后直接创建对象(即使不需要使用对象),所以线程安全
懒汉式在需要使用时才会创建对象,非线程安全
工程中:建议使用饿汉式
懒汉式的问题:并发访问时,T1执行到A处暂停,T2同样执行到A处,并继续往下执行,
T2实例化了instance,T2执行完,T1线程继续执行,此时T1线程会继续执行instance = new Singleton2();
无法保证单例
3、单例双检锁模式
class Singleton3{
private static volatile Singleton3 instance; //注意此处volatile关键字
private Singleton3(){}
public static /*synchronized*/ Singleton3 getInstance(){
if(instance==null){ //A位置
synchronized (Singleton3.class) {
if(instance==null){
instance=new Singleton3();// B位置
}
}
}
return instance;
}
}
不加volatile关键字的双检锁模式,解决了懒汉式的线程安全问题,但它带来了新的问题
双检锁模式---存在问题(与Java内存模型有关)
理论上时很完美的,但是实际会因Java内存模型,设计指令重排序,出现问题
/**
*
* 一、好处:避免在函数上使用synchronized关键字,导致每次调getIstance()函数都要
* 读锁的开销,提高效率
* 二、潜在问题:
* instance=new Singleton3();分为3步
* 1)申请空间
* 2)初始化空间的值
* 3)将引用instance指向该空间
* 分析:实际应该让3步按照顺序来,但由于Java内存模型,允许他们不按顺序执行,试想:
* T1执行B处时,初始化时按照1)->3)->2)的顺序,刚好执行完3)就被中断了,
* 此时,T2执行到A处,判断instance==null发现instance不为null,于是
* 将该对象返回,而该对象并未被初始化,这就导致了问题
* 三、解决之道:单例类的成员用volatile关键字修饰,内部原理参考另一篇博客
4、静态内部类方式
class Singleton4{
private Singleton4(){}
private static class Singleton4Holder{
private static Singleton4 instance = new Singleton4();
}
public static Singleton4 getInstance(){
return Singleton4Holder.instance;
}
}
具有延迟加载特性,同时也是线程安全的,是比较推荐的写法
1.Singleton4类被加载的时候,并不会实例化instance对象
2.只有在调用getInstance()函数的时候,才开始加载Singleton4Holder类,并创建instance实例
5、枚举
enum Singleton5{
INSTANCE;
public void dosomething(){}
}
使用枚举方法的好处在于:
1.枚举天生就是线程安全的,其在任意情况下都是单例
2.枚举具有防止反射和发序列化的特点
二、单例模式防止反射和反序列化
1、防止发射,我们知道,可以通过发射方式来获取类的构造方法,并用纸创建对象,即便构造方法为private修饰的,为了防止发射的漏洞,只需在构造函数内部做个判断,如下:
private Singleton(){
if(null!=instance){
throw new RuntimeException("单例已经存在");
}
}
2、防止反序列化
反序列化:即强对象写入磁盘再读入内存,得到一个新的实例,破坏了了单例的唯一性
Java提供了readResolve()方法,可以让开发者控制对象的反序列化
解决反序列化方法:在单例类中加入方法
本质:无论是实现Serializable接口,或是Externalizable接口,当从I/O流中读取对象时,readResolve()方法都会被调用到。
实际上就是用readResolve()中返回的对象直接替换在反序列化过程中创建的对象。
private Object readResolve() throws ObjectStreamException {
return instance;
}
3、以饿汉式为例,设计防止发射和反序列化漏洞的单例
class Singleton6 implements Serializable{
private static Singleton6 instance = new Singleton6();
//防止反射破坏单例
private Singleton6(){
if(null!=instance){
throw new RuntimeException("单例已经存在");
}
}
//防止反序列化破坏单例
private Object readResolve() throws ObjectStreamException {
return instance;
}
public static Singleton6 getInstance(){
return instance;
}
}
4、以静态内部类方式为例,设计防止反射和反序列化的单例
class Singleton7 implements Serializable{
//防止反射破坏单例模式
private Singleton7(){
if(null!=SingletonHolder.instance){
throw new RuntimeException("单例已存在");
}
}
//防止反序列化破坏单例模式
private Object readResolve() throws ObjectStreamException {
return SingletonHolder.instance;
}
private static class SingletonHolder{
private static Singleton7 instance = new Singleton7();
}
public static Singleton7 getInstance(){
return SingletonHolder.instance;
}
}
总结:实际中,需要根据需要,选择合适的单例类型,从上面可以看出,一个单例涉及的知识点还是挺多的,如volatile关键字的原理和作用、线程安全问题、synchronized关键字的锁的对象是谁、反射和反序列化的原理,如何预防、类加载机制等等。