# 单例模式

本文作者:程序员飞云

本站地址:https://www.flycode.icu (opens new window)

# 单例模式(Singleton)

保证一个类只有一个实例,并且能够提供一个访问它的全局访问点。避免创建多次实例,节省内存空间。

# 1. 懒汉模式----线程不安全

先看一下最简单代码

public class Singleton {
    private static Singleton singleton;
	// 防止外界手动new实例化的可能
    private Singleton() {
    }

    public static Singleton getInstance() {
        if (singleton == null) {
            singleton = new Singleton();
        }
        return singleton;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

既然是单例模式,只有一个实例,可以编写下相应的测试代码正确

public class Main {
    public static void main(String[] args) {
        Singleton singleton1 = Singleton.getInstance();
        Singleton singleton2 = Singleton.getInstance();
        System.out.println(singleton1 == singleton2);  // 输出 true
    }
}
1
2
3
4
5
6
7

image-20231231183639699

运行结果true也是认为是同一个实例。

仔细思考下上面代码如果在多线程情况下会出现什么问题,很明显,线程之前会抢占资源,多线线程同时访问这个实例,实例此时都是null,都能过if条件,会创建多个实例,那就违背了单例的特性。

# 2. 懒汉式----单锁校验,线程安全

这个方式最简单,只需要在方法上面加上锁就可以

public static synchronized Singleton getInstance()  {
    if (singleton == null) {
        singleton = new Singleton();
    }
    return singleton;
}
1
2
3
4
5
6

能够保证只有一个线程能够进入方法执行,只会产生一个实例,但是每次调用getInstance都需要先获取锁,然后再释放锁,无疑是会浪费一定的性能。这里锁加在方法上,而不是实例,最根本的原因是目前线程不知道有没有创建过实例。

# 3. 双锁校验—–线程安全

上面的突破口在于其他线程怎么知道是否创建过实例,这个可以通过一个关键字volatile保证。

volatile关键字能够保证多线程之间变量的可见性,这个可以看一下JavaGuide (opens new window)的一张图

主要就是每次线程对数据进行修改后,它会将这个修改的记录同步至主内存,然后主内存也修改,其他的线程也就能够获取到最新的数据。

详细代码如下。

public class Singleton {
    // 多线程之间变量可见
    private static volatile Singleton singleton;

    private Singleton() {
    }
 
    public static Singleton getInstance() {
        // 判断实例是否为空
        if (singleton == null) {
            // 锁住当前实例
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  1. 第一个if是用于判断实例是否存在,只有实例为空的时候才能进入下一步
  2. synchronized是防止多个线程同时进入创建实例
  3. 第二个if也是用于判断实例是否存在,但是区别在于多线程的环境下,可能有多个线程同时调用getInstance,而他们的实例都是null,很容易突破第一个if判断,线程依次等待进入锁创建实例,第一个线程创建完实例,第二个线程也要进入锁来创建实例,很明显会出现问题

这样也就大大减少了获取锁以及释放锁的时间,提升了性能。

为什么又叫懒汉式,因为它只有在第一次被引用的时候,才会将自己实例化

# 4. 饿汉式单例----静态初始化,线程安全

上面这个方式依然会有一点占用性能,所以还有一个更好的方法

public class Singleton {
    private static  Singleton singleton = new Singleton();
    // 私有构造函数,防止外部直接创建实例
    private Singleton() {
    }

    public static  Singleton getInstance() {
        return singleton;
    }
}
1
2
3
4
5
6
7
8
9
10

因为构造方法是私有的,除了类本身其他类无法实例化Singleton,所以可以在自己被加载的时候就实例化,需要提前占用部分系统资源,所以称为饿汉式。

# 5. 静态内部类----线程安全

在类加载的过程中,静态内部类不会被加载,只有在调用 getInstance 方法时,Holder 类才会被加载,从而实例化 Singleton

这种方式的优点是简单且线程安全,而且不需要使用 synchronized 关键字。

public class Singleton {
    // 私有构造函数,防止外部直接创建实例
    private Singleton() {
    }

    public static Singleton getInstance() {
        return Holder.INSTANCE;
    }

    private static class Holder {
        // 在静态内部类中初始化单例实例
        private static final Singleton INSTANCE = new Singleton();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

# 6. 枚举单例模式----线程安全

这个好像是目前最佳的方法,反射安全,序列化安全,实现简单等等,因为前面几个方式都是使用到了构造方法,而我们知道通过反射就能动态的创建类,获取类的内部信息,操作类等等,所以相对而言构造方法并不是台安全的。反射在通过newInstance创建对象时,会检查该类是否ENUM修饰

具体可以查看这篇博客https://cloud.tencent.com/developer/article/1497592 (opens new window)

public enum Singleton {
   INSTANCE;
    public void method() {
        System.out.println("我是一个单例!");
    }
}
1
2
3
4
5
6
public static void main(String[] args)  {
    Singleton singleton1 = Singleton.INSTANCE;
    Singleton singleton2 = Singleton.INSTANCE;
    System.out.println(singleton1 == singleton2);  // 输出 true
}
1
2
3
4
5

# 反射测试

先测试一下反射,来测试下是否可行,是否真的有效果

# 静态初始化

public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
    Singleton singleton = Singleton.getInstance();

    Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
    constructor.setAccessible(true);
    // 使用空构造函数new一个实例。即使它是private的
    Singleton rSingleton = constructor.newInstance();

    System.out.println(singleton);
    System.out.println(rSingleton);
    System.out.println(singleton == rSingleton); // false
}
1
2
3
4
5
6
7
8
9
10
11
12

很显然,出现了不是同一个实例的问题,静态初始化没办法防御反射

image-20231231202023686

# 枚举单例

public class Main {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        Singleton singleton = Singleton.INSTANCE;
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        // 使用空构造函数new一个实例。即使它是private的
        Singleton rSingleton = constructor.newInstance();

        System.out.println(singleton);
        System.out.println(rSingleton);
        System.out.println(singleton == rSingleton); // false
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13

image-20231231195711747

运行报错,我这边也是翻了下enum的源码,enum是有一个构造方法

image-20231231200700192

那么可以修改下上面的测试代码,添加相应的构造类型

Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(String.class,int.class);
1

然后依然是报错

image-20231231200900033

根据报错信息不能创建一个enum的反射,然后查看了对应的错误位置,发现了以下代码

if ((clazz.getModifiers() & Modifier.ENUM) != 0)
    throw new IllegalArgumentException("Cannot reflectively create enum objects");
ConstructorAccessor ca = constructorAccessor;   // read volatile
1
2
3

只要检测到是枚举值的时候就会报错,所以是能够防御反射的。

# 序列化测试

# 静态初始化

需要实现Serializable接口

image-20231231205853108

Spring里面自带了一个SerializationUtils实现序列化与反序列化的工具

public static void main(String[] args) {
    Singleton singleton = Singleton.getInstance();
    System.out.println(singleton);
    // 进行序列化和反序列化操作
    byte[] bytes = SerializationUtils.serialize(singleton);

    Object deserialize =  SerializationUtils.deserialize(bytes);
    System.out.println(deserialize);
    System.out.println(deserialize == singleton);
}
1
2
3
4
5
6
7
8
9
10

结果很明显,不是同一个实例了

image-20231231205949363

# 枚举单例

public static void main(String[] args) {
    Singleton singleton = Singleton.INSTANCE;
    System.out.println(singleton);
    // 进行序列化和反序列化操作
    byte[] bytes = SerializationUtils.serialize(singleton);

    Object deserialize =  SerializationUtils.deserialize(bytes);
    System.out.println(deserialize);
    System.out.println(deserialize == singleton);
}
1
2
3
4
5
6
7
8
9
10

运行结果显示都是同一个单例所以,也是安全的

image-20231231210151017

# 单例模式的优点

  1. 节省系统资源:只创建了一个实例
  2. 简化了对象访问:只提供了一个获取实例的方法
  3. 饿汉式单例模式:类加载的时候创建单例对象,缺点是不支持延迟加载
  4. 懒汉式单例模式:只有第一次使用的时候会创建实例,缺点是多线程安全
  5. 双锁校验单例模式:在第一次使用时创建实例,缺点是仍然有一点性能问题
  6. 枚举单例模式:使用简单,能够防御反射,序列化的问题,推荐使用。

# 使用场景

  1. 数据库连接池不会反复创建
  2. 配置文件管理器
  3. Spring里面的ApplicationContext提供了一个全局访问点,保证只有一个bean
  4. 个人项目:飞云代码生成器里面就使用到了单例模式,在读取json形式的meta数据的时候,只需要读取一次就可以,然后其他的里面字段信息通过提供的一个接口进行获取,有效减少了资源的消耗。
最近更新: 1/14/2025, 1:13:36 AM
飞云编程   |