vlambda博客
学习文章列表

搞清楚单例模式,其实很简单


Java中单例定义

“一个类有且仅有一个实例,并且自行实例化向整个系统提供。”


实现单例的特点

1. 构造方法定义成私有方法防止通过构造方法进行实例化对象

2. 提供一个统一的静态方法,调用时只能通过静态方法获取实例对象



常见的单例模式

1. 饿汉式

2. 懒汉式

3. 静态内部类

4. 枚举


饿汉式


我们已经提供了私有的构造方法和静态方法,定义了一个属性叫instance,并且指向new Singleton()。

不管能不能用得到,总之我们先把对象实例化。称之为饿汉式。


懒汉式

搞清楚单例模式,其实很简单


私有的构造方法不变,将instance指向null,在静态方法中,增加判断。

如果instance为空,进行实例化操作,否则,直接返回实例。这种方式实现了延迟加载,在被调用时,进行实例化,称为懒汉式。


弊端:多线程环境下,无法保证唯一实例。


模拟多线程环境进行验证: 我们在main方法中循环20次每次都新建一个线程调用getInstance方法,获取类的hashCode。


搞清楚单例模式,其实很简单


搞清楚单例模式,其实很简单


可以看到,返回对象并不是同一个对象。说明多线程环境下此种方式不可用。



对上面的代码进行改造,在方法中加入synchronized同步关键字,确保同一时间只有一个线程进行访问


搞清楚单例模式,其实很简单


搞清楚单例模式,其实很简单


加上synchronized后,返回结果已经是同一个对象。


但是:在方法上加锁,粒度比较粗,真正想实现同步执行的,仅仅是实例化对象的这段代码。优化代码:


搞清楚单例模式,其实很简单


搞清楚单例模式,其实很简单


测试发现,返回的并不是同一个对象


分析:当线程一执行if判断后,拿到锁进行创建实例(还没有实例化完成),这时候线程二也执行if判断,发现实例为空,进入等待,线程一完成实例化后,释放锁,线程二拿到锁,又进行了一遍实例化。导致创建了多个实例。继续优化:


搞清楚单例模式,其实很简单


在synchronized里面,在加一层if判断,当线程二拿到锁之后,重新判断,如果instance还是为空,进行实例化操作。这种也被称为是DCL模式(double check lock双重校验)


但是:JVM是会进行指令重排的,一旦出现指令重排序可能就会出现空指针异常。大家可能都会想到volatile关键字,它的第二个特性,禁止指令重排序。所以在属性前加上volatile,禁止JVM进行指令重排。(关于JVM指令重排序和volatile禁止重排的底层实现,后面在给大家分享)


搞清楚单例模式,其实很简单


弊端:引入了synchronized后,时耗增大。为了延迟加载,增加同步关键字,反而降低了系统性能。


提问:那么有没有一种不加同步关键字的方式呢?


静态内部类

搞清楚单例模式,其实很简单


当类被加载的时候,内部类并不会被初始化,调用getInstance()方法时,才会被加载,然后进行实例化。并且,实例的创建是在类加载时完成的,所以并不需要使用同步关键字。



两种极端情况:
1. 通过序列化和反序列化破坏单例。

搞清楚单例模式,其实很简单

搞清楚单例模式,其实很简单

我们本地实现了一个序列化和反序列化的过程,比较之后,发现前后两个对象已经不是同一个对象。说明:序列化和反序列化可能会破坏单例。

解决方法:重写readResolve()方法

搞清楚单例模式,其实很简单

搞清楚单例模式,其实很简单

2. 通过反射机制强行调用私有构造方法进行实例化。


搞清楚单例模式,其实很简单


搞清楚单例模式,其实很简单


经过比较也验证了反射会破坏序列化

解决方法:通常我们需要在私有构造方法中加入判断,如果实例已经存在,则抛出异常,不能在进行新的实例化操作

搞清楚单例模式,其实很简单


有没有一种方案,能完美解决上述的所有问题?


枚举


搞清楚单例模式,其实很简单


枚举类是线程安全的,不需要使用同步代码块来实现,更能保证不被反射和反序列化攻击。


尝试用反射破坏枚举单例


搞清楚单例模式,其实很简单


搞清楚单例模式,其实很简单


因为Java的枚举类都隐式的继承自Enum抽象类,Enum抽象类本身并没有无参的构造方法。只有一个Enum(String name, int ordinal)的构造方法。所以会报NoSuchMethodException,我们在来看一下Constructor的newInstance()方法的源码


搞清楚单例模式,其实很简单


可以看到如果类是ENUM修饰,则抛出异常,所以JDK反射机制从内部就已经禁止了用反射创建枚举实例


尝试用序列化反序列化破坏枚举单例


搞清楚单例模式,其实很简单


搞清楚单例模式,其实很简单


结果:序列化和反序列化是同一个对象


看下源码,在ObjectInputStream类中会判断如果是枚举类型的调用readEnum()方法



readEnum()方法中,通过描述符获取枚举单例的类型,在获取单例的名字,然后在调用Enum.valueOf()方法,根据类型和名字直接获取到单例对象



总结:枚举是目前比较好的一种实现单例的方式