Java设计模式——单例模式
每一个模式描述了一个在我们周围不断重复发生的问题,以及该问题的解决方案的核心。这样,你就能一次又一次地使用该方案而不必做重复劳动。
单例(Singleton)模式是在Java代码中使用频率非常高的一种设计模式。提到设计模式,可能很多人第一个想到的就是单例模式。单例模式要实现的效果是在全局范围内只有一个实例对象,用该实例对象管理其他对象或者一些资源的话就相当于在全局范围内有一个唯一的管理者,能够使代码结构更加清晰。
设计模式本身是不区分编程语言的,它是一种通用的良好的设计思路。然而不同编程语言又具有自身的特点,因而在对设计模式的实现上可能具有一些特有的“个性”,这一点在单例模式的Java实现中尤为明显,特别是针对多线程访问的情况。
最简单的单例模式
code-1:
class Singleton {
private static Singleton instance;
private Singleton() {} //私有化构造函数
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
将构造函数私有化,以保证无法再外部构造该对象。使用一个静态方法,先检查对象是否为空然后构造对象实例来保证对象唯一。
这种“先检查后执行”的办法称为“Check-Then-Act”操作,在Java并发编程中这是一个常见的“竞态条件(Race Condition)”,即在多线程情况下由于一些可能的线程执行顺序导致错误的结果。也就是线程不安全的。
线程安全的单例模式
要想得到线程安全的单例,常见的有两种做法。第一种也就是加上一个互斥锁,保证多线程在临界区的顺序性,如下(注意synchronized
关键字):
code-2:
class Singleton {
private static Singleton instance;
private Singleton() {}
public static synchronized Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
另一种做法就是在所有线程能够访问对象实例之前完成对象的构造过程,保证所有线程能够访问该对象时对象已经构造完成。也就是著名的饿汉式!
code-3:
class Singleton {
private static Singleton instance = new Singleton();
private Singleton() {}
public static Singleton getInstance() {
return instance;
}
}
一种更好的改进
上面的两种线程安全的单例实现都有一定的不足之处:
* 第一种,每次获取单例对象都要先获取锁,得到单例对象后释放锁。由于锁的互斥性,多个线程获取单例对象时只能排队依次获取,效率低下;
* 第二种,类加载时就要完成对象构造,由于类加载的时机不容易控制,再者如果对象构造比较耗时而我们又希望系统能够快速启动,这些情况都不是我们希望的。
选择性加锁,双重检查的单例实现
code-4:
class Singleton {
private volatile static Singleton singleton;
private Singleton() {}
public static Singleton getSingleton() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
上面代码相当于在code-2 的基础上加上了空值判断,只有在判断实例对象为空时才加锁并构造对象,后续对象已存在的情况下就不会再进入加锁的同步代码块,有效提高了并发效率。
注意上面代码中的volatile
关键字,该关键字在上述代码中必不可少,否则这个单例的类并不是线程安全的。Java中构造一个对象并将一个指向该对象的引用保存在其他代码可以访问的地方称为“对象发布(Publish)”,如果在对象构造完成之前发布该对象就会破坏线程的安全性。
new
一个对象需要多步操作,现代处理器可能将这多步操作顺序进行重排序以提高并行度。比如:
singleton = new Singleton();
//假如上述语句可以分解为如下4条指令
operation 1: //malloc memory
operation 2: //init property p1
operation 3: //init property p2
operation 4: //set singleton point to the memory
//则实际的执行顺序可能可能是:
operation 1: //malloc memory
operation 2: //init property p1
operation 4: //set singleton point to the memory
operation 3: //init property p2
//上面operation 4发布对象,而这时operation 3还未执行,这就是一个不完整的对象
上面示例展示了一种不安全的发布对象的方式,这时重排序造成的。当然这种重排序 在单线程情况下是安全的,这时Java内存模型保证的。在多线程情况下,后一个线程可能取得前一个线程未构造完成的对象。
Java中volatile
关键字可以保证对象的构造不逸出构造函数之外,因而能够保证如果后一条线程能够取得对象引用,那么对象一定是构造完成的,从而保证了该对象的线程安全性。
一种取巧的方法
前面提到过,饿汉式单例实现使用类加载器完成对象构造,对象构造的时机不易控制,如果能解决这个问题呢?
class Singleton {
private static class InstanceHolder {
private static final Singleton instance= new Singleton();
}
private Singleton() {}
public static final Singleton getInstance() {
return InstanceHolder.instance;
}
}
使用内部类,内部类并不随着外部类的加载而被加载。只有调用getInstance()方法是,内部类才被加载,此时对象才被构造。因为类加载过程是线程安全的,因为这种实现方式是线程安全的。
还可能面临的问题以及解决办法
上面的线程安全的单例实现已经可以满足大多数情况下的需求了,但是还存在一些更加“变态”的情况可以将上述单例搞出多个实例来。
1. Java的类是通过类加载器ClassLoader加载类到内存中的,普通情况下的Classloader使用双亲委托机制,能够保证被加载类的唯一性。然而Web服务器为了隔离多个web应用,打破了这种机制,使用自定义类加载器优先的方式加载类。Web服务器一般使用多个类加载器加载类,甚至为每个Servlet分配一个类加载器。这种情况下两个不同的Servlet访问一个单例,单例类可能被加载两次,就产生了两个对象,这就破坏了单例的语义。解决办法就是使用同一个ContextClassLoader绑定在线程上,使用ContextClassLoader加载单例类。
2. 单例类可能实现了Serialable接口,重复的序列化/反序列化可以复制出多个相同的对象。此外还可以通过反射构造出多个对象。着也称为序列化攻击和反射攻击。对于这种情况, 《Effective Java》上推荐一种方式,即用枚举构造单例,具有抗序列化攻击和反射攻击的能力。
enum Singleton {
INSTANCE;
public void methodA() {}
public void methodB() {}
//any method for this instance
}