天无二日,国无二主
定义
确保一个类只有一个实例,并提供一个全局的访问点。
使用场景
只需要有一个对象:线程池、数据库连接池、缓存、日志对象、处理偏好设置和注册表对象、任务管理器等
特点
- 优点:
- 节省内存空间
- 避免频繁创建销毁对象,减轻GC工作,提高性能
- 为整个系统提提供了一个全局访问点
- 缺点:
- 不适用于频繁变化的对象
- 扩展困难
实现
饿汉式
1
2
3
4
5
6
7
8
9
10
public class HungrySingleton {
private static HungrySingleton hungrySingleton = new HungrySingleton();
public HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
线程安全
懒汉式(Double-check Locking)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LazySingleton {
private static volatile LazySingleton lazySingleton = null;
public LazySingleton() {
}
public static LazySingleton getInstance() {
if (lazySingleton == null) {
synchronized (LazySingleton.class) {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
}
}
return lazySingleton;
}
}
保证线程安全的原理:
双重判空
:第一次判空,如果实例已存在,则直接返回实例,提升性能;第二次判空:当多个线程一起达到锁位置时,待第一个线程创建对象完成释放锁,其他线程竞争到锁后,判空直接返回实例
synchronized
:保证需要执行创建对象的线程只能有一个
volatile
:可见性、禁止指令重排序
- 可见性:volatile修饰的变量的修改对其他任何线程都是可见的,两次判空依赖该特性
- 禁止指令重排序:通过在创建对象的指令前后添加内存屏障来禁止指令冲排序
new关键字创建对象不是原子的,需要三步
1.在堆内存开辟内存空间
2.调用构造方法,初始化对象
3.引用变量指向内存空间
为了提高性能,JVM虚拟机常常会对既定的代码执行顺行进行指令重排序,经过指令重排序后,创建对象的顺序可能1、2、3,也有可能1、3、2。当执行顺序是1、3、2时,执行到3(引用变量指向内存空间)时,另一个线程执行if (lazySingleton == null)
时,会拿到没有初始化的非NUll实例,从而出现著名的DCL失效问题。
静态内部类(Static-Inner)
1
2
3
4
5
6
7
8
9
10
11
12
13
public class StaticInnerSingleton {
public StaticInnerSingleton() {
}
public static StaticInnerSingleton getInstance() {
return InnerClass.INSTANCE;
}
private static class InnerClass {
private static final StaticInnerSingleton INSTANCE = new StaticInnerSingleton();
}
}
优点:懒加载、线程安全、效率较高、实现简单
静态内部类是怎么实现懒加载的?
一个类从被加载到虚拟机中开始,到卸载出内存为止,它的整个生命周期将经历加载、验证、准备、解析、初始化、使用、卸载七个阶段,其中验证、准备、解析统称为连接。
对于类初始化阶段,《Java虚拟机规范》严格规定了有且只有六种情况必须立即对类进行初始化:
- 遇到new、getstatic、putstatic或invokestatic这四条字节码指令时,如果类型没有进行过初始化,则需要先触发其初始化阶段。能够生成这四条指令的典型Java代码场景有:
- 使用new关键字实例化对象的时候
- 读取或设置一个类型的静态字段(被final修饰、已在编译期把结果放入常量池的静态字段除外)的时候
- 调用一个类型的静态方法的时候
- 使用
java.lang.reflect
包的方法对类型进行反射调用的时候,如果类型没有进行过初始化,则需要先触发其初始化 - 当初始化类的时候,如果发现其父类还没有进行过初始化,则需要先触发其父类的初始化
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的那个类),虚拟机会先初始化这个主类
- 当使用JDK 7新加入的动态语言支持时,如果一个
java.lang.invoke.MethodHandle
实例最后的解析结果为REF_getStatic
、REF_putStatic
、REF_invokeStatic
、REF_newInvokeSpecial
四种类型的方法句柄,并且这个方法句柄对应的类没有进行过初始化,则需要先触发其初始化 - 当一个接口中定义了JDK8新加入的默认方法(被default关键字修饰的接口方法)时,如果有这个接口的实现类发生了初始化,那该接口要在其之前被初始化
当getInstance()
方法被调用时,InnerClass
才在StaticInnerSingleton
的运行时常量池里,把符号引用替换为直接引用,这时静态对象INSTANCE
也真正被创建,然后再被getInstance()
方法返回出去,这点同饿汉模式。
静态内部类是如何保证线程安全的?
Java虚拟机必须保证一个类的
枚举(Enum)
1
2
3
4
5
6
7
public enum EnumSingleton {
INSTANCE;
public void exec() {
System.out.println("exec...");
}
}
优点:简单,高效,线程安全,可以避免通过反射破坏枚举单例
先编译再反编译EnumSingleton
类:
1
2
javac EnumSingleton.java
javap EnumSingleton
结果:
1
2
3
4
5
6
7
8
Compiled from "EnumSingleton.java"
public final class com.neil.parent.EnumSingleton extends java.lang.Enum<com.neil.parent.EnumSingleton> {
public static final com.neil.parent.EnumSingleton INSTANCE;
public static com.neil.parent.EnumSingleton[] values();
public static com.neil.parent.EnumSingleton valueOf(java.lang.String);
public void exec();
static {};
}
从枚举的反编译结果可以看到:
INSTANCE
被static final
修饰,所以可以通过类名直接调用- 创建对象的实例是在静态代码块中创建的,因为
static
类型的属性会在类被加载之后被初始化,当一个Java类第一次被真正使用到的时候静态资源被初始化、Java类的加载和初始化过程都是线程安全的,所以创建一个enum类型是线程安全的。
参考:《Head First 设计模式》、《深入理解Java虚拟机》