单例概述
单例是需要在内存中永远只能创建一个类的实例
单例的作用:节约内存和保证共享计算的结果正确,以及方便管理。
单例模式的适用场景:
- 全局信息类:例如任务管理器对象,或者需要一个对象记录整个网站的在线流量等信息。
- 无状态工具类:类似于整个系统的日志对象等,我们只需要一个单例日志对象负责记录,管理系统日志信息。
单例模式有8种
单例模式我们可以提供出8种写法,有很多时候我们存在饿汉式 单例的概念,以及懒汉式单例的概念。
饿汉式单例的含义是:在获取单例对象之前对象已经创建完成了。
懒汉式单例是指:在真正需要单例的时候才创建出该对象。
饿汉单例的2种写法
特点:在获取单例对象之前对象已经创建完成了。
饿汉式(静态常量)
java
/**
* 饿汉式(静态常量)
*/
public class Singleton1 {
private static final Singleton1 INSTANCE = new Singleton1();
private Singleton1() {
}
public static Singleton1 getInstance() {
return INSTANCE;
}
}
饿汉式(静态代码块)
java
/**
* 饿汉式(静态代码块)(可用)
*/
public class Singleton2 {
private final static Singleton2 INSTANCE;
static {
INSTANCE = new Singleton2();
}
private Singleton2() {
}
public static Singleton2 getInstance() {
return INSTANCE;
}
}
懒汉式单例4种写法
特点:在真正需要单例的时候才创建出该对象。在Java程序中,有时候可能需要推迟一些高开销对象的初始化操作, 并且只有在使用这些对象的时候才初始化,此时,程序员可能会采用延迟初始化。
值得注意的是:要正确的实现线程安全的延迟初始化
还是需要一些技巧的,否则很容易出现问题。
懒汉式(线程不安全)
java
/**
* 描述: 懒汉式(线程不安全,不推荐的方案)
*/
public class Singleton3 {
private static Singleton3 instance;
private Singleton3() {
}
public static Singleton3 getInstance() {
if (instance == null) {
instance = new Singleton3();
}
return instance;
}
}
懒汉式(线程安全,性能差)
分析
使用synchronized关键字修饰方法包装线程安全,但性能差多,并发下只能有一个线程正在进入获取单例对象。
案例
java
/**
* 描述: 懒汉式(线程安全 ,性能差,不推荐)
*/
public class Singleton4 {
private static Singleton4 instance;
private Singleton4() {
}
public synchronized static Singleton4 getInstance() {
if (instance == null) {
instance = new Singleton4();
}
return instance;
}
}
懒汉式(线程不安全)
特点:是一种优化后的似乎线程安全的机制。
java
/**
* 描述: 懒汉式(线程不安全 ,不推荐)
*/
public class Singleton5 {
private static Singleton5 instance;
private Singleton5() {
}
public static Singleton5 getInstance() {
// 性能得到了优化,但是依然不能保证第一次获取对象的线程安全!
if (instance == null) {
// A , B
synchronized (Singleton5.class) {
instance = new Singleton5();
}
}
return instance;
}
}
懒汉式(volatile双重检查模式,推荐)
案例代码
java
/**
* 描述: 双重检查,推荐面试中进行使用。
*/
public class Singleton6 {
// 静态属性,volatile保证可见性和禁止指令重排序
private volatile static Singleton6 instance = null;// 私有化构造器
private Singleton6() {
}
public static Singleton6 getInstance() {
// 第一重检查锁定
if (instance == null) {
// 同步锁定代码块
synchronized (Singleton6.class) {
// 第二重检查锁定
if (instance == null) {
// 注意:非原子操作
instance = new Singleton6();
}
}
}
return instance;
}
}
分析
双重检查的优点:线程安全,延迟加载,效率较高!
以上是否就可以了呢,答案是否定的,实际上我们还需要加上volatile修饰,为何要使用volatile保证安全?
1、禁止指令重排序
对象实际上创建对象要进过如下几个步骤
a. 分配内存空间。
b. 调用构造器, 初始化实例。
c. 返回地址给引用
所以,new Singleton()是一个非原子操作,编译器可能会重排序【构造函数可能在整个对象初始化完成前执行 完毕,即赋值操作(只是在内存中开辟一片存储区域后直接返回内存的引用)在初始化对象前完成】。而线程B在线程A赋值完时判断instance就不为null了,此时B拿到的将是一个没有初始化完成的半成品。这样是很危险的。因为极有可能线程B会继续拿着个没有初始化的对象中的数据进行操作,此时容易触发"NPE异常"
图解如下

2、保证可见性。
- 由于可见性问题,线程A在自己的工作线程内创建了实例,但此时还未同步到主存中;此时线程B在主存中判断 instance还是null,那么线程B又将在自己的工作线程中创建一个实例,这样就创建了多个实例。
- 如果加上了volatile修饰instance之后,保证了可见性,一旦线程A返回了实例,线程B可以立即发现Instance不为null。
静态内部类单例方式
引入:JVM在类初始化阶段(即在Class被加载后,且线程使用之前),会执行类的初始化。在执行类的初始化期 间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。
基于这个特性,可以实现另一种线程安全的延迟初始化方案
java
/**
* 描述:静态内部类方式,可用
*/
public class Singleton7 {
private Singleton7() {
}
private static class SingletonInstance {
private static final Singleton7 INSTANCE = new Singleton7();
}
// 线程 A 线程B
public static Singleton7 getInstance() {
return SingletonInstance.INSTANCE;
}
}
小结
- 静态内部类是在被调用时才会被加载,这种方案实现了懒汉单例的一种思想,需要用到的时候才去创建单例,加上 JVM的特性,这种方式又实现了线程安全的创建单例对象。
- 通过对比基于volatile的双重检查锁定方案和基于类初始化方案的对比,我们会发现基于类初始化的方案的实现代码更简洁。但是基于volatile的双重检查锁定方案有一个额外的优势 :除了可以对静态字段实现延迟加载初始化外, 还可以对实例字段实现延迟初始化。
枚举实现单例
java
/**
* 描述: 枚举单例
*/
public enum Singleton8 {
INSTANCE;
public void whateverMethod() {
}
}
**上面的双重锁校验的代码之所以很臃肿,是因为大部分代码都是在保证线程安全。**为了在保证线程安全和锁粒度之间做权衡,代码难免会写的复杂些。但是,双重锁校验还是有问题的,因为他无法解决反射和反序列化会破坏单例的问题。
枚举可解决线程安全问题
上面提到过。使用非枚举的方式实现单例,都要自己来保证线程安全,所以,这就导致其他方法必然是比较臃肿的。那么,为什么使用枚举就不需要解决线程安全问题呢?
其实,并不是使用枚举就不需要保证线程安全,只不过线程安全的保证不需要我们关心而已。也就是说,其实在"底层"还是做了线程安全方面的保证的。
那么,"底层"到底指的是什么?
定义枚举时使用enum和class一样,是Java中的一个关键字。就像class对应用一个Class类一样,enum也对应有一个Enum类。
通过将定义好的枚举反编译,我们就能发现,其实枚举在经过javac
的编译之后,会被转换成形如public final class T extends Enum
的定义。
而且,枚举中的各个枚举项同事通过static
来定义的。如:
java
public enum T {
SPRING,SUMMER,AUTUMN,WINTER;
}
反编译后代码为:
java
public final class T extends Enum
{
//省略部分内容
public static final T SPRING;
public static final T SUMMER;
public static final T AUTUMN;
public static final T WINTER;
private static final T ENUM$VALUES[];
static
{
SPRING = new T("SPRING", 0);
SUMMER = new T("SUMMER", 1);
AUTUMN = new T("AUTUMN", 2);
WINTER = new T("WINTER", 3);
ENUM$VALUES = (new T[] {
SPRING, SUMMER, AUTUMN, WINTER
});
}
}
了解JVM的类加载机制
的朋友应该对这部分比较清楚。static
类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的(因为虚拟机在加载枚举的类的时候,会使用ClassLoader的loadClass方法,而这个方法使用同步代码块保证了线程安全)。所以,创建一个enum类型是线程安全的。
也就是说,我们定义的一个枚举,在第一次被真正用到的时候,会被虚拟机加载并初始化,而这个初始化过程是线程安全的。而我们知道,解决单例的并发问题,主要解决的就是初始化过程中的线程安全问题。
所以,由于枚举的以上特性,枚举实现的单例是天生线程安全的。
枚举可避免反射破坏单例
双重锁校验被反射破坏示例代码
java
import java.lang.reflect.Constructor;
public class BreakSingleton {
public static void main(String[] args) throws Exception {
// 1. 正常获取单例实例
Singleton6 s1 = Singleton6.getInstance();
// 2. 反射破坏流程
Constructor<Singleton6> constructor = Singleton6.class.getDeclaredConstructor();
constructor.setAccessible(true); // 强制访问私有构造器
Singleton6 s2 = constructor.newInstance();
Singleton6 s3 = constructor.newInstance();
// 3. 验证结果
System.out.println("s1 == s2? " + (s1 == s2)); // false
System.out.println("s2 == s3? " + (s2 == s3)); // false
}
}
枚举 可避免反射
破坏单例原理
-
JVM底层拦截
反射调用
newInstance()
创建对象时,JDK会检查目标类是否ENUM
修饰(源码级硬编码拦截),直接抛出异常阻止实例化。 -
枚举本质特殊类
每个枚举值本质是
public static final
常量,在类加载阶段由JVM原子初始化(线程安全),不存在空实例期,从根源消除反射破坏入口。
核心:Java语言规范(JLS 8.9)明确禁止反射操作枚举类构造器。
枚举可避免反序列化破坏单例
双重锁校验被反序列化破坏示例代码
java
// 实现序列化接口(破坏前提)
public class Singleton6 implements Serializable {
// ... 原双重检查代码不变
}
// 测试类
public class BreakDemo {
public static void main(String[] args) throws Exception {
Singleton6 s1 = Singleton6.getInstance();
// 序列化
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(bos);
oos.writeObject(s1);
oos.close();
// 反序列化
ByteArrayInputStream bis = new ByteArrayInputStream(bos.toByteArray());
ObjectInputStream ois = new ObjectInputStream(bis);
Singleton6 s2 = (Singleton6) ois.readObject(); // 破坏!
System.out.println(s1 == s2); // false (单例被破坏)
}
}
解决方法:
添加 readResolve()
方法拦截反序列化:
java
public class Singleton6 implements Serializable {
// ... 原代码不变
// 反序列化防护盾
private Object readResolve() {
return getInstance(); // 始终返回真实单例
}
}
枚举 可避免反序列
破坏单例原理
-
序列化仅存储枚举名
枚举序列化时只保存枚举常量名称 (如
INSTANCE
),不存储对象状态信息。 -
反序列化执行
valueOf
重建反序列化时直接调用:
javaEnum.valueOf(Singleton.class, "INSTANCE"); // JVM内置逻辑
等同于显式调用
Singleton.INSTANCE
,永远返回唯一实例。 -
底层强制拦截新实例创建
ObjectInputStream
源码硬编码判断:javaif (cl.isEnum()) { return Enum.valueOf((Class)cl, name); // 禁止新建对象 }