一文搞定单例模式

一,为什么要单例


二,搞定这3个问题就搞定单例模式了


1,多线程下线程安全吗

单线程下保证只有一个实例还不是真正的单例,多线程并发也要保证只会有一个实例。

补充思考:是否需要考虑线程安全问题,你只要问自己如下3个问题:

1,是多线程吗
2,非原子操作吗
3,有共享数据吗

如果以上三个问题的答案都是肯定的,那就要考虑线程安全了。


2,能防止用户通过反射创建新对象吗?

3,能防止在反序列化时创建新对象吗?

我把上面三个问题称之为 “ 单 例 三 刀 ” \color{red}{“单例三刀”} ,要是能禁得住以上三刀而分毫不伤,那就是完美的单例模式。

接下来,结合上述三个问题逐个分析单例模式的经典实现方式。


三,单例模式的实现及原理


1,单例模式实现方式一:饿汉式

饿汉式3要点:

1,声明一个静态变量并初始化
2,构造函数私有化
3,提供一个公有的Get函数返回第1步创建的对象

饿汉式初步实现:
public class SingleTon {
    private static SingleTon singleTon = new SingleTon();
    
    private SingleTon() {
        
    }

    public static SingleTon getSingleTon() {
        return singleTon;
    }
}

接下来,我们就用“单例三刀”来检验这种实现的成色。

第一刀 线程安全吗?

相当安全,虚拟机会保证静态变量只在类加载时被初始化一次

第二刀 能防止用户用反射创建新对象吗?

不能。可以通过反射创建新对象。

口说无凭,做个测试吧:

package DesignPattern.Singleton;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class SingleTonTest {
    public static void main(String[] args) throws ClassNotFoundException, IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        // 通过反射破解单例
        Class<?> aClass = Class.forName("DesignPattern.Singleton.SingleTon");
        Constructor<?> constructor = aClass.getDeclaredConstructor();

        constructor.setAccessible(true);

        Object o = constructor.newInstance();

        if( o != SingleTon.getSingleTon()) {
            System.out.println("单例模式不单例,通过反射创建了新对象");
        }
    }
}

结果如下:

image.png

说明当前这种实现方式是有缺陷的,完善的办法有很多,比如【effective java】书提到在构造函数中进行判断,如果用户创建新对象就抛错。

饿汉式实现优化1(防止通过反射创建新对象):
public class SingleTon {
    private static SingleTon singleTon = new SingleTon();
    
    private SingleTon() {
        if (null != singleTon) {
           throw new RuntimeException("Method invoke error"); 
       }
    }

    public static SingleTon getSingleTon() {
        return singleTon;
    }
}

用户通过反射创建对象,会抛出异常,并得到如下错误信息:

image.png

第三刀 能防止通过反序列化创建新对象吗

不能。

口说无凭,再做个测试吧:

 public static void main(String[] args) throws IOException, ClassNotFoundException {
        SingleTon singleTon = SingleTon.getSingleTon();

        // 序列化破解单例
        FileOutputStream fos = new FileOutputStream("../a.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(singleTon);
        oos.close();
        fos.close();

        //反序列化
        ObjectInputStream ois =new ObjectInputStream(new FileInputStream("../a.txt"));
        SingleTon singleTon2 = (SingleTon) ois.readObject();

        if( singleTon2 != singleTon) {
            System.out.println("单例模式不单例,通过反序列化创建了新对象");
        } else {
            System.out.println("单例模式真单例,反序列化创建不了新对象");
        }
    }

结果如下:
image.png

完善方案如下,加入一个反序列化相关的方法即可。

饿汉式实现优化2(防止反序列化创建新对象):
package DesignPattern.Singleton;

import java.io.ObjectStreamException;
import java.io.Serializable;

public class SingleTon implements Serializable {
    private volatile static SingleTon singleTon = new SingleTon();

    private SingleTon() {
        if (null != singleTon) {
            throw new RuntimeException("Method invoke error");
        }
    }

    public static SingleTon getSingleTon() {
        return singleTon;
    }

    //反序列化时(加这个方法可以防止反序列化漏洞)
    private Object readResolve() throws ObjectStreamException {
        return singleTon;
    }

}

再次测试,结果暴爽:
image.png

所有的单例实现都要考虑反射和反序列化,但处理方式都雷同(除了枚举)。

2,单例模式实现方式二:懒汉式

懒汉式三要点:

1,声明一个私有变量
2,构造函数私有化
3,声明一个公有的Get方法,用户第一次调用Get方法是创建对象

懒汉式初步实现:
package DesignPattern.Singleton;

import java.io.ObjectStreamException;
import java.io.Serializable;

public class SingleTonLazy implements Serializable {
    private SingleTonLazy singleTon;

    private SingleTonLazy() {

    }

    public SingleTonLazy getSingleTon() {
        if(null == singleTon) {
            singleTon = new SingleTonLazy();
        }
        return singleTon;
    }
}

接下来, “ 单 例 三 刀 ” \color{red}{“单例三刀”} 来检验这种实现的成色。

第一刀 线程安全吗?

不安全,多线程下可能会有多个实例

第二刀 能防止用户用反射创建新对象吗?

不能

第三刀 能防止通过反序列化创建新对象吗

不能

刀刀毙命,意味着我们要做更多的工作确保单例正常工作。

如何破第二刀反射和第三刀反序列化,如前所述,不再赘述。

下面讲讲如何保证懒汉式的线程安全。

要保证线程安全,粗暴有效的就是加锁。

懒汉式实现优化1:给Get方法加锁
package DesignPattern.Singleton;

import java.io.ObjectStreamException;
import java.io.Serializable;

public class SingleTonLazy implements Serializable {
    private SingleTonLazy singleTon;

    private SingleTonLazy() {
        if (null != singleTon) {
            throw new RuntimeException("Method invoke error");
        }
    }

    // 加代码锁,保证线程安全
    public synchronized SingleTonLazy getSingleTon() {
        if(null == singleTon) {
            synchronized() {
                 singleTon = new SingleTonLazy();
            }
        }
        return singleTon;
    }

    //反序列化时(加这个方法可以防止反序列化漏洞)
    private Object readResolve() throws ObjectStreamException {
        return singleTon;
    }

}

但这样加锁,显然过于粗暴,影响效率。

懒汉式实现优化2:给Get方法加代码锁

package DesignPattern.Singleton;

import java.io.ObjectStreamException;
import java.io.Serializable;

public class SingleTonLazy implements Serializable {
    private SingleTonLazy singleTon;

    private SingleTonLazy() {
        if(null == singleTon) {
            synchronized (SingleTonLazy.class) {
                singleTon = new SingleTonLazy();
            }
        }
    }

    // 加方法锁,保证线程安全
    public SingleTonLazy getSingleTon() {
        if(null == singleTon) {
            singleTon = new SingleTonLazy();
        }
        return singleTon;
    }

    //反序列化时(加这个方法可以防止反序列化漏洞)
    private Object readResolve() throws ObjectStreamException {
        return singleTon;
    }

}

这样减少了加锁的次数和锁范围,效率提高了,貌似也线程安全了。但真的线程安全吗?

no,这样改还是线程不安全

懒汉式实现优化3:语句锁+双重判空
package DesignPattern.Singleton;

import java.io.ObjectStreamException;
import java.io.Serializable;

public class SingleTonLazy implements Serializable {
    private SingleTonLazy singleTon;

    private SingleTonLazy() {
        if (null != singleTon) {
            throw new RuntimeException("Method invoke error");
        }
    }

    // 加锁,保证线程安全,双重判空,保证单例
    public SingleTonLazy getSingleTon() {
        if(null == singleTon) {
            synchronized (SingleTonLazy.class) {
                if(null == singleTon) {
                    singleTon = new SingleTonLazy();
                }
            }
        }
        return singleTon;
    }

    //反序列化时(加这个方法可以防止反序列化漏洞)
    private Object readResolve() throws ObjectStreamException {
        return singleTon;
    }

}

ok,恭喜你实现了无漏洞的懒汉式单例模式。


3,单例模式实现方式三:静态内部类

这也是一种常见的单例实现方式。

静态内部类单例模式三要素:

1,私有的成员变量
2,私有的静态内部类,内部类拥有一个私有静态成员变量
3,公有的Get方法

单例模式的实现:
package DesignPattern.Singleton;

import java.io.ObjectStreamException;
import java.io.Serializable;

public class SingleTonStatic implements Serializable {
    private SingleTonStatic singleTon;

    private SingleTonStatic() {
        if (null != singleTon) {
            throw new RuntimeException("Method invoke error");
        }
    }

    public SingleTonStatic getSingleTon() {
        return SingleTonHolder.singleTonStatic;
    }

    private static class SingleTonHolder{
        private static SingleTonStatic singleTonStatic = new SingleTonStatic();
    }
    
    //反序列化时(加这个方法可以防止反序列化漏洞)
    private Object readResolve() throws ObjectStreamException {
        return singleTon;
    }

}

来,使出 “ 单 例 一 刀 ” \color{red}{“单例一刀”} (反序列化和反射不再讨论,详见饿汉式部分)

第一刀 线程安全吗?

安全,多线程下也只有一个实例

静态内部类利用虚拟机加载类的机制,保证类只会被加载一次、初始化一次。


4,单例模式实现方式三:枚举

看了上面三种单例实现,会觉得心好累,一个简单的单例模式,要搞出这么多幺蛾子!

好吧,其实java5之后,出现了一种最简单的单例实现语法-枚举。

枚举单例的实现
package DesignPattern.Singleton;

import java.io.ObjectStreamException;
import java.io.Serializable;

public enum SingleTonEnum {
    SINGLE_TON_ENUM
}

辣眼睛吧,不信吧,可这就是现实,就这么简单。

来吧,使出“单例三刀”。

第一刀 线程安全吗?

安全

第二刀 能防止用户用反射创建新对象吗?

第三刀 能防止通过反序列化创建新对象吗

的确,如你所见,我们什么都不用做,一个完美的单例就这样产生了。

image.png


枚举是怎么做到的呢?

接下来逐一分析枚举如何通过简单的代码实现完美的单例。

一,枚举单例是线程安全的

枚举是一种语法糖,是对类class的包装,我们通过反编译(如何反编译,见参考文献1)看看枚举的真实面目。

// Decompiled by Jad v1.5.8g. Copyright 2001 Pavel Kouznetsov.
// Jad home page: http://www.kpdus.com/jad.html
// Decompiler options: packimports(3) 
// Source File Name:   SingleTonEnum.java

package DesignPattern.Singleton;


public final class SingleTonEnum extends Enum
{

    public static SingleTonEnum[] values()
    {
        return (SingleTonEnum[])$VALUES.clone();
    }

    public static SingleTonEnum valueOf(String s)
    {
        return (SingleTonEnum)Enum.valueOf(DesignPattern/Singleton/SingleTonEnum, s);
    }

    private SingleTonEnum(String s, int i)
    {
        super(s, i);
    }

    public static final SingleTonEnum SINGLE_TON_ENUM;
    private static final SingleTonEnum $VALUES[];

    static 
    {
        SINGLE_TON_ENUM = new SingleTonEnum("SINGLE_TON_ENUM", 0);
        $VALUES = (new SingleTonEnum[] {
            SINGLE_TON_ENUM
        });
    }
}

从反编译结果可以发现三个有趣的东西:

1,构造方法为private
2,定义了静态变量
3,在static代码块中对其进行初始化

靠,这不就是前文用饿汉式和静态内部类实现单例时处理多线程采用的方案吗?

image.png

接下来,要问另一个问题,枚举单例怎样防止通过反射创建对象?


二,不能通过反射创建枚举实例

看下Constructor这个类的源码,很容易理解:

@CallerSensitive
    public T newInstance(Object ... initargs)
        throws InstantiationException, IllegalAccessException,
               IllegalArgumentException, InvocationTargetException
    {
        if (!override) {
            if (!Reflection.quickCheckMemberAccess(clazz, modifiers)) {
                Class<?> caller = Reflection.getCallerClass();
                checkAccess(caller, clazz, null, modifiers);
            }
        }

       // 这里决定了不能通过反射创建枚举实例
       if ((clazz.getModifiers() & Modifier.ENUM) != 0){ 
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
        ConstructorAccessor ca = constructorAccessor;   // read volatile
        if (ca == null) {
            ca = acquireConstructorAccessor();
        }
        @SuppressWarnings("unchecked")
        T inst = (T) ca.newInstance(initargs);
        return inst;
    }

注意上述代码中的注释部分:当类有enum修饰符时,不能通过反射创建新对象。

可见,java在源码级别已经限制了不能通过反射创建枚举实例。

image.png

对,还有一个问题,枚举是如何防止序列化创建新对象的?


三,不能通过序列化创建枚举实例

看看java规范关于枚举的序列化的说明吧:

Enum constants are serialized differently than ordinary serializable or externalizable objects. 
The serialized form of an enum constant consists solely of its name; 
field values of the constant are not present in the form. 
To serialize an enum constant, ObjectOutputStream writes the value returned by the enum constant’s name method. 
To deserialize an enum constant, ObjectInputStream reads the constant name from the stream;
 the deserialized constant is then obtained by calling the java.lang.Enum.valueOf method,
 passing the constant’s enum type along with the received constant name as arguments. 
Like other serializable or externalizable objects, enum constants can function as the targets of back references appearing subsequently in the serialization stream.
 The process by which enum constants are serialized cannot be customized: any class-specific writeObject, readObject, readObjectNoData, writeReplace, 
and readResolve methods defined by enum types are ignored during serialization and deserialization. Similarly, any serialPersistentFields or 
serialVersionUID field declarations are also ignored–all enum types have a fixedserialVersionUID of 0L. 
Documenting serializable fields and data for enum types is unnecessary, since there is no variation in the type of data sent

其实就是表达了2个意思:

1,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的

2,在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。

也就是说,以下面枚举为例,序列化的时候只将 SINGLE_TON_ENUM 这个名称输出,反序列化的时候再通过这个名称,查找对于的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。

写个DEMO测试一下:

public static void main(String[] args) throws IOException, ClassNotFoundException {
        SingleTonEnum singleTonEnum = SingleTonEnum.SINGLE_TON_ENUM;

        // 序列化破解单例
        FileOutputStream fos = new FileOutputStream("../a.txt");
        ObjectOutputStream oos = new ObjectOutputStream(fos);
        oos.writeObject(singleTonEnum);
        oos.close();
        fos.close();

        //反序列化
        ObjectInputStream ois =new ObjectInputStream(new FileInputStream("../a.txt"));
        SingleTonEnum singleTonEnum1 = (SingleTonEnum) ois.readObject();

        if( singleTonEnum != singleTonEnum1) {
            System.out.println("枚举单例模式不单例,通过反序列化创建了新对象");
        } else {
            System.out.println("枚举单例模式真单例,反序列化创建不了新对象");
        }
    }

结果如下:
image.png

好吧,不能通过反序列化创建枚举实例也得到了证明。

[参考文献]

1,JAD 反编译枚举

2,枚举实现单例原理分析

3,枚举的序列化

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小手追梦

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值