你必须掌握的设计模式:单例模式

403 阅读10分钟

大家好,我是codeyang~ 请大家关注公众号codeyang,每周都会分享技术文章 😊

今天和大家分享下 设计模式创建型中最简单也是最常用的单例模式。

设计模式分类:

​ 创建型模式(Creational Patterns):创建型模式关注对象的创建机制,包括对象 的实例化、初始化和组装。常见的创建型模式包括工厂方法模式、抽象工厂模式、单 例模式、原型模式和建造者模式。

​ 结构型模式(Structural Patterns):结构型模式关注对象之间的组合和关联关系, 以形成更大的结构。常见的结构型模式包括适配器模式、桥接模式、装饰器模式、外 观模式、享元模式和代理模式。

​ 行为型模式(Behavioral Patterns):行为型模式关注对象之间的通信和交互,以 实现特定的行为和责任分配。常见的行为型模式包括策略模式、模板方法模式、观察 者模式、迭代器模式、责任链模式、命令模式、备忘录模式、解释器模式、状态模 式、访问者模式 和中介者模式。

是否还记得面试时被问起的:

给我讲讲你项目中都使用了哪些设计模式?

哎,那你给说下你是咋实现的单例?

哈哈哈😂,有没有画面~

什么是单例模式

顾名思义一个类只能有一个实例对象,多次获取得倒的都是同一个对象。

单例模式的使用场景

单例模式适用于以下场景:

  1. 资源共享:当多个对象需要共享同一资源时,可以使用单例模式来确保只有一个实例管理和提供该资源,例如数据库连接池、线程池等。
  2. 对象缓存:当需要缓存对象并在整个应用程序中共享时,可以使用单例模式。这样可以避免重复创建对象,提高性能,例如全局配置信息、日志对象等。
  3. 系统服务:某些系统级服务和管理类只需要一个实例来提供服务,例如消息队列管理器、文件系统管理器等。
  4. 全局访问点:当需要在应用程序的不同部分中访问相同的对象实例时,可以使用单例模式。这样可以避免在不同模块之间传递实例引用,简化代码逻辑。

需要注意的是,单例模式应该谨慎使用,只在确实需要全局唯一实例的情况下使用。滥用单例模式可能导致代码耦合性增加、测试困难以及隐藏的线程安全问题等。在设计时要考虑到单例模式的适用性和后续扩展的可能性。

windows系统大家可以尝试下看可以打开几个任务管理器窗口。其实任务管理器对象也是单例的

单例模式分为两类

​ 饿汉式:类加载就会创建实例。可以想象成一个人很饿的人,他的锅里不能是空的要一直有饭。

​ 懒汉式:类加载时不会立即创建实例,首次使用时才会创建实例。好比一个人肚子饿了才会去做饭

如何实现

通常实现单例模式掌握三个要点即可

  • 私有化构造方法,防止其他类直接实例化它
  • 创建实例对象赋值给成员变量
  • 提供公共的静态方法来获取对象

单例模式的实现方式很多,都有各自的特点和优劣。我们一起来看下吧~

方式1(静态变量方式-饿汉式)

使用静态变量方式实现单例模式的优点是简单且线程安全。由于单例实例在类加载时就被创建,因此不存在多线程下的竞争条件。但这也带来了一定的缺点,在程序运行期间单例实例始终存在,无法延迟实例化(懒加载),可能会导致资源浪费。

package com.yang.pattern.singleton.case01;

/**
 * @Description: 饿汉式-静态成员变量的方式
 * @Author : wy 公众号codeyang
 * @Date : Created 2023/5/21 10:27 下午
 */
public class Singleton {

    //私有化构造函数
    private Singleton() {
    }

    //在成员变量位置创建实例对象
    private static Singleton instance = new Singleton();

    //公共的获取对象的方法
    public static Singleton getInstance() {
        return instance;
    }
}

方式2 (同步代码块方式-饿汉式)

成员变量位置声明对象,在静态代码块中创建对象。类加载时就创建对象,同样存在资源浪费。

package com.yang.pattern.singleton.case02;

/**
 * @Description: 同步代码块方式-饿汉式
 * @Author : wy 公共号codeyang
 * @Date : Created 2023/5/27 4:13 下午
 */
public class Singleton {

    //私有化构造函数
    private Singleton() {

    }

    //在成员变量位置创建对象
    private static Singleton instance;

    static {
        //在静态代码块中实例化对象
        instance = new Singleton();
    }

    //公共的获取对象的方法
    public static Singleton getInstance() {
        return instance;
    }
}

方式3 (懒汉式)

这次我们在成员变量声明了对象,但没有立即创建。是把对象创建放在了公共的静态获取方法中,这样就实现了延迟加载。可以看见我们还判断了对象不为null才创建对象,但是在多线程下还是会出现创建多个对象的问题。

package com.yang.pattern.singleton.case03;

/**
 * @Description: 懒汉式-线程不安全
 * @Author : wy 公众号codeyang
 * @Date : Created 2023/5/27 4:31 下午
 */
public class Singleton {

    //私有化构造函数
    private Singleton() {
    }

    //声明对象
    private static Singleton instance;

    //公共的获取对象方法,使用时才会创建对象
    public static Singleton getInstance() {
        //对象不存在,创建对象
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }


}

线程不安全问题:

​ 多线程环境下,调用getInstance()。线程A第一个进来发现对象还未创建,正要准备创建,此时线程B来了if(instance == null) 走完发现,耶~没创建我来new 一个,但是A也创建了。这下导致两个实例对象被创建,这下就出问题了说好的只有一个对象。A和B怎么获取到的对象不一样😫

方式4(懒汉式-同步方法)

为了解决线程安全问题,在方式三基础上做点改动 getInstance() 方法加载上synchronized来确保线程安全。

该方式也实现了懒加载效果,同时又解决了线程安全问题。但是在getInstance()方法上添加了synchronized关键字,导致该方法的执行效果特别低。从代码我们可以看出,其实就是在初始化instance的时候才会出现线程安全问题,一旦初始化完成就不存在了。

package com.yang.pattern.singleton.case04;

/**
 * @Description: 懒汉式-线程安全的
 * @Author : wy 公众号codeyang
 * @Date : Created 2023/5/27 4:51 下午
 */
public class Singleton {

    //私有化构造函数
    private Singleton() {
    }

    //声明对象
    private static Singleton instance;

    //公共的获取对象的方法,使用时才会创建对象
    public synchronized static Singleton getInstance() {
        //对象不存在,创建对象
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

方式5(双重检查锁定-懒汉式)

调整加锁时机,并不时每次调用都需要加锁。只有在首次调用对象没被创建时加锁保证唯一实例。加载放在调用后new 对象前,同时加入在加锁后进行第二次判断来检查是否创建了对象,从而保证了线程安全实现单例。由此也产生了一种新的实现模式:双重检查锁模式。

双重检查锁定模式解决了单例、性能、线程安全问题,下面的代码看上去完美无缺,其实是存在问题,在多线程的情况下,可能会出现空指针问题,出现问题的原因是JVM在实例化对象的时候会进行优化和指令重排序操作。

要解决双重检查锁模式带来空指针异常的问题,只需要使用 volatile 关键字, volatile 关键字可以保证可见性和有序性。

package com.yang.pattern.singleton.case05;

/**
 * @Description: 双重检查锁定-懒汉式
 * @Author : wy 公众号codeyang
 * @Date : Created 2023/5/27 4:58 下午
 */
public class Singleton {

    //私有化构造函数
    private Singleton() {
    }

    //声明对象
    private static Singleton instance;

    //公共的获取对象的方法,使用时才会创建对象
    public static Singleton getInstance() {
        //第一次判断对象存在,直接返回
        if (instance == null) {

            synchronized (Singleton.class){
                //第二次抢到锁后再次判断是否为null
                if (instance == null)
                instance = new Singleton();
            }

        }
        return instance;
    }
}

通过使用volatile关键字修饰静态变量instance,可以确保多线程环境下,当一个线程成功创建了实例并将其写入到主内存后,其他线程可以立即感知到这个变化,避免了多个线程同时创建实例的问题。

完整代码

package com.yang.pattern.singleton.case05;

/**
 * @Description: 双重检查锁定-懒汉式
 * @Author : wy 公众号codeyang
 * @Date : Created 2023/5/27 4:58 下午
 */
public class Singleton {

    //私有化构造函数
    private Singleton() {
    }

    //声明对象
    private static volatile Singleton instance;

    //公共的获取对象的方法,使用时才会创建对象
    public static Singleton getInstance() {
        //第一次判断对象存在,直接返回
        if (instance == null) {

            synchronized (Singleton.class){
                //第二次抢到锁后再次判断是否为null
                if (instance == null)
                instance = new Singleton();
            }

        }
        return instance;
    }
}ß

方式6(静态内部类方式-懒汉式)

静态内部类只有在被使用时才会被加载,而且加载过程是线程安全的。我们在静态内部类中定义的对象变量被static修饰,保证了对象只被实例化一次并严格保证实例化顺序。当调用getInstance()方法时,会返回SingletonHolder中的INSTANCE,从而获取到单例实例

使用静态内部类实现单例模式可以避免使用同步锁或双重检查锁定等方式,简化了代码,并且能够确保延迟加载和线程安全。所以,是一种常见且推荐的单例模式实现方式。

package com.yang.pattern.singleton.case06;

/**
 * @Description: 懒汉式-静态内部类方式
 * @Author : wy 公众号 codeyang
 * @Date : Created 2023/5/27 10:54 上午
 */
public class Singleton {

    //私有化构造函数
    private Singleton() {

    }

    //静态内部类
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }

    //获取实例
    public static Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }

}

方式7(枚举实现方式-饿汉式)

枚举类是饿汉式

枚举类的实现特别简单。枚举类是线程安全的,只会被加载一次。使用枚举类方式实现单例是特别推荐的方式。

枚举类型是所用单例实现中唯一一种不会被破坏的单例实现模式,因为枚举类的实例是在类加载时创建的,并且不会被其他方式实例化。解决了可能存在的反序列化、反射攻击等问题。

package com.yang.pattern.singleton.case07;

/**
 * @Description: 枚举方式
 * @Author : wy 公众号 codeyang
 * @Date : Created 2023/5/27 3:17 下午
 */
public enum Singleton {
    INSTANCE;
}

开发中选择饿汉式还是懒汉取决于你的应用程序需求和考虑的因素,包括线程安全性、延迟初始化、性能和实例化方式等。推荐使用枚举、双重检查锁定方式。👌