玩转单例模式(先看前言)
前言
因为今年7月份要出去找校招工作,听在大厂的学长学姐们说,面试官关于设计模式会问,除了单例模式
和工厂模式
,你还会什么模式,嘿嘿。但是今天我先不搞其他设计模式,先把单例模式搞得明明白白的,再扯其他的模式,希望大家看完我写的这篇单例模式后能对单例模式有一个非常清楚的了解,所以希望大家耐心的看下去。所以这篇文章我会讲以下几点
- 单例模式几种创建方式
- 饿汉式
- 懒汉式
- DCL懒汉式(双重检测锁懒汉式)
- 静态内部类
- 枚举型
- 使用反射破坏我们手写的单例模式
- 再写出反射也不会破坏的单例模式
- 为什么enum(枚举)类型的单例模式不会被反射破坏
一、单例模式的几种创建方式(由简单到复杂)
一说到单例模式,最起码得知道两种方式吧,一是饿汉式 ,二是懒汉式
1. 先说说饿汉式(是最简单的单例模式的实现)
饿汉式就是当类加载的时候就实例化,并且创建单例对象
步骤:
- 创建一个类,类里面声明一个静态的类的变量并实例化它
- 写一个获取该实例的方法
上代码
public class Hungry {
private final static Hungry HUNGRY = new Hungry();
public static Hungry getInstance(){
return HUNGRY;
}
}
饿汉式的缺点:
饿汉式在类加载的时候就初始化,不管你是否使用,它都实例化了,所以会占据空间,浪费内存。
但如果我们为了节省内存空间,想要在什么时候需要这个单例对象什么时候实例化,应该怎么做呢,由此我们想出了懒汉式这种单例模式
2.谈谈最简单的懒汉式
为了解决不管你是否使用这个单例对象,它都实例化了,所以会占据空间,浪费内存的问题,我们就让这个单例对象只在第一次被使用的时候创建出来。
步骤:
- 创建一个类,类里面声明一个静态的类的变量
- 创建一个私有的构造器
- 写一个获取该实例的方法
上代码
/**
* 1.创建一个类,类里面声明一个静态的类的变量
*/
public class LazyMan {
public static LazyMan LAZYMAN;
// 2.创建一个私有的构造器
private LazyMan(){
}
// 3.写一个获取该实例的方法
public static LazyMan getInstance() {
// 判断该单例对象是否是第一次调用,如果是第一次就实例化出来
if (LAZYMAN == null){
LAZYMAN = new LazyMan();
}
return LAZYMAN;
}
}
测试:
// 在多线程的情况下测试单例模式
public static void main(String[] args) {
Runnable runnable = () -> {
System.out.println(Thread.currentThread().getName() + ":" + LazyMan.getInstance());
};
new Thread(runnable,"A").start();
new Thread(runnable,"B").start();
new Thread(runnable,"C").start();
}
问题:
我通过这个代码虽然写出了懒汉式,但是我的这段代码只有在单线程的时候在是安全的,当我们的程序多线程工作时,可能会出现这个对象被创建了多次,单例模式不再单例(多次几次,真的会出现创建了多个对象的情况),一个程序只能有一个单例对象,如果它被多次创建,还是单例对象吗
,所以为了解决这个问题,我们引出了下面的DCL懒汉式。
3.DCL懒汉式(Double Check Lock懒汉式,双重检测锁懒汉式)
为了在多线程的情况下,还用懒汉式只创建一个单例对象,我们应该怎么做?
好做,不就是多线程嘛,加锁就完事了,一提到多线程,你就得想到一件事加锁
,所以我们想出了双重检测锁的懒汉式
步骤:
- 创建一个类,类里面声明一个静态的类的变量
- 创建一个私有的构造器
- 写一个获取该实例的方法(在这个方法上我们要做一些文章)
- 首先判断该实例是否存在,
- 如果不存在,去实例化一个对象,但是实例化对象的这段代码要加锁。
- 最后返回单例对象
上代码
// 1.创建一个类,类里面声明一个静态的类的变量
public class DCLlazyMan {
public static DCLlazyMan DCLLAZYMAN;
// 2.创建一个私有的构造器
private DCLlazyMan(){
}
// 3.写一个获取该实例的方法
// 双重检测锁模式的 懒汉式单利 DCL懒汉式
public static DCLlazyMan getInstance(){
//首先判断该实例是否存在
if (DCLLAZYMAN == null){
// 如果不存在,去实例化一个对象,但是实例化对象的这段代码要加锁。
synchronized (DCLlazyMan.class){
// 再次判断该实例是否存在,这个双重检测锁我感觉有点像乐观锁
if (DCLLAZYMAN == null){
DCLLAZYMAN = new DCLlazyMan();
/**
* 1.分配内存空间
* 2.执行构造方法,初始化对象
* 3.把这个对象指向这个空间
*
*/
}
}
}
// 最后返回单例对象
return DCLLAZYMAN; // 可能出现指令重排,导致DCLLAZYMAN可能还没有完成构造 //解决方案:加volatile
}
}
问题(最少三个问题)
-
问什么要检测两次单例对象是否为空?
回答:这个问题还真有点不好描述,举个例子,现在两个线程,线程A和线程B,线程A执行到了这行代码if (DCLLAZYMAN == null){
的时候休眠了,这时线程B也执行到了这行代码if (DCLLAZYMAN == null){
并且一直执行下去,实例化了一个对象,然后线程B进行阻塞,线程A运行,这时如果我们不进行第二次检测,就会出现单例对象被实例化多次的现象(因为线程B已经实例化了单例对象),所以这就使我们问什么要使用双重检测锁 -
这个DCL懒汉式真的没问题吗?
回答:有问题,最起码还有两个问题-
不知大家是否听过一个神奇的名词叫做
指令重排
,就是java会为了优化代码会对你的代码顺序进行重排,什么意思呢?就是jvm执行的代码不一定是完全按照你写的代码去执行,恐怖吗,嘿嘿,当然人家的重排也是有一定的逻辑的,有兴趣的小伙伴可以去了解了解。为什么说这个事情,先问大家一个问题,DCLLAZYMAN = new DCLlazyMan();
这行代码是一个原子操作吗,显而易见不是呀,我都能看到它这一行代码起码作了三步操作,- 分配内存空间
- 执行构造方法,初始化对象
- 把这个对象指向这个空间
我们理想中的代码执行顺序是这个123,但是指令重排可能会让我们的代码执行顺序变成132,这个是完全可以做到的(不明白的小伙伴去了解一下指令重排),那当我们的代码执行顺序变成132的时候,会不会出现这种情况:还是两个线程,线程A和线程B,线程A执行
getInstance()
方法,然后当我执行到这行代码DCLLAZYMAN = new DCLlazyMan();
并且这行代码的执行顺序是132,当我执行到3的时候,线程A睡眠了,此时DCLLAZYMAN
已经不在是一个NULL(虽然还没有构造完成),此时线程B也执行这个方法getInstance()
,的时候,返回了判断DCLLAZYMAN
不为null,然后返回了这个对象,但是我们出现了一个问题,就是这个对象不完整,是残缺的,对不对
解决方案:禁止重排,使用volatile关键字禁止重排。(volatile关键字有三个特点:1.保证可见性;2,不保证原子性;3.禁止指令重排)
-
-
其实还有第三个问题,反射还是可以破解这个单例模式的,这个问题放在下面讲,先留个悬念
如今的代码
// 1.创建一个类,类里面声明一个静态的类的变量
public class DCLlazyMan {
// 使用volatile禁止指令重排
public volatile static DCLlazyMan DCLLAZYMAN;
// 2.创建一个私有的构造器
private DCLlazyMan(){
}
// 3.写一个获取该实例的方法
// 双重检测锁模式的 懒汉式单利 DCL懒汉式
public static DCLlazyMan getInstance(){
//首先判断该实例是否存在
if (DCLLAZYMAN == null){
// 如果不存在,去实例化一个对象,但是实例化对象的这段代码要加锁。
synchronized (DCLlazyMan.class){
// 再次判断该实例是否存在,这个双重检测锁我感觉有点像乐观锁
if (DCLLAZYMAN == null){
DCLLAZYMAN = new DCLlazyMan();
/**
* 1.分配内存空间
* 2.执行构造方法,初始化对象
* 3.把这个对象指向这个空间
*
*/
}
}
}
return DCLLAZYMAN; // 可能出现指令重排,导致DCLLAZYMAN可能还没有完成构造 //解决方案:加volatile
}
}
4.使用静态内部类实现单例模式(纯属炫技,没什么用)
步骤:
- 创建一个类并在创建一个内部类
直接上代码喽
/**
* 1.创建一个类并在创建一个内部类
*/
public class Holder {
private Holder(){
}
// 3. 写一个getInstance()方法返回单例对象
public static Holder getInstance(){
return InnerClass.HOLDER;
}
public static class InnerClass{
// 2.在内部类中定义一个实例化出来一个单例对象
private static final Holder HOLDER = new Holder();
}
}
5.使用枚举类创建单例模式(现在实现单例模式最常用的方式)
枚举类实现单例模式太简单了,直接上代码
public enum EnumSingle {
INSTANCE;
public static EnumSingle getInstance() {
return INSTANCE;
}
}
完事了。
枚举类创建单例模式的好处
- 1.既是懒汉式,还足够简单
- 2.反射破坏不了我们的单例模式(下面讲为什么反射破坏不了我们用enum实现的单例模式)
二、反射破坏单例模式
如果只是实现了我上面所述的DCL懒汉式,就可以完美实现单例模式,那就大错特错了,有没有人想到用Java的反射来破解单例模式,我这就来用反射破坏我实现的DCL懒汉式
上代码
// 用反射破坏单例模式
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// 输出单例对象
System.out.println(DCLlazyMan.getInstance());
// 使用反射创建出来DCLlazyMan的实例化对象
Constructor<DCLlazyMan> declaredConstructor = DCLlazyMan.class.getDeclaredConstructor(null);
// 设置让变量的访问控制修饰符无效(private,default,protected,public)
declaredConstructor.setAccessible(true);
// 输出通过反射创建的对象
DCLlazyMan dcLlazyMan = declaredConstructor.newInstance();
System.out.println(dcLlazyMan);
}
输出结果:不是一个对象(完犊子,DCL懒汉式被破坏了)
怎么办
在单例类中定义一个非当前对象(随便定义一个变量,boolean类型,默认false,这个变量是其他人看不到的)来作为标志位,在构造方法中进行判断。这个标志位在反射创建对象时是完全不知道的
上代码
// 1.创建一个类,类里面声明一个静态的类的变量
public class DCLlazyMan {
// 定义的是我的博客名字
private static boolean taomewhy = false;
// 使用volatile禁止指令重排
public volatile static DCLlazyMan DCLLAZYMAN;
// 2.创建一个私有的构造器
private DCLlazyMan(){
synchronized (DCLlazyMan.class){
if (taomewhy == false){
taomewhy = true;
}else {
throw new RuntimeException("不要试图使用反射破坏异常");
}
}
}
// 3.写一个获取该实例的方法
// 双重检测锁模式的 懒汉式单利 DCL懒汉式
public static DCLlazyMan getInstance(){
//首先判断该实例是否存在
if (DCLLAZYMAN == null){
// 如果不存在,去实例化一个对象,但是实例化对象的这段代码要加锁。
synchronized (DCLlazyMan.class){
// 再次判断该实例是否存在,这个双重检测锁我感觉有点像乐观锁
if (DCLLAZYMAN == null){
DCLLAZYMAN = new DCLlazyMan();
/**
* 1.分配内存空间
* 2.执行构造方法,初始化对象
* 3.把这个对象指向这个空间
*
*/
}
}
}
return DCLLAZYMAN; // 可能出现指令重排,导致DCLLAZYMAN可能还没有完成构造 //解决方案:加volatile
}
// 用反射破坏单例模式
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
System.out.println(DCLlazyMan.getInstance());
Constructor<DCLlazyMan> declaredConstructor = DCLlazyMan.class.getDeclaredConstructor(null);
declaredConstructor.setAccessible(true);
DCLlazyMan dcLlazyMan = declaredConstructor.newInstance();
System.out.println(dcLlazyMan);
}
}
三、为什么enum实现的单例模式没有办法破坏掉
试试反射能不能破坏用enum实现的单例模式
先用反射试试,才能知道能不能破坏
上代码
class Test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
// 输出单例对象
System.out.println(EnumSingle.INSTANCE);
// 使用反射创建出来DCLlazyMan的实例化对象
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor();
// 设置让变量的访问控制修饰符无效(private,default,protected,public)
declaredConstructor.setAccessible(true);
// 输出通过反射创建的对象
EnumSingle enumSingle = declaredConstructor.newInstance();
System.out.println(enumSingle);
}
}
结果如下:
没有这个构造方法???,为什么?
反射创建对象时传的参数不够,不然不应该是报这个错误,为什么,我们来扒一波源码,首先我们是用Constructor类newInstance()
方法的来创建对象,我们点进去看一下源码发现
所以我们是反射创建对象时传的参数不够,我们来反编译一下,我们的class文件
步骤,在自己写的类的所在目录下打开dos窗口,然后使用javap -p 你的class文件
反编译返现有这个空参构造方法,可运行解决却报错没有,所以我们反编译的代码有问题,所以我们用一个更专业的反编译软件jda.exe
想要下载jad的小伙伴点下方的连接
百度网盘jad链接:https://pan.baidu.com/s/1j3-q36EHxGOIkiL1qL-VlA:
提取码:x4gt
使用jad步骤,把下载的jad放在自己写的类的所在目录下,然后打开dos窗口,
此刻当前目录下多了一个使用jad文件反编译出来的java类,打开java类
package single;
public final class EnumSingle extends Enum
{
public static EnumSingle[] values()
{
return (EnumSingle[])$VALUES.clone();
}
public static EnumSingle valueOf(String name)
{
return (EnumSingle)Enum.valueOf(single/EnumSingle, name);
}
private EnumSingle(String s, int i)
{
super(s, i);
}
public static EnumSingle getInstance()
{
return INSTANCE;
}
public static final EnumSingle INSTANCE;
private static final EnumSingle $VALUES[];
static
{
INSTANCE = new EnumSingle("INSTANCE", 0);
$VALUES = (new EnumSingle[] {
INSTANCE
});
}
}
这是反编译出来的枚举类,发现不是一个空参的构造方法,所以我们改造我们的反射实例化的程序的代码,传入两个参数,改造如下
class Test{
public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
System.out.println(EnumSingle.INSTANCE);
// 就多传两个参数String.class,int.class,切记不要穿Integer.class,int和Integer不是一个玩意
Constructor<EnumSingle> declaredConstructor = EnumSingle.class.getDeclaredConstructor(String.class, int.class);
declaredConstructor.setAccessible(true);
EnumSingle enumSingle = declaredConstructor.newInstance();
System.out.println(enumSingle);
}
}
然后再执行测试代码,报错,不能通过反射创建枚举类,跟我们在Constructor类newInstance()
方法中看到的抛出的异常完全一样。石锤,enum创建的单例模式没办法用反射破坏掉。
四、其他
补充一些小知识,反编译出来的代码可能跟你写的代码有一点出入,毕竟是被优化过的代码,但是实现的功能完全一样。