饿汉模式
public class Singleton {
private Singleton() {
// 必须是私有构造方法
}
private static Singleton instance = new Singleton();
public static Singleton getInstance() {
return instance;
}
}
上面的写法最简单最有效并且是线程安全的(由JVM保证)。但是没有实现延迟初始化。
注意:
- 上面私有的构造方法必须要有,否则JVM会自动添加默认的构造方法,这个是public的。
- 不要将成员变量 instance 设置为public的,这违反了成员私有的原则。而且 instance 成员变量必须是 static 的。如果不是 static 的,getInstance方法无法访问非静态成员。 static关键字也满足了只有一个实例的的语义。
- 成员方法必须是 static 的。否则必须要有 Singleton 对象才能调用,这是一个悖论。本来 getInstance 方法就是要拿到 Singleton 对象。
不延迟加载有什么问题?
- 如果不延迟加载的话,在 JVM 加载 Singleton 这个类时,由于 instance 是 static 的,所以会进行初始化(类加载过程中的初始化阶段),new的过程很耗时的话(可能有关于对象的其他初始化操作),应用程序启动的会很慢,降低体验的友好性。
- 如果不延迟加载的话,假设 Singleton 这个类还有其他的功能,如提供了public static void sayHello()的方法,当初次调用 sayHello 方法时,会实例化 Singleton 对象。然而,在这里可能并用不到这个对象,造成了浪费。
注意,只有当应用程序访问到 Singleton 时,类加载器才会加载 Singleton class。 - 静态的成员变量肯定是在类加载时就进行了初始化操作。
懒汉式,线程不安全
class Singleton {
private Singleton() {
//must
}
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
这段代码使用了懒加载模式,但是却存在致命的问题。当有多个线程同时调用 getInstance() 的时候,就会创建多个实例(同时判定instance == null)。也就是说在多线程下不能正常工作。
懒汉式,线程安全
public class Singleton {
private Singleton() {
//must
}
private static Singleton instance = null;
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
注意:
- instance = null; 这样即使加载了类,也不会造成创建对象的耗时。
- synchronized关键字的使用。如果没有 synchronized 关键字,在高并发情况下,会new出多个instance实例。
上面的代码实现了延迟加载。但是存在一些问题。如果 getInstance 的调用频率很高的话,每次都 synchronized 同步访问,效率岂不是很低。因此,要降低锁的力度。
双重检查锁
双重检验锁模式(double checked locking pattern),是一种使用同步块加锁的方法。程序员称其为双重检查锁,因为会有两次检查 instance == null
,一次是在同步块外,一次是在同步块内。为什么在同步块内还要再检验一次?因为可能会有多个线程一起进入同步块外的 if,如果在同步块内不进行二次检验的话就会生成多个实例了。
public class Singleton {
private Singleton() {
//must
}
private static Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
上面的代码其实就是大名鼎鼎的DCK了。这个大大提高了并发访问性能,而且实现了延迟初始化。如果第一个 instance 不为 null 的话,就可以直接返回了,减少了同步开销。只有当 instance 是 null 的时候,才会进行同步操作。这个时候需要在进行一次 instance 是否为 null 的检查。因为,有可能两个线程都判断 instance 为 null,一个线程加锁,对instance进行了实例化,释放锁,另外一个线程拿到锁后,不进行 instance 是否为 null 的再次判断,会再次进行instance的实例化。
上面代码有一个问题:instance 不是 volatile 的。看 new 操作的反汇编代码,其实他包含3条汇编指令:new、dup、init。这样当一个线程执行了new汇编指令后,被唤出。一个新的线程到来,在第一个判断 instance 是否为null时,发现 instance 已经不为null了,直接返回。这时的instance其实是不完整的。而且,即使一个线程实例化了 instance,由于每个线程都有自己的working 缓存,可能另一个线程看不到前一个线程对 instance 的操作。
new 字节码对应的处理流程:
- 给 instance 分配内存
- 调用 Singleton 的构造函数来初始化成员变量(invokespecial)
- 将instance对象指向分配的内存空间(执行完这步 instance 就为非 null 了)
但是在 JVM 的即时编译器中存在指令重排序的优化。也就是说上面的第二步和第三步的顺序是不能保证的,最终的执行顺序可能是 1-2-3 也可能是 1-3-2。如果是后者,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance,然后使用,然后顺理成章地报错。
volatile 实现
public class Singleton {
private Singleton() {
//must
}
private static volatile Singleton instance = null;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
JDK1.5 及之后版本增加了 volatile 关键字的一个语义:禁止指令重排序优化。
volatile 的两层语义:
- 可见性
可见性指的是在一个线程中对该变量的修改会马上由工作内存(Work Memory)写回主内存(Main Memory),所以会马上反应在其它线程的读取操作中。顺便一提,工作内存和主内存可以近似理解为实际电脑中的高速缓存和主存,工作内存是线程独享的,主存是线程共享的。 - 禁止指令重排序优化
静态内部类实现
public class Singleton {
private Singleton() {
//must, otherwise JVM create default constructor(public)
}
public static Singleton getInstance() {
return SingletonHolder.instance;
}
private static class SingletonHolder {
private static Singleton instance = new Singleton();
}
}
内部的静态类 SingletonHolder,只有当 getInstance 方法被调用时才会被加载。注意 SingletonHolder 这个内部类是 static 的,否则其成员变量不能声明成 static 的。JVM在类加载时保证了线程安全性问题,以及 instance 的唯一性。
说到底就是 JVM 本身保证了只能有一个线程能够完成对某个类的加载,也即 JVM 中仅有一个该类的实例(Class的实例)。
枚举实现
上面提到的几种实现单例的方式都有共同的缺点:
1)需要额外的工作来实现序列化,否则每次反序列化一个序列化的对象时都会创建一个新的实例。
2)可以使用反射强行调用私有构造器(如果要避免这种情况,可以修改构造器,让它在创建第二个实例的时候抛异常)。
而枚举类很好的解决了这两个问题,使用枚举除了线程安全和防止反射调用构造器之外,还提供了自动序列化机制,防止反序列化的时候创建新的对象。因此,《Effective Java》作者推荐使用的方法。不过,在实际工作中,很少看见有人这么写。
public enum Singleton {
INSTANCE
}
我们可以通过 Singleton.INSTANCE 来访问实例,这比调用 getInstance() 方法简单多了。创建枚举默认就是线程安全的,所以不需要担心double checked locking,而且还能防止反序列化导致重新创建新的对象。
javap -v Singleton
Classfile /Users/n3verl4nd/Desktop/Singleton.class
Last modified 2018-8-8; size 730 bytes
MD5 checksum 4820457d608bac02cd356d7af672026d
Compiled from "Singleton.java"
public final class Singleton extends java.lang.Enum<Singleton>
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_FINAL, ACC_SUPER, ACC_ENUM
Constant pool:
#1 = Fieldref #4.#29 // Singleton.$VALUES:[LSingleton;
#2 = Methodref #30.#31 // "[LSingleton;".clone:()Ljava/lang/Object;
#3 = Class #14 // "[LSingleton;"
#4 = Class #32 // Singleton
#5 = Methodref #10.#33 // java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
#6 = Methodref #10.#34 // java/lang/Enum."<init>":(Ljava/lang/String;I)V
#7 = String #11 // INSTANCE
#8 = Methodref #4.#34 // Singleton."<init>":(Ljava/lang/String;I)V
#9 = Fieldref #4.#35 // Singleton.INSTANCE:LSingleton;
#10 = Class #36 // java/lang/Enum
#11 = Utf8 INSTANCE
#12 = Utf8 LSingleton;
#13 = Utf8 $VALUES
#14 = Utf8 [LSingleton;
#15 = Utf8 values
#16 = Utf8 ()[LSingleton;
#17 = Utf8 Code
#18 = Utf8 LineNumberTable
#19 = Utf8 valueOf
#20 = Utf8 (Ljava/lang/String;)LSingleton;
#21 = Utf8 <init>
#22 = Utf8 (Ljava/lang/String;I)V
#23 = Utf8 Signature
#24 = Utf8 ()V
#25 = Utf8 <clinit>
#26 = Utf8 Ljava/lang/Enum<LSingleton;>;
#27 = Utf8 SourceFile
#28 = Utf8 Singleton.java
#29 = NameAndType #13:#14 // $VALUES:[LSingleton;
#30 = Class #14 // "[LSingleton;"
#31 = NameAndType #37:#38 // clone:()Ljava/lang/Object;
#32 = Utf8 Singleton
#33 = NameAndType #19:#39 // valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
#34 = NameAndType #21:#22 // "<init>":(Ljava/lang/String;I)V
#35 = NameAndType #11:#12 // INSTANCE:LSingleton;
#36 = Utf8 java/lang/Enum
#37 = Utf8 clone
#38 = Utf8 ()Ljava/lang/Object;
#39 = Utf8 (Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
{
public static final Singleton INSTANCE;
descriptor: LSingleton;
flags: ACC_PUBLIC, ACC_STATIC, ACC_FINAL, ACC_ENUM
public static Singleton[] values();
descriptor: ()[LSingleton;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=1, locals=0, args_size=0
0: getstatic #1 // Field $VALUES:[LSingleton;
3: invokevirtual #2 // Method "[LSingleton;".clone:()Ljava/lang/Object;
6: checkcast #3 // class "[LSingleton;"
9: areturn
LineNumberTable:
line 1: 0
public static Singleton valueOf(java.lang.String);
descriptor: (Ljava/lang/String;)LSingleton;
flags: ACC_PUBLIC, ACC_STATIC
Code:
stack=2, locals=1, args_size=1
0: ldc #4 // class Singleton
2: aload_0
3: invokestatic #5 // Method java/lang/Enum.valueOf:(Ljava/lang/Class;Ljava/lang/String;)Ljava/lang/Enum;
6: checkcast #4 // class Singleton
9: areturn
LineNumberTable:
line 1: 0
static {};
descriptor: ()V
flags: ACC_STATIC
Code:
stack=4, locals=0, args_size=0
0: new #4 // class Singleton
3: dup
4: ldc #7 // String INSTANCE
6: iconst_0
7: invokespecial #8 // Method "<init>":(Ljava/lang/String;I)V
10: putstatic #9 // Field INSTANCE:LSingleton;
13: iconst_1
14: anewarray #4 // class Singleton
17: dup
18: iconst_0
19: getstatic #9 // Field INSTANCE:LSingleton;
22: aastore
23: putstatic #1 // Field $VALUES:[LSingleton;
26: return
LineNumberTable:
line 2: 0
line 1: 13
}
Signature: #26 // Ljava/lang/Enum<LSingleton;>;
SourceFile: "Singleton.java"
http://wuchong.me/blog/2014/08/28/how-to-correctly-write-singleton-pattern/
https://blog.csdn.net/fan2012huan/article/details/53454724
https://blog.csdn.net/goodlixueyong/article/details/51935526