设计模式——单例模式完全解析

介绍

单例模式是设计模式中最常用也最简单的一种,主要的使用场景是在程序开发中的各种工具类上,其作用在于保证一个类在整个程序中只有一个实例

实例

我们一般将单例模式分为饿汉单例懒汉模式

饿汉单例

饿汉单例在此类加载时就主动创建出来,实例如下

/**
 * @author : psyduck
 * @Date : 2022/2/18 3:10
 * @Desc :单例模式——饿汉 相比懒汉
 * 由于最开始就将对象创建了出来 所以操作时速度更快 但是存在的时间会更长 占用的资源更多 容易产生垃圾对象
 * 初始化懒加载 否
 * 多线程安全 是
 **/
public class SingletonModeHungry {

    // 存在该类型私有静态实例
    private static SingletonModeHungry singletonModeHungry = new SingletonModeHungry();

    /**
     * description : 单例模式只提供私有的构造<br/>
     * @author psyduck
     * @date 2022/2/20 11:01
     * @return
     */
    private SingletonModeHungry(){

    }

    /**
     * description : 通过公有方法返回singletonMode对象<br/>
     * @param  
     * @author psyduck
     * @date 2022/2/20 11:10
     * @return DesignMode.SingletonMode
     */
    public static SingletonModeHungry getSingletonMode(){
        return singletonModeHungry;
    }
}

通过这个例子可以看出
1.此类无法通过new生成实例,因为手动创建了私有构造
2.此类手动创建了此类的静态对象
3.只能通过暴露的公有静态方法来访问私有静态对象
这也是单例模式的基本特征

除此以外,饿汉模式本身的优缺点如下

优点

1.操作速度更快,因为对象在类加载时直接被创建了出来,不必在初次使用时浪费创建对象的时间
2.多线程安全

缺点

1.因为在类加载时就创建了对象,所以对象存在的时间比需要的时间更长,占用内存和资源的时间更长
2.容易产生垃圾对象,及从始至终未使用过的对象

懒汉单例

/**
 * @author : psyduck
 * @Date : 2022/2/20 11:26
 * @Desc : 单例模式 懒汉
 * 第一次调用才初始化对象 避免了内存浪费
 * 懒加载 是
 * 线程安全 否
 * 不过由于此方法在整体程序中应当不会调用多次 所以效率影响不大
 **/
public class SingletonModeLazy {
    // 静态私有对象
    private static SingletonModeLazy singletonModeLazy;
    // 静态构造
    private SingletonModeLazy(){

    }
    //判断是否是第一次调用此方法 是的话则创建对象
    public static SingletonModeLazy getSingletonModeLazy(){
        if(singletonModeLazy == null){
            singletonModeLazy = new SingletonModeLazy();
        }
        return singletonModeLazy;
    }
}

懒汉模式和饿汉模式一样,同样是单例模式的一种,不过相比饿汉模式有一定的区别,懒汉模式只会在对象初次被调用时生成对象

懒汉模式的优缺点如下

优点

1.相比饿汉模式避免了内存浪费

缺点

1.在多线程的情况下并不安全,可能出现存在多个对象的情况
…以及其他一些问题会在下面的例子一一提出

问题详解

懒汉模式虽然相比饿汉模式多了懒加载,避免了内存浪费,不过这种形式也导致在多线程的情况下因为getSingletonModeLazy方法被同时调用导致执行了多次new SingletonModeLazy使得对象不唯一,而单例模式就是为了保证此类在整个程序中有且只有一个实例,所以需要进行部分改进

懒汉单例改进——方法同步锁

第一种解决方案大部分人应该都能想出,在get方法上加上synchronized关键字使得方法成为同步方法,自然也就不会出现多线程并发导致存在多个对象的情况了,实例如下

/**
 * @author : psyduck
 * @Date : 2022/2/21 16:20
 * @Desc :第一种解决线程安全的方法 —— 方法同步锁
 * 给get方法加上synchronized同步锁 会因此牺牲效率
 **/
public class SingletonModeLazyThreadSafeOne {

    private static SingletonModeLazyThreadSafeOne singletonModeLazyThreadSafeOne;

    private SingletonModeLazyThreadSafeOne(){

    }

    public static synchronized SingletonModeLazyThreadSafeOne getSingletonModeLazyThreadSafeOne(){
        if(singletonModeLazyThreadSafeOne == null){
            singletonModeLazyThreadSafeOne = new SingletonModeLazyThreadSafeOne();
        }
        return singletonModeLazyThreadSafeOne;
    }
}

那么相比上一个懒汉模式的例子,此例子又有哪些优缺点呢?

优点

1.解决了多线程出现多个对象的情况

缺点

1.对整个方法进行同步是非常影响性能的,举例如果此方法耗时0.1s,同时有100个线程在调用这个方法,即其中99个进入阻塞状态,那么整体浪费了9.9s,所以此时依然需要改进
…其他问题会在接下来的例子中提出

懒汉单例改进——双重校验锁

为了优化上一个例子的性能,所以我们需要缩小被同步的代码量,那么就有了双重校验锁的写法,实例如下

/**
 * @author : psyduck
 * @Date : 2022/2/21 16:53
 * @Desc : 第三种解决懒汉式线程安全问题的方案 ——双重校验锁
 **/
public class SingletonModeLazyThreadSafeTwo {
    private static SingletonModeLazyThreadSafeTwo singleton;

    private SingletonModeLazyThreadSafeTwo(){

    }
    
    // 在使用双重校验避免懒汉模式线程不安全问题时 存在多线程重排序问题
    // 即
    // 将 singleton = new SingletonModeLazyThreadSafeTwo(); 拆解为三步
    // 1.分配内存空间 2.初始化对象 3.设置singleton指向对应内存空间
    // 不过这三步的顺序可能并不固定 这是符合Java规范的
    // 当顺序改为132时 当执行完第三步时 另一个线程访问这个方法 就会出现 singleton!=null的情况 此时返回的对象是一个未经初始化的对象
    public static SingletonModeLazyThreadSafeTwo getSingleton(){
        if(singleton == null){
            // 因为get方法是一个静态方法,在它内部不能使用未静态的或者未实例的类对象
            // 所以我们一般直接同步此类本身
            synchronized (SingletonModeLazyThreadSafeTwo.class){
                if (singleton == null){
                    singleton = new SingletonModeLazyThreadSafeTwo();
                }
            }
        }
        return singleton;
    }
}

此例子相比上一例子的优点和缺点如下

优点

1.被锁定的同步代码块耗费时间极少,提高了性能

缺点

1.因为同步代码块耗费时间变低,所以出现了重排序问题

问题详解

简单来说,A线程执行完代码之后new SingletonModeLazyThreadSafeTwo方法后,
Java虚拟机会执行3步操作来实际创建对象,在此情况下由于此同步代码块耗时极低,导致3步操作未结束时,B线程也执行了同样的操作,此时A线程的对象创建还未结束,所以B线程得到的是一个未初始化完成的对象,实际的3步操作图如下

在这里插入图片描述所以针对此问题,我们依然需要对代码进行一定的优化操作

懒汉单例改进——volatile+双重校验锁

实例如下

/**
 * @author : psyduck
 * @Date : 2022/2/21 16:53
 * @Desc : 第三种解决懒汉式线程安全问题的方案 ——双重校验锁
 **/
public class SingletonModeLazyThreadSafeTwo {
    // 在使用volatile的情况下 不会出现重排序现象
    private volatile static SingletonModeLazyThreadSafeTwo singleton;

    private SingletonModeLazyThreadSafeTwo(){

    }

    public static void main(String[] args) throws Exception{
        // 首先获取一个类对象
        SingletonModeLazyThreadSafeTwo singleton = SingletonModeLazyThreadSafeTwo.getSingleton();
        // 之后通过反射获取此类的所有构造器
        Constructor<SingletonModeLazyThreadSafeTwo> constructor = SingletonModeLazyThreadSafeTwo.class.getDeclaredConstructor();
        // 然后设置构造器 可以访问私有变量/抑制对应类的安全检查
        constructor.setAccessible(true);
        // 最终使用构造器直接调用该类的私有构造以达成建立新对象的目的
        SingletonModeLazyThreadSafeTwo newSingleton = constructor.newInstance();
        // 比较新对象和老对象 可以发现内存地址是不同的
        System.out.println(singleton);
        System.out.println(newSingleton);
        System.out.println(singleton == newSingleton);
    }
    // 在使用双重校验避免懒汉模式线程不安全问题时 存在多线程重排序问题
    // 即
    // 将 singleton = new SingletonModeLazyThreadSafeTwo(); 拆解为三步
    // 1.分配内存空间 2.初始化对象 3.设置singleton指向对应内存空间
    // 不过这三步的顺序可能并不固定 这是符合Java规范的
    // 当顺序改为132时 当执行完第三步时 另一个线程访问这个方法 就会出现 singleton!=null的情况 此时返回的对象是一个未经初始化的对象
    public static SingletonModeLazyThreadSafeTwo getSingleton(){
        if(singleton == null){
            // 因为get方法是一个静态方法,在它内部不能使用未静态的或者未实例的类对象
            synchronized (SingletonModeLazyThreadSafeTwo.class){
                if (singleton == null){
                    singleton = new SingletonModeLazyThreadSafeTwo();
                }
            }
        }
        return singleton;
    }
}

在加入了volatile关键字后,JVM虚拟机针对此对象的创建步骤会被强制定为按照顺序执行,这样就不会出现重排序的问题了
除此以外还有另一种解决重排序问题的方案,如下

懒汉单例改进——静态内部类

/**
 * @author : psyduck
 * @Date : 2022/2/21 16:23
 * @Desc :第二种解决单例模式懒汉情况下线程安全问题的方案 —— 静态内部类
 * 使用静态内部类的方式生成类对象并由外部获取,隔绝了多线程所带来的问题,因为Java虚拟机在同一时间只会让一个线程执行类的初始化加载操作
 * 同样符合懒汉模式的规范,第一次使用时才会进行加载此类
 **/
public class SingletonModeLazyThreadSafeThree {

    private static class Singleton{
        private static SingletonModeLazyThreadSafeThree singleton = new SingletonModeLazyThreadSafeThree();
    }

    private SingletonModeLazyThreadSafeThree(){

    }

    public static SingletonModeLazyThreadSafeThree getSingleton(){
        return Singleton.singleton;
    }
}

这种单例的实现方式可能和前面的例子稍有不同,不过同样满足单例模式的要求,同时因为此时对象在匿名内部类中,我们使用get方法获取相应对象时,匿名内部类(匿名内部类只有在被使用时才会开始类加载)才会开始类加载,而Java虚拟机在同一时间只会让一个线程执行类的初始化操作,强制其他线程进入阻塞,这样的话,虽然对象直接进行了new操作,但是依然起到了懒加载的作用,同时还是线程安全的。
不过以上两种线程安全的单例依然存在另外的问题,如下

单例模式中的反射、反序列化攻击

/**
     * description : 反射攻击问题展现<br/>
     * @param args
     * @author psyduck
     * @date 2022/2/21 16:38
     * @return void
     */
    public static void main(String[] args) throws Exception{
        // 首先获取一个类对象
        SingletonModeLazyThreadSafeThree singleton = SingletonModeLazyThreadSafeThree.getSingleton();
        // 之后通过反射获取此类的所有构造器
        Constructor<SingletonModeLazyThreadSafeThree> constructor = SingletonModeLazyThreadSafeThree.class.getDeclaredConstructor();
        // 然后设置构造器 可以访问私有变量/抑制对应类的安全检查
        constructor.setAccessible(true);
        // 最终使用构造器直接调用该类的私有构造以达成建立新对象的目的
        SingletonModeLazyThreadSafeThree newSingleton = constructor.newInstance();
        // 比较新对象和老对象 可以发现内存地址是不同的
        System.out.println(singleton);
        System.out.println(newSingleton);
        System.out.println(singleton == newSingleton);
    }

而此问题以上所有实例都存在,所以我们需要考虑另一种更安全的方式来实现单例

单例模式——枚举

在Effective Java(强烈推荐有Java基础想要进阶提高的朋友康康)中曾提高,使用枚举来实现单例是最简洁优雅的方式.,实例如下

/**
 * @author : psyduck
 * @Date : 2022/2/21 18:29
 * @Desc : 解决懒汉单例模式线程安全的第四种方案——枚举
 * 这种方法的优点在于写法简单 从JVM保证了序列化安全和多线程安全
 **/
public enum SingletonModeLazyThreadSafeFour{

    DATEUTIL;

    public long getNow(){
        return System.currentTimeMillis();
    }
}

此时如果我们想要调用此实例的方法,可以使用如下方式

/**
 * @author : psyduck
 * @Date : 2022/2/20 11:11
 * @Desc :用于设计模式测试的测试类
 **/
public class DesignModeTest{
    public static void main(String[] args) throws Exception{
        SingletonModeLazyThreadSafeFour.DATEUTIL.getNow();
    }
}

在这种情况下,不仅代码更加简洁,同时保证了多线程安全,且实现了懒加载,只有在调用枚举中的方法时才会进行枚举类的加载,还考虑到了反射和反序列化攻击,因为枚举无法通过setAccessible抑制对应类的安全检查,也就无法访问枚举的私有成员变量或方法来另外创建额外的对象.
不过此方案在某些场景下无法使用,即此类需要继承其他类的情况下,因为枚举无法继承其他类,所以我们需要根据相应场景,来选择上方的其他单例实现进行应用

最后更新于2022年2月22日
原创不易,如果该文章对你有所帮助,望左上角点击关注~如有任何技术相关问题,可通过评论联系我讨论,我会在力所能及之内进行相应回复以及开单章解决该问题.

该文章如有任何错误请在评论中指出,感激不尽,转载请附出处!
个人博客首页:https://blog.csdn.net/yjrguxing ——您的每个关注和评论都对我意义重大

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值