单例模式

  • 懒汉式单例
1
2
3
4
5
6
7
8
9
10
public class Hunger {
//构造方法私有
private Hunger(){}
//私有静态变量
private static final Hunger INSTANCE = new Hunger();

public static Hunger getInstance(){
return INSTANCE;
}
}

这种单例模式消耗资源,占用内存

  • 饿汉式单例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Lazy {
private Lazy() {
System.out.println(Thread.currentThread().getName() + "ok");
}

private static Lazy INSTANCE;

public static Lazy getInstance() {
if (INSTANCE == null) {
INSTANCE = new Lazy();
}
return INSTANCE;
}
}

上述代码存在问题,如果多线程并发,会有多个线程同时执行到判断对象是否为null,多次调用构造

1
2
3
4
5
6
7
8
9
10
11
12
13
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
Lazy.getInstance();
}).start();
}
}


Thread-0ok
Thread-3ok
Thread-2ok
Thread-1ok
  • DCL(双重检测懒汉式单例)
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
public class DCL {
private DCL(){
System.out.println(Thread.currentThread().getName()+"ok");
}

private volatile static DCL INSTANCE;

public static DCL getInstance(){
if (INSTANCE==null){
synchronized (DCL.class){
if (INSTANCE==null){
INSTANCE = new DCL();//不是一个原子操作
/**
* 1.分配内存空间
* 2.执行构造方法,初始化对象
* 3.把对象指向这个内存空间
*
* 132 A
* 在3这一步线程b来了,发现对象!=null直接返回了没有初始化的对象
* 避免发生指令重排,使用volatile
*
*/
}
}
}
return INSTANCE;
}
}

使用synchronized锁定整个类,和实例对象无关,全局只有一份

  • 静态内部类
1
2
3
4
5
6
7
8
9
10
11
public class Holder {
private Holder(){}

public static Holder getInstance(){
return InnerClass.HOLDER;
}

public static class InnerClass{
private static final Holder HOLDER = new Holder();
}
}

单例实现原理

  1. 利用类加载机制保证唯一性:Java 中类的加载过程由类加载器负责,一个类只会被加载一次(在同一个类加载器体系下)。对于静态内部类 InnerClass,只有当它被首次主动使用(比如访问它的静态成员变量或者静态方法等情况)时,才会触发类加载过程,进而初始化 HOLDER 这个静态常量,创建 Holder 类的实例。这就保证了不管在什么情况下,整个应用程序中 Holder 类的实例最多只会被创建一次,实现了单例的要求。
  2. 实现延迟加载(懒加载):与饿汉式单例(在类定义时就直接创建实例)不同,基于静态内部类的这种单例模式只有在真正调用 getInstance 方法,进而触发对 InnerClass.HOLDER 的访问,导致 InnerClass 类加载时才会创建实例。如果在整个程序运行过程中一直没有调用 getInstance 方法去获取单例实例,那么 Holder 类的实例就不会被创建,节省了内存资源等,实现了懒加载的特性。
  • 枚举类实现单例

枚举类本身就是class,继承enum接口

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
public enum SingletonEnum {
INSTANCE;

// 可以在这里添加单例对象需要的属性和方法
private String someProperty;

public String getSomeProperty() {
return someProperty;
}

public void setSomeProperty(String value) {
this.someProperty = value;
}
}

class A{
public static void main(String[] args) {
SingletonEnum instance1 = SingletonEnum.INSTANCE;
SingletonEnum instance2 = SingletonEnum.INSTANCE;

// 比较两个实例是否为同一个
System.out.println(instance1 == instance2);

instance1.setSomeProperty("test value");
System.out.println(instance2.getSomeProperty());
}
}
  1. 基于枚举的特性:在 Java 中,枚举类型本质上是一种特殊的类,它的实例是有限且固定的。当定义一个枚举类型时,枚举常量(如上述代码中的INSTANCE)就是这个枚举类的实例,并且这些实例在类被加载时就会被实例化,且只会有这一次实例化过程。
  2. 线程安全保障:由于枚举类的加载和实例化是由 Java 语言本身机制来保证的,它天生就具备线程安全性。Java 虚拟机会确保在多线程环境下,枚举类的初始化以及其枚举常量对应的实例创建过程是原子性的,不会出现多个线程导致创建多个实例的情况,这就天然符合单例模式中对实例唯一性的要求。
  3. 防止反射和序列化破坏单例:普通的单例模式实现方式(如前面提到的懒汉式、饿汉式等)可能会面临通过反射机制来调用私有构造函数创建新实例,或者在进行序列化和反序列化操作后得到多个不同实例的问题。而枚举类实现单例则不存在这些隐患,Java 规范中明确禁止通过反射来创建枚举类的新实例,并且在序列化和反序列化时,Java 会保证始终返回同一个枚举实例,维护了单例的完整性。