这篇文章主要着重介绍单例模式的优缺点并做分析,如何使用更优的单例模式。
维基百科:单例模式
单例模式,也叫单子模式,是一种常用的软件设计模式,属于创建型模式的一种。在应用这个模式时,单例对象的类必须保证只有一个实例存在。
许多时候整个系统只需要拥有一个的全局对象,这样有利于我们协调系统整体的行为。比如在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,然后服务进程中的其他对象再通过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。
public class EagerSingleton {
// 由于这个实例在Application创建的时候就会被创建,不管这个实例是否会被使用。
// 这可能会导致内存泄露的问题。
private static final EagerSingleton instance = new EagerSingleton();
private EagerSingleton() {
}
/**
* 饿汉模式
*/
public static EagerSingleton getInstance() {
return instance;
}
}
public class LazySingleton1 {
private static LazySingleton1 instance = null;
private LazySingleton1() {
}
/**
* 仅仅适用于单线程环境,不能保证线程安全
*/
public static LazySingleton1 getInstance() {
if (null == instance) {
instance = new LazySingleton1();
}
return instance;
}
}
public class LazySingleton2 {
private static LazySingleton2 instance = null;
private LazySingleton2() {
}
/**
* 适用于多线程环境,但是由于加了方法锁,强行将并行变串行,效率低下。
*/
public static synchronized LazySingleton2 getInstance() {
if (instance == null) {
instance = new LazySingleton2();
}
return instance;
}
}
public class LazySingleton3 {
// 这一步不是线程安全的
private static LazySingleton3 instance = null;
private LazySingleton3() {
}
/**
* 双重检查加锁(推荐);问题:不能彻底保证线程安全。
*/
public static synchronized LazySingleton3 getInstance() {
// 先判断实例是否存在,若不存在再对类对象进行加锁处理
if (instance == null) {
// 类锁,确保创建唯一的对象
synchronized (LazySingleton3.class) {
if (instance == null) {
instance = new LazySingleton3();
}
}
}
return instance;
}
}
不正确
的对象。Partially created objects can be returned by the Double Checked Locking pattern when used in Java. An optimizing JRE may assign a reference to the baz variable before it calls the constructor of the object the reference points to.
Note: With Java 5, you can make Double checked locking work, if you declare the variable to be volatile.
➜ Singleton git:(master) ✗ javap -c -s -l LazySingleton3.class
Compiled from "LazySingleton3.java"
public class Practice.Java.Singleton.LazySingleton3 {
public static synchronized Practice.Java.Singleton.LazySingleton3 getInstance();
descriptor: ()LPractice/Java/Singleton/LazySingleton3;
Code:
0: getstatic #2 // Field instance:LPractice/Java/Singleton/LazySingleton3;
3: ifnonnull 37
6: ldc #3 // class Practice/Java/Singleton/LazySingleton3
8: dup
9: astore_0
10: monitorenter
11: getstatic #2 // Field instance:LPractice/Java/Singleton/LazySingleton3;
14: ifnonnull 27
17: new #3 // class Practice/Java/Singleton/LazySingleton3
20: dup
21: invokespecial #4 // Method "":()V
24: putstatic #2 // Field instance:LPractice/Java/Singleton/LazySingleton3;
27: aload_0
28: monitorexit
29: goto 37
32: astore_1
33: aload_0
34: monitorexit
35: aload_1
36: athrow
37: getstatic #2 // Field instance:LPractice/Java/Singleton/LazySingleton3;
40: areturn
Exception table:
from to target type
11 29 32 any
32 35 32 any
LineNumberTable:
line 15: 0
line 17: 6
line 18: 11
line 19: 17
line 21: 27
line 23: 37
static {};
descriptor: ()V
Code:
0: aconst_null
1: putstatic #2 // Field instance:LPractice/Java/Singleton/LazySingleton3;
4: return
LineNumberTable:
line 5: 0
}
// 开辟一块内存区域,作为对象的空间
17: new #3 // class Practice/Java/Singleton/LazySingleton3
// 复制操作数栈顶值,并将其压入栈顶
20: dup
// 执行对象的初始化操作
21: invokespecial #4 // Method "":()V
// 给类的静态字段赋值
24: putstatic #2 // Field instance:LPractice/Java/Singleton/LazySingleton3;
上述核心代码中,21行和24行的代码可能由于指令重排
,导致getInstance出现错误。
以下摘录自:JVM 指令重排对双重校验锁单例模式的影响
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置 instance 指向刚分配的内存地址
但是经过重排序后如下:
memory = allocate(); //1:分配对象的内存空间
instance = memory; //3:设置 instance 指向刚分配的内存地址,此时对象还没被初始化
ctorInstance(memory); //2:初始化对象
可以看到指令重排之后,instance 指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。在线程 A 初始化完成这段内存之前,线程 B 虽然进不去同步代码块,但是在同步代码块之前的判断就会发现 instance 不为空,此时线程 B 获得 instance 对象进行使用就可能发生错误。
public class LazySingletonDoubleCheck {
// 单单的static并不能保证线程安全,应该加上volatile
private static volatile LazySingletonDoubleCheck instance = null;
private LazySingletonDoubleCheck() {
}
/**
* 双重检查加锁(推荐)
*/
public static synchronized LazySingletonDoubleCheck getInstance() {
// 先判断实例是否存在,若不存在再对类对象进行加锁处理
if (instance == null) {
synchronized (LazySingletonDoubleCheck.class) {
if (instance == null) {
instance = new LazySingletonDoubleCheck();
}
}
}
return instance;
}
}
根据上述分析,给出如上的单例模式用法。
public class StaticSingleton {
private StaticSingleton() {
}
/**
* 静态内部类(推荐)
*/
public static StaticSingleton getInstance() {
return SingletonHolder.instance;
}
private static class SingletonHolder {
private static final StaticSingleton instance = new StaticSingleton();
}
}
静态内部类instance在创建过程中又是如何保证线程安全的呢?——《深入理解JAVA虚拟机》
虚拟机会保证一个类的
方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的
方法,其他线程都需要阻塞等待,直到活动线程执行
方法完毕。如果在一个类的
方法中有耗时很长的操作,就可能造成多个进程阻塞(需要注意的是,其他线程虽然会被阻塞,但如果执行
方法后,其他线程唤醒之后不会再次进入
方法。同一个加载器下,一个类型只会初始化一次。),在实际应用中,这种阻塞往往是很隐蔽的。