单例模式
维基百科:
In software engineering, the singleton pattern is a software design pattern that restricts the instantiation of a class to one "single" instance. This is useful when exactly one object is needed to coordinate actions across the system. The term comes from the mathematical concept of a singleton.
说人话:
让一个类在系统中永远最多有一个实例存在。
从定义上看,这似乎是一个非常简单的设计模式,但是当涉及到实现时,它会带来很多实现方面的问题。
特点
-
单例模式限制了类的实例化,并确保Java虚拟机中仅存在该类的一个实例。
-
单例类必须提供全局访问点才能获取该类的实例。
场景
-
用于日志记录,驱动程序对象,缓存和线程池。
-
还用于其他设计模式,例如 Abstract Factory,Builder,Prototype,Facade等。
-
还在Java核心类中使用,例如:java.lang.Runtime,java.awt.Desktop。
-
还在Spring框架中使用,比如Spring中的bean的创建。
实现
为了实现Singleton模式,有不同的方法,但是所有方法都具有以下共同的思路。
-
私有构造函数,用于限制该类在别的类中实例化。
-
同一个单例类的私有静态变量,是该类的唯一实例(对象)。
-
返回类实例的公共静态方法,是外部获取单例类实例的唯一全局访问点。
1、饿汉(Eager initialization)
饿汉方式在
类加载时立即创建单例类的实例,这是最简单的方法。但是,缺点是几十客户端应用程序可能不是用它,该实例
也会被创建。
public class EagerInitializedSingleton {
private static final EagerInitializedSingleton INSTANCE = new EagerInitializedSingleton();
// 私有构造方法保证了无法被私自创建实例
private EagerInitializedSingleton() {}
public static EagerInitializedSingleton getInstance() {
return INSTANCE;
}
}
如果你的单例类没有使用很多资源,则可以用这种方法。
但是大多情况下,都是为文件系统、数据库连接等资源创建单例类的。
另外,它不能处理Excepton。
2、静态代码块(Static block initialization)
静态代码块初始化与饿汉类似,不同的是,实例化单例类时可以对Exception进行处理。
public class StaticBlockSingleton {
private static StaticBlockSingleton instance;
private StaticBlockSingleton() {}
// 可以在静态代码块中处理异常
static {
try {
instance = new StaticBlockSingleton();
} catch(Exception e) {
throw new RuntimeException(“单例对象初始化异常");
}
}
public static StaticBlockSingleton getInstance() {
return instance;
}
}
饿汉与静态代码块的初始化方法都是在
使用实例之前就创建了实例,这显然不是最佳实践。
3、懒汉(Lazy Initialization)
懒汉初始化方法的特点是在
全局访问方法中创建实例。
public class LazyInitializedSingleton {
private static LazyInitializedSingleton instance;
private LazyInitializedSingleton() {}
public static LazyInitializedSingleton getInstance() {
if (instance == null) {
instance = new LazyInitializedSingleton();
}
return instance;
}
}
上面的实现在单线程环境下可以很好地工作,但是对于
多线程系统,如果多个线程同时位于if条件中,则可能导致问题。它将
破坏单例模式,并且两个线程都将获得单例类的不同实例。下面,我们将介绍创建线程安全的单例类的不同方法。
4、线程安全的懒汉(Thread Safe Singleton)
实现一个线程安全的单例类,最简单的方式时使用全局访问的
同步方法,以便一次仅有一个线程可以执行此方法。
public class ThreadSafeSingleton {
private static ThreadSafeSingleton instance;
private ThreadSafeSingleton() {}
public static synchronized ThreadSafeSingleton getInstance() {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
return instance;
}
}
这种实现可以很好的工作,并且事线程安全的。但是由于同步方法存在一定的成本,所以它降低了性能,尽管我们可能只是在创建实例的前几个线程中需要这种同步锁。
所以为了避免每次的额外开销,我们可以使用双重检查的锁定原理。
Talk is cheap. Show me the code.
public static ThreadSafeSingleton getInstanceUsingDoubleLocking() {
if (instance == null) {
synchronized (ThreadSafeSingleton.class) {
if (instance == null) {
instance = new ThreadSafeSingleton();
}
}
}
return instance;
}
但是,这种方式在高并发情况下可能
会失败!
会失败?
请看:
instance = new ThreadSafeSingleton();
这行代码不是一个
原子操作,实际上在JVM中大概做了三件事:
①给instance分配内存
②
调用构造方法完成实例的初始化(写入堆内存)
③让实例对象instance变量指向①的内存地址(完成这一步instance就不是null了)
在JVM即时编译器中存在
指令重排序,它会对语句执行优化,只要语句间没有逻辑依赖关系,那它就有可能并且有权对其进行优化。
在这里要说下as-is-serial这个东西,as-if-serial是指在执行结果不会改变的情况下,JVM为了提高程序的执行效率会对指令进行重排序。
就像上面的程序,
②是依赖于①的,所以为了保证程序的正确性,②在①之后执行,但是③和②不存在依赖性,是③和②谁先执行都不影响结果,所以JVM就可能会对它们进行重排序。也就是执行顺序可能是:①->③->
②
如果在
③已经执行完了,但是
②还没有完成初始化时,这时候来了一个线程,当这个线程执行到
if(instance == null)
时,它发现不为null,则立即 return instance 了。
问题来了,如果这时要用到这个instance,但还没初始化完成,可能就会出现意料之外的事情。
那有
解决办法
吗?
有的。我们可以通过
volatile关键字来修饰instance,它可以
阻止指令重排序,在 volatile 变量的赋值操作后面会有一个
内存屏障(生成的汇编代码上),读操作不会被重排序到内存屏障之前,从而保证初始化的有序性。
private static volatile ThreadSafeSingleton instance;
万无一失了吗?
在
Java 5 以前的版本使用了 volatile 的双重检查锁还是有问题的。其原因是 Java 5 以前的 JMM(Java 内存模型)是
存在缺陷的,既是将变量声明成 volatile 也不能完全避免重排序,主要是 volatile 变量前后的代码仍然存在重排序问题。这个 volatile 屏蔽重排序的问题在 Java 5 中才得以修复,所以在这之后才可以放心使用 volatile。
Tips:不得不说这种方式太多地方要注意了,很不友好,所以我们再换个路子吧!
5、静态
内部
类(
Bill Pugh
Singleton Implementation)
冷知识:
Bill Pugh
是个歪果仁,是他想到的这种方法。
public class BillPughSingleton {
private BillPughSingleton() {}
private static class SingletonHelper {
private static final BillPughSingleton INSTANCE = new BillPughSingleton();
}
public static BillPughSingleton getInstance() {
return SingletonHelper.INSTANCE;
}
}
私有的静态内部类中包含单例类的实例。加载 singleton类时,SingletonHelper 类不会夹在到内存中,在调用 getInstance 方法时类才会加载到内存中并创建类的实例。
这是一种比较广泛的实现方法,因为它不需要同步。
6、枚举(Enum Singleton)
Joshua Bloch 建议使用Enum来实现Singleton设计模式,因为Java确保在Java程序中仅将一次枚举值实例化一次。由于Java枚举值可全局访问,因此单例也是如此。缺点是枚举类型有些不灵活;例如,它不允许延迟初始化。
public enum EnumSingleton {
INSTANCE;
public static void doSomething(){
// 该干嘛干嘛
}
}
关于这位大哥为什么建议用枚举,后面说到“破坏者”时会提到。
破坏者
1、
反射来也!bububu……
反射
可以销毁上述除枚举之外的所有单例实现方法。让我们用一个示例类来看看。
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) {
// 下面的代码会毁掉单例模式
constructor.setAccessible(true);
instanceTwo = (EagerInitializedSingleton) constructor.newInstance();
break;
}
} catch (Exception e) {
e.printStackTrace();
}
System.out.println(instanceOne.hashCode());
System.out.println(instanceTwo.hashCode());
}
}
运行上述测试类时,会注意到两个实例的hashCode会不相同,这会破坏单例模式。反射非常强大,并在例如Spring和Hibernate的许多框架中使用。
2、
序列化来搞事!ada……
有时候我们需要在Singleton类中实现Serializable接口,以便我们可以将其状态存储在文件系统中,并在以后的某个时间使用它。
请看一个例子。
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;
}
}
序列化一个单例类的问题在于,每当我们反序列化它时,它将创建该类的新实例。让我们用一个简单的程序看看它。
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();
// 将文件反序列化为对象
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() throws ObjectStreamException {
System.out.println("readResolve() invoked.");
return getInstance();
}
这样的话上述程序的运行结果就会是:两个对象的 hashCode 相同。