设计模式之单例模式

8/24/2021 设计模式创建型模式

# 单例模式的定义与特点✨

单例(Singleton)模式的定义:指一个类只有一个实例,且该类能自行创建这个实例的一种模式。

实际应用

单例模式使用最多的就是在Spring的IOC容器中,对于Bean的管理,默认都是单例。一个bean只会创建一个对象,存在内置map中,之后无论获取多少次该bean,都返回同一个对象。

# 单例模式有 3 个特点🧨

1、单例类只有一个实例对象;

2、该单例对象必须由单例类自行创建;

3、单例类对外提供一个访问该单例的全局访问点

# 单例模式的优点🎋

1、单例模式可以保证内存里只有一个实例,减少了内存的开销。

2、可以避免对资源的多重占用。

3、单例模式设置全局访问点,可以优化和共享资源的访问。

# 单例模式的缺点🎋

1、单例模式一般没有接口,扩展困难。如果要扩展,则除了修改原来的代码,没有第二种途径,违背开闭原则。

2、在并发测试中,单例模式不利于代码调试。在调试过程中,如果单例中的代码没有执行完,也不能模拟生成一个新的对象。

3、单例模式的功能代码通常写在一个类中,如果功能设计不合理,则很容易违背单一职责原则。

# 代码实现🎟

# 1、饿汉模式

/**
 * @version 1.0.0
 * @className: HungrySingleton
 * @description: 饿汉模式
 * 该模式的特点是类一旦加载就创建一个单例,保证在调用 getInstance 方法之前单例已经存在了。
 * 饿汉式单例在类创建的同时就已经创建好一个静态的对象供系统使用,以后不再改变,所以是线程安全的,可以直接用于多线程而不会出现问题。
 * @author: LiJunYi
 * @create: 2021/5/27 15:05
 */
public class HungrySingleton
{
    // 通过private static变量持有唯一实例,保证全局唯一性;
    private static final HungrySingleton instance = new HungrySingleton();

    // 使用private私有化构造方法,确保外部无法实例化;
    private HungrySingleton() 
    {
    }
    
    //通过public static方法返回此唯一实例,使外部调用方能获取到实例。
    public static HungrySingleton getInstance() {
        return instance;
    }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25

# 1.2、静态内部类

静态内部类,是基于饿汉式模式下的优化。

第一次加载Singleton类时不会初始化instance,只有在第一次调用getInstance()方法时,虚拟机才会加载SingletonHolder类,初始化instance。

instance 的唯一性、创建过程的线程安全性,都由 JVM 来保证。

这种方式既保证线程安全,单例对象的唯一,也延迟了单例的初始化,推荐使用这种方式来实现单例模式。

/**
 * 静态内部类实现单例模式
 */
public class Singleton {
  private Singleton() {
  }
 
  public static Singleton getInstance() {
    return SingletonHolder.instance;
  }
 
  /**
     * 静态内部类
     */
  private static class SingletonHolder {
    private static Singleton instance = new Singleton();
  }
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19

# 静态内部类单例优点:

  • 对象的创建是线程安全的。

  • 支持延时加载。

  • 获取对象时不需要加锁。

  • 这是一种比较常用的模式之一。

# 2、懒汉模式

/**
 * @version 1.0.0
 * @className: LazySingleton
 * @description: 懒汉式单例(饱汉模式)
 * DCL双重检查式单例
 * 该模式的特点是类加载时没有生成单例,只有当第一次调用 getlnstance 方法时才去创建这个单例。
 * 注意:如果编写的是多线程程序,则不要删除代码中的关键字 volatile 和 synchronized,否则将存在线程非安全的问题。
 * 如果不删除这两个关键字就能保证线程安全,但是每次访问时都要同步,会影响性能,且消耗更多的资源,这是懒汉式单例的缺点。
 * @author: LiJunYi
 * @create: 2021/5/27 15:00
 */
public class LazySingleton
{
    /**
     * 保证 instance 在所有线程中同步
     * 第二层锁,volatile关键字禁止指令重排
     */
    private static volatile LazySingleton instance = null;

    /**
     * private 避免类在外部被实例化
     */
    private LazySingleton() {
    }

    /**
     * 获得实例
     * getInstance 方法前加同步
     * @return {@link LazySingleton}
     */
    public static LazySingleton getInstance() 
    {
        //第一层检查,检查是否有引用指向对象,高并发情况下会有多个线程同时进入
        if (instance == null)
        {
            //第一层锁,保证只有一个线程进入
            //双重检查,防止多个线程同时进入第一层检查(因单例模式只允许存在一个对象,故在创建对象之前无引用指向对象,所有线程均可进入第一层检查)
            //当某一线程获得锁创建一个LazySingleton对象时,即已有引用指向对象,instance不为空,从而保证只会创建一个对象
            //假设没有第二层检查,那么第一个线程创建完对象释放锁后,后面进入对象也会创建对象,会产生多个对象
            synchronized(LazySingleton.class)
            {
                //volatile关键字作用为禁止指令重排,保证返回LazySingleton对象一定在创建对象后
                if (instance == null)
                {
                    instance = new LazySingleton();
                }
            }
        }
        return instance;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

# 3、基于枚举实现单例

用枚举来实现单例,是最简单的方式。这种实现方式通过Java枚举类型本身的特性,保证了实例创建的线程安全性和实例的唯一性。

public enum SingletonEnum {
 
    INSTANCE;
 
    public void execute(){
        System.out.println("begin execute");
    }
 
    public static void main(String[] args) {
        SingletonEnum.INSTANCE.execute();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12

基于枚举实现单例会发现它并不需要前面描述的几个操作

  • 构造方法私有化

  • 实例化的变量引用私有化

  • 获取实例的方法共有

这类的方式实现枚举其实并不保险,因为私有化构造并不能抵御反射攻击