Java设计模式-单例模式

目录

单例模式:

单例模式特点:

单例模式应用:

单例实例:

懒汉模式

双重检测加锁模式:

饿汉模式:

内部静态模式:

枚举模式:

那个单例模式最安全?

上述代码 GitHub 地址:https://github.com/baicun/designPatterns


单例模式:

保证在一个JVM中,该对象只有一个实例存在,并提供一个全局的访问点。

单例模式特点:

1. 一些类创建比较频繁,对于一些大型复杂的对象,这是一笔很大的系统开销。

2. 省去了 new 操作符,降低了系统内存的使用频率,减轻GC压力。

3. 便于管理,一个系统有且只有一个实例。

单例模式应用:

        在计算机系统中,线程池、缓存、日志对象、对话框、打印机、显卡的驱动程序对象常被设计成单例。这些应用都或多或少具有资源管理器的功能。也就是说凡是涉及资源的使用就一定会出现多个使用一个的问题,这样容易导致冲突,结果不一致的问题,所以单例的应用是普遍的有效的。

单例实例:

类图:

先看一个最简单的单例模式:

懒汉模式

public class Singleton {

    // 私有的静态实例
    private static Singleton instance = null;  //懒汉模式
    // 私有构造方法
    private Singleton(){
    }
    // 静态工厂创建
    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

简单的单例模式完成,在多线程环境下会出现一个bug,例如同时俩个线程进来,第一个线程判断 instance == null,则去创建对象,此时第二个线程进来,在第一个线程未创建对象前,第二个线程同样又去创建新对象,问题出现了...。粗暴的改进一下,在创建静态工厂的时候增加关键字 synchronized(同步的) 。如下:

    // 静态工厂创建
    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }

这样做一点问题也没有,但是作为一名优秀的程序员就要做到精益求精,不要留下烂代码...不然以后阅读你代码的人一定会骂娘

首先分析一下为什么这么写不好,synchronized 加在整个方法上边,也就是说我们的项目只要实例化这个类,调用 getInstance() 就会锁住这个对象,多线程并发的时候每一个线程都要先锁住这个对象,然后判断(instance == null)是否初始化,毋庸置疑这样写不够健壮,非常影响性能。

不在方法上直接加关键字 synchronized ,那继续细化,在单例没有初始化为 null 的时候锁定这个对象,代码如下:

    // 静态工厂创建
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (instance) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

这样写就解决了上面并发加锁的问题,我刚开始学习的时候,以为这就完美了。

后来我才知道 JVM 并不能保证线程执行的顺序,也就是说如果有俩个线程同时调用我们的这个实例类,线程 A 锁定实例,并且instance = new Singleton(); 这时线程 B 进来发现 instance 不是 null ,直接返回对象,这里可能就出现了问题,为什么?分析一下【instance = new Singleton()】JVM 初始化的顺序:类的静态成员 --> 类的实例成员 --> 类的构造方法。也就是说在执行构造方法前,线程 B 进来了,并且直接返回使用,这时其实 JVM 还没有线程 A 初始化完成。

对于上述线程顺序无法保证,这里可以使用如下模式:

双重检测加锁模式:

    private volatile  static Singleton instance = null;//volatile 原子性,有序性,可见性
    // 私有构造方法
    private Singleton(){
    }
    // 静态工厂创建
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

并发情景:

1.线程 A 调用 getInstance(),并且进入锁定块,开始初始化,

2.线程 B 进来,开始 instance == null,进入锁定块,此时已经被线程 A 锁定,

3.线程 A 初始化完成,退出解除锁,

4.线程 B 进入锁定,此时已经存在实例对象,退出解除锁,

5.线程 A 获得实例对象,线程 B 获得线程A 实例的对象。

 

对于上述初始化的问题呢就有了饿汉模式,内部静态模式,枚举模式出现,如下图:

饿汉模式:

    // 私有的静态实例
    private static Singleton instance = new Singleton(); //饿汉模式
    // 私有构造方法
    private Singleton() {

    }
    // 静态工厂创建
    public static Singleton getInstance() {
        return instance;
    }

内部静态模式:

    // 私有的静态实例
    private static Singleton instance = null;
    // 内部静态代码块
    static {
        instance = new Singleton();
    }
    // 私有构造方法
    private Singleton() {

    }
    // 静态工厂创建
    public static Singleton getInstance() {
        return instance;
    }

枚举模式:

public enum EnumSingleton {
    INSTANCE;
    public void read(){
        System.out.println("read");
    }
}

综上所述,不同应用场景使用不同的模式,我个人推荐内部静态模式,双重检测加锁模式和枚举模式。

那个单例模式最安全?

在《effective java》一书中,作者提到单例模式是保证线程安全和单一实例的最佳实践。有俩种代码攻击类型,反射攻击和反序列化攻击,以下分别来看一下。

1. 反射攻击:

//************************【反射攻击单例模式比较 -start】***************************************************************
    // 测试反射机制下的“双重检测加锁模式”
    /*public static void main(String[] args) throws Exception {
        Singleton sg1=Singleton.getInstance();
        Singleton sg2=Singleton.getInstance();
        System.out.println("正常情况下,获得的两个实例是否相同:"+(sg1==sg2));
        //  获取所有构造器
        Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton s2=constructor.newInstance();
        System.out.println(sg1+"\n"+sg2+"\n"+s2);
        System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(sg1==s2));
        //输出结果
        //正常情况下,实例化两个实例是否相同:true
        //通过反射攻击单例模式情况下,实例化两个实例是否相同:false
    }*/

    // 测试反射机制下的“枚举单例模式”
    /*public static void main(String[] args) throws Exception{
        EnumSingleton singleton1=EnumSingleton.INSTANCE;
        EnumSingleton singleton2=EnumSingleton.INSTANCE;
        System.out.println("正常情况下,实例化两个实例是否相同:"+(singleton1==singleton2));
        Constructor<EnumSingleton> constructor= null;
        constructor = EnumSingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        EnumSingleton singleton3= null;
        singleton3 = constructor.newInstance();
        System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3);
        System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(singleton1==singleton3));

        // 输出结果
        //正常情况下,实例化两个实例是否相同:true
        //Exception in thread "main" java.lang.NoSuchMethodException: com.example.singletonPattern.EnumSingleton.<init>()
        //会检查该类是否ENUM修饰,如果是则抛出异常,反射失败。所以枚举是不怕发射攻击的。
    }*/

    //经过比较,枚举单例模式不会受到反射攻击
    //************************【反射攻击单例模式比较 -end】***************************************************************

2. 反序列化攻击

//************************【反序列化攻击单例模式比较  -start】***************************************************************

    // 测试反序列化机制下的“双重检测加锁模式”
    /*public static void main(String[] args) throws Exception {
        Singleton instance=Singleton.getInstance();
        byte[] serialize = SerializationUtils.serialize(instance);
        Singleton newInstance = (Singleton) SerializationUtils.deserialize(serialize);
        System.out.println("反序列化情况下,两个实例是否相同:"+(instance==newInstance));
        //输出结果
        //反序列化情况下,两个实例是否相同:false
    }*/
    // 测试反射机制下的“枚举单例模式”
    public static  void main(String[] args){
        EnumSingleton enumSingleton = EnumSingleton.INSTANCE;
        byte[] serialize = SerializationUtils.serialize(enumSingleton);
        EnumSingleton newInstance = (EnumSingleton) SerializationUtils.deserialize(serialize);
        System.out.println("反序列化情况下,两个实例是否相同:"+(enumSingleton==newInstance));
        //输出结果
        // 反序列化情况下,两个实例是否相同:true
    }

    //经过比较,枚举单例模式不会受到反序列化攻击
    //************************【反序列化攻击单例模式比较  -end】***************************************************************

通过上面方法测试,枚举模式在安全方面的确无懈可击,实现也非常简单,但不同的场景、项目应用,选择适合的就好

 

上述代码 GitHub 地址:https://github.com/baicun/designPatterns

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值