单例模式
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
|
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();
} } } 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(); } }
|
单例实现原理
- 利用类加载机制保证唯一性:Java 中类的加载过程由类加载器负责,一个类只会被加载一次(在同一个类加载器体系下)。对于静态内部类
InnerClass
,只有当它被首次主动使用(比如访问它的静态成员变量或者静态方法等情况)时,才会触发类加载过程,进而初始化 HOLDER
这个静态常量,创建 Holder
类的实例。这就保证了不管在什么情况下,整个应用程序中 Holder
类的实例最多只会被创建一次,实现了单例的要求。
- 实现延迟加载(懒加载):与饿汉式单例(在类定义时就直接创建实例)不同,基于静态内部类的这种单例模式只有在真正调用
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()); } }
|
- 基于枚举的特性:在 Java 中,枚举类型本质上是一种特殊的类,它的实例是有限且固定的。当定义一个枚举类型时,枚举常量(如上述代码中的
INSTANCE
)就是这个枚举类的实例,并且这些实例在类被加载时就会被实例化,且只会有这一次实例化过程。
- 线程安全保障:由于枚举类的加载和实例化是由 Java 语言本身机制来保证的,它天生就具备线程安全性。Java 虚拟机会确保在多线程环境下,枚举类的初始化以及其枚举常量对应的实例创建过程是原子性的,不会出现多个线程导致创建多个实例的情况,这就天然符合单例模式中对实例唯一性的要求。
- 防止反射和序列化破坏单例:普通的单例模式实现方式(如前面提到的懒汉式、饿汉式等)可能会面临通过反射机制来调用私有构造函数创建新实例,或者在进行序列化和反序列化操作后得到多个不同实例的问题。而枚举类实现单例则不存在这些隐患,Java 规范中明确禁止通过反射来创建枚举类的新实例,并且在序列化和反序列化时,Java 会保证始终返回同一个枚举实例,维护了单例的完整性。