恶汉式
线程安全,调用率高,但是不能延时加载,类初始化时,立即加载这个对象
public class Demo01 {
private static Demo01 instance = new Demo01();
private Demo01() { }
public static Demo01 getInstance() {
return instance;
}
}
懒汉式 和 DCL(✨)
可以延时加载,存在线程问题,可以加锁,并且为了兼顾效率,再加一次判断,减少判断锁的次数,即双重锁定检查(DCL,Double Check Lock)。
public class Single {
private static Single instance;
private Single() { }
public static Single getInstance() {
if(instance == null){ // 线程二检测到instance不为空
synchronized(Single.class){
if (instance == null) {
//线程一被指令重排,先执行了赋值,但还没执行完构造函数(即未完成初始化)
instance = new Single();
}
}
}
// 后面线程二执行时将引发:对象尚未初始化错误
return instance;
}
}
看样子已经达到了要求,除了第一次创建对象之外,其它的访问在第一个 if 中就返回了,因此不会走到同步块中,已经完美了吗?
如上代码段中的注释:假设线程一执行到instance = new Singleton()
这句,这里看起来是一句话,但实际上其被编译后在 JVM 执行的对应会变代码就发现,这句话被编译成8条汇编指令,大致做了三件事情:
- 给 instance 实例分配内存,属性赋值默认值;
- 初始化 instance 的构造器,属性赋值为初始值;
- 将 instance 对象指向分配的内存空间(注意到这步时 instance 就非 null 了)
如果指令按照顺序执行倒也无妨,但 JVM 为了优化指令,提高程序运行效率,允许指令重排序。如此,在程序真正运行时以上指令执行顺序可能是这样的:
- 给 instance 实例分配内存;
- 将 instance 对象指向分配的内存空间(这步之后为非 null)
- 初始化 instance 的构造器
此时,当线程一执行第2步完毕,在执行第3步前,被切换到线程二上,此时对于线程二,instance 判断为非空,线程二直接来到return instance
语句,拿走 instance 然后使用,就自然会报错(对象未初始化)。
具体来说就是 synchronized 虽然保证了线程的原子性(即 synchronized 块中的语句要么全部执行,要么一条也不执行),但单条语句编译后形成的指令并不是一个原子操作(即可能该条语句的部分指令未得到执行,就被切换到另一个线程了)。
解决这个问题的方法是:使用 volatile 禁止指令重排序。
synchronized 锁 + DCL(Double Check Lock)+ volatile
public class Singleton {
private volatile static Singleton instance = null;
private Single() { }
public static Singleton getInstance() {
if(null == instance) {
synchronized (Singleton.class) {
if(null == instance) {
instance = new Singleton();
}
}
}
return instance;
}
}
将变量 instance 使用 volatile
修饰 即可实现单例模式的线程安全。
静态内部类
优点
- 基于类初始化,线程安全、调用率高,能延时加载。
- 外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化 instance,故而不占内存
缺点:
- 由于是静态内部类的形式去创建单例的,故外部无法传递参数进去,例如 Context 这种参数,所以,我们创建单例时,可以在静态内部类与 DCL 模式里自己斟酌。
该解决方案的根本就在于:利用 classloder
的机制来保证初始化 instance 时只有一个线程。JVM 在类初始化阶段会获取一个锁,这个锁可以同步多个线程对同一个类的初始化。
public class Demo03 {
private Demo03() { }
private static class SingletonClassHolder {
private static final SingletonClassHolder instance = new SingletonClassHolder();
}
public static SingletonClassHolder getInstacne() {
return SingletonClassHolder.instance;
}
}
虚拟机会保证一个类的
类加载时机:JAVA 虚拟机在有且仅有的 5 种场景下会对类进行初始化。
- 遇到
new
、getstatic
、setstatic
或者invokestatic
这4个字节码指令时。对应的 java 代码场景为:- new 一个关键字或者一个实例化对象时
- 读取或设置一个静态字段时( final 修饰、已在编译期把结果放入常量池的除外)
- 调用一个类的静态方法时。
- 使用
java.lang.reflect
包的方法对类进行反射调用时,如果类没进行初始化,需要先调用其初始化方法进行初始化。 - 当初始化一个类时,如果其父类还未进行初始化,会先触发其父类的初始化。
- 当虚拟机启动时,用户需要指定一个要执行的主类(包含main()方法的类),虚拟机会先初始化这个类。
- 当使用 JDK 1.7 等动态语言支持时,如果一个 java.lang.invoke.MethodHandle 实例最后的解析结果 REF_getStatic、REF_putStatic、REF_invokeStatic 的方法句柄,并且这个方法句柄所对应的类没有进行过初始化,则需要先触发其初始化。
这 5 种情况被称为是类的主动引用,注意,这里《虚拟机规范》中使用的限定词是”有且仅有”,那么,除此之外的所有引用类都不会对类进行初始化,称为被动引用。静态内部类就属于被动引用的行列。
所以当 getInstance()
方法被调用时,SingleTonHoler 才在 SingleTon 的运行时常量池里,把符号引用替换为直接引用,这时静态对象 INSTANCE 也真正被创建,然后再被 getInstance()
方法返回出去,这点同饿汉模式
枚举单利
线程安全、调用率高,推荐使用
@ThreadSafe
@Recommend
public class Demo4 {
// 私有构造函数
private Demo4() {
}
public static Demo4 getInstance() {
return Singleton.INSTANCE.getInstance();
}
private enum Singleton {
INSTANCE;
private Demo4 singleton;
// JVM保证这个方法绝对只调用一次
Singleton() {
singleton = new Demo4();
}
public Demo4 getInstance() {
return singleton;
}
}
}