01 单例模式(Singleton Pattern)
一句话概括:限制类的实例化,全局最多只有一个该类的实例。
单例模式是四大模式之一,概念很简单,但实现起来会有诸多问题。
Java Singleton
- 单例模式限制类的实例化,确保只有一个实例在JAVA虚拟机中。
- 单例模式必须提供一个全局访问来获取类的实例。
- 单例模式用于日志记录(logging),驱动程序对象(drivers objects),缓存(caching)和线程池(thread pool)等。
- Singleton设计模式也用于其他设计模式,如Abstract Factory,Builder,Prototype,Facade等。
Java Singleton Pattern
实现Singleton模式有多种方法,但是它们都有以下共同的概念。
- 构造函数私有化(private constructor),以限制被其它类初始化。
- 类的内部提供一个私有的静态变量来保存该类的全局唯一实例。
- 提供一个公共的静态方法返回该类的唯一实例,这是外部获取该单例类实例的全局唯一访问点。
在下面的章节中,我们将学习Singleton模式实现的不同方法以及各个实现中涉及的问题。
Eager initialization
Eager initialization是指Singleton类的实例在类被加载时创建,这是创建单例类最简单的方法。
package com.journaldev.singleton;
public class EagerInitializedSingleton {
//在类被加载时就初始化该类的实例
private static final EagerInitializedSingleton instance = new EagerInitializedSingleton();
//防止其它成员初始化该类
private EagerInitializedSingleton(){}
public static EagerInitializedSingleton getInstance(){
return instance;
}
}
若你的单例类没有使用太多的资源,这种方法比较合适。但在大多数情况下,Singleton类是为文件系统,数据库连接等资源创建的。
除了调用getInstance方法获取实例外,我们应该避免被其它类实例化。另外注意,这种方法在实例化时没有办法提供任何异常处理选项。
Lazy Initialization
在全局初次使用该实例的时候进行初始化
示例代码:
package com.journaldev.singleton;
public class LazyInitializedSingleton {
private static LazyInitializedSingleton instance;
private LazyInitializedSingleton(){}
//在初次调用的时候进行初始化
public static LazyInitializedSingleton getInstance(){
if(instance == null){
instance = new LazyInitializedSingleton();
}
return instance;
}
}
上面的两个实现在单线程环境下工作良好,但是当涉及到多线程时,如果多个线程进入getInstance()方法的if循环内部,则会导致问题(尤其是在类的实例化需要一定时间时经常发生)。 它会破坏单例模式,两个线程都会得到单例类的不同实例。 接下来我们来讨论创建线程安全的单例类的方法。
Thread Safe Singleton
创建线程安全单例类的更简单方法是使全局访问方法同步,以便一次只有一个线程可以执行此方法。
package com.journaldev.singleton;
public class ThreadSafeSingleton {
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton(){}
public static synchronized ThreadSafeSingleton getInstance(){
if(instance == null){
instance = new ThreadSafeSingleton();
}
return instance;
}
}
以上方法能在多线程下工作良好提供了线程安全的方式,但是牺牲了性能,因为增加了同步方法(synchronized),其实我们仅仅需要用在可能会导致单独创建实例的前几个线程(参考:Java多线程相关)。为了避免每次额外的开销,我们可以使用双重检查锁定(double-checked locking),在if条件中使用synchronized块进行附加检查,以确保只创建一个singleton类实例。
public static ThreadSafeSingleton getInstanceUsingDoubleLocking(){
if(instance == null){
synchronized (ThreadSafeSingleton.class) {
if(instance == null){
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
Bill Pugh Singleton Implementation
在Java 5之前,Java内存模型存在很多问题,以上方法在某些情况下会失败,因为太多线程试图同时获取Singleton类的实例。 所以Bill Pugh提出了一种使用内部静态帮助类来创建Singleton类的另一种方法。 Bill Pugh Singleton的实现是这样的:
package com.journaldev.singleton;
public class BillPughSingleton {
private BillPughSingleton(){}
private static class SingletonHelper{
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance(){
return SingletonHelper.INSTANCE;
}
}
注意private static class SingletonHelper私有静态内部类,当单例类被加载时,SingletonHelper类没有被加载到内存,仅当有人调用了getInstance方法时,这个私有类才会有加载并创建单例类的实例。
这是Singleton类最广泛使用的方法,因为它不需要同步。我在很多项目中都使用这种方法,并且很容易理解和实施。
Using Reflection to destroy Singleton Pattern
用反射来破坏单例模式,反射可以破坏上述所有单例模式实现,我们来看个例子:
package com.journaldev.singleton;
import java.lang.reflect.Constructor;
public class ReflectionSingletonTest {
public static void main(String[] args) {
EagerInitializedSingleton instanceOne = EagerInitializedSingleton.getInstance();
EagerInitializedSingleton instanceTwo = null;
try {
Constructor[] constructors = EagerInitializedSingleton.class.getDeclaredConstructors();
for (Constructor constructor : constructors) {
//Below code will destroy the singleton pattern
constructor.setAccessible(true);
instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(instanceOne.hashCode());
System.out.println(instanceTwo.hashCode());
}
}
当你运行上述测试代码,你会发现两个instance的hashCode是不同的,破坏了单例模式。反射非常强大,在大量的类库里被用到,例如Spring和Hibernate.
Enum Singleton
为了克服Reflection的这种情况,Joshua Bloch建议使用Enum来实现Singleton设计模式,因为Java确保任何枚举值在Java程序中仅实例化一次。 由于Java Enum值是全局访问的,单例也是全局可访问的。 缺点是枚举类型有点不灵活; 例如,它不允许延迟初始化。
package com.journaldev.singleton;
public enum EnumSingleton {
INSTANCE;
public static void doSomething(){
//do something
}
}
Serialization and Singleton
有时在分布式系统中,我们需要在Singleton类中实现Serializable接口,以便我们可以将它的状态存储在文件系统中,并在之后的某个时间点取回它。 这是一个实现Serializable接口的小型单例类。
package com.journaldev.singleton;
import java.io.Serializable;
public class SerializedSingleton implements Serializable{
private static final long serialVersionUID = -7604766932017737115L;
private SerializedSingleton(){}
private static class SingletonHelper{
private static final SerializedSingleton instance = new SerializedSingleton();
}
public static SerializedSingleton getInstance(){
return SingletonHelper.instance;
}
}
上面的序列化单例类的问题是,只要我们反序列化它,它就会创建一个新的类实例。 让我们看一个简单的程序。
package com.journaldev.singleton;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInput;
import java.io.ObjectInputStream;
import java.io.ObjectOutput;
import java.io.ObjectOutputStream;
public class SingletonSerializedTest {
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException {
SerializedSingleton instanceOne = SerializedSingleton.getInstance();
ObjectOutput out = new ObjectOutputStream(new FileOutputStream(
"filename.ser"));
out.writeObject(instanceOne);
out.close();
//deserailize from file to object
ObjectInput in = new ObjectInputStream(new FileInputStream(
"filename.ser"));
SerializedSingleton instanceTwo = (SerializedSingleton) in.readObject();
in.close();
System.out.println("instanceOne hashCode="+instanceOne.hashCode());
System.out.println("instanceTwo hashCode="+instanceTwo.hashCode());
}
}
上面程序的输出结果是
instanceOne hashCode=2011117821
instanceTwo hashCode=109647522
很明显破坏了单例模式,避免这种情况的方法是在单例类中提供一个readResolve()方法的实现。
protected Object readResolve() {
return getInstance();
}
之后你再运行之前那段代码就会发现,两个hashCode的值一样了。