设计模式在软件开发中一直是一个非常热门的话题。通常来说,设计模式是一种解决某一类软件开发问题的解决方案。而这篇文章所讲的单例模式就是创建型模式中的一种。
单例模式的目的
单例模式的主要目的是限制对象的创建操作,从而保证在JVM内存中有且只有1个该类的实例对象。而所谓的限制操作就是屏蔽构造器,然后对外只开放另外一种方式来创建类的对象。因为内存中只有一个类的实例,所以单例经常被用在与资源相关的使用上,比如database connections 或者 sockerts。
乍一听感觉单例模式是一种很简单的设计模式,但是当我们真正的去实现一个单例的时候,往往会发现很多问题。
实现一个单例
想实现一个单例模式,最常用也是最简单的做法就是先将类构造器设为 private,在此基础上有两种方式实现单例:
1. 饿汉式单例
在饿汉式单例中,单例类的实例对象是在类加载的时候创建的,这也是创建一个单例最简单的方式。
通过将构造器设为private,开发者不能再通过new的方式来创建类的实例对象。取而代之的是,我们为之创建了一个public static的方法getInstance()来创建单例对象。
public class SingletonClass {
private static volatile SingletonClass sSoleInstance = new SingletonClass();
//private constructor.
private SingletonClass(){}
public static SingletonClass getInstance() {
return sSoleInstance;
}
}
这种方式虽说可以快速方便的创建单例模式,但是这种方式也存在一些缺陷:即使程序中暂时不需要使用到该类的对象,这个单例创建的代码还是会被系统执行,也就是说系统中存在一个暂时用不到的对象,但是这个对象又不能被GC回收。设想一下如果创建的单例是一个数据库的链接对象或者是一个socket,那会占用系统相当多的内存资源。解决办法就是使用下面的懒汉式实现单例模式。
2. 懒汉式单例
与饿汉式相反,在懒汉式中我们只在 getInstance() 方法内部去主动new出实例对象。在这个方法内部我们会判断该单例是否已经被创建?如果是则直接返回被创建的对象 sSoleInstance 即可。如果不是就初始化 sSoleInstance 并返回。
public class SingletonClass {
private static SingletonClass sSoleInstance;
private SingletonClass(){} //private constructor.
public static SingletonClass getInstance(){
if (sSoleInstance == null){ //if there is no instance available... create new one
sSoleInstance = new SingletonClass();
}
return sSoleInstance;
}
}
我们知道在Java中可以通过两个对象的hashCode()值是否相等来判断是不是同一个对象。如果上面的单例模式是被正确实现的,那下面的代码应该返回的是同样的hash值。
public class SingletonTester {
public static void main(String[] args) {
//Instance 1
SingletonClass instance1 = SingletonClass.getInstance();
//Instance 2
SingletonClass instance2 = SingletonClass.getInstance();
//now lets check the hash key.
System.out.println("Instance 1 hash:" + instance1.hashCode());
System.out.println("Instance 2 hash:" + instance2.hashCode());
}
}
如下是打印日志
可以看出两个对象的hash值是一样的。但是这样就可以说这个懒汉式单例模式就是一个完美的单例模式了吗??? 答案是否定的!为了实现完美的单例,我们还需要做到以下几点:
- 屏蔽反射调用
- 添加线程安全
- 防止反序列化
屏蔽反射调用
在Java中,开发者除了使用new的方式创建对象之外,还可以使用反射间接的调用类构造器来创建对象。就如上面懒汉式的单例模式中,通过反射还是可以创建出多个 SingletonClass 的实例对象
public class SingletonTester {
public static void main(String[] args) {
//Create the 1st instance
SingletonClass instance1 = SingletonClass.getInstance();
//Create 2nd instance using Java Reflection API.
SingletonClass instance2 = null;
try {
Class<SingletonClass> clazz = SingletonClass.class;
Constructor<SingletonClass> cons = clazz.getDeclaredConstructor();
cons.setAccessible(true);
instance2 = cons.newInstance();
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
//now lets check the hash key.
System.out.println("Instance 1 hash:" + instance1.hashCode());
System.out.println("Instance 2 hash:" + instance2.hashCode());
}
}
以下是上面代码的输出结果:
可以看出两个实例对象的hashCode值依然是不同的,这样就不能保证系统中只存在唯一的 SingletonClass 实例。
解决办法:
为了防止其他人通过反射间接调用类构造器,我们可以在这个构造器中进行判断,如果 sSoleInstance 不为null,则抛出异常,并告知使用者应该使用 getInstance()方法来获取实例对象。
public class SingletonClass {
private static SingletonClass sSoleInstance;
//private constructor.
private SingletonClass(){
//Prevent form the reflection api.
if (sSoleInstance != null){
throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
}
}
public static SingletonClass getInstance(){
if (sSoleInstance == null){ //if there is no instance available... create new one
sSoleInstance = new SingletonClass();
}
return sSoleInstance;
}
}
添加线程安全
除了有可能被反射调用之外,上面的单例模式还存在线程安全问题。如果两个线程几乎在同一时间调用 getInstance() 方法来初始化单例,那会发生什么呢? 我们可以用一段测试代码来演示一下:
public class SingletonTester {
public static void main(String[] args) {
//Thread 1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
SingletonClass instance1 = SingletonClass.getInstance();
System.out.println("Instance 1 hash:" + instance1.hashCode());
}
});
//Thread 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
SingletonClass instance2 = SingletonClass.getInstance();
System.out.println("Instance 2 hash:" + instance2.hashCode());
}
});
//start both the threads
t1.start();
t2.start();
}
}
上述代码被运行很多次的情况下,有可能输出两个不一样的实例对象hash值
很显然目前的 getInstance() 方法并不是线程安全的, 当在并发情况下很难保证单例状态。
解决办法:
1 先给getInstance方法添加 Synchonized 标识,实现同步操作
public class SingletonClass {
private static SingletonClass sSoleInstance;
//private constructor.
private SingletonClass(){
//Prevent form the reflection api.
if (sSoleInstance != null){
throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
}
}
public synchronized static SingletonClass getInstance(){
if (sSoleInstance == null){ //if there is no instance available... create new one
sSoleInstance = new SingletonClass();
}
return sSoleInstance;
}
}
添加锁同步之后,设想还是有两个线程A、B同时调用这个方法。当线程A在调用getInstance()方法时,线程B就必须等到第一个线程执行完getInstance()之后,才会进入方法,从而实现getInstance()方法的线程安全。
但是这种写法也会伴随着一些性能缺陷:
a. 因为JVM锁实现机制,getInstance()方法的执行效率会降低
b. 当getInstance方法被系统调用过一次,也就是sSoleInstance被创建之后,没用必要在进行锁机制的判断了
2. 二次检查单例是否为null
我们可以通过二次检查 sSoleInstance 是否为null,来判断是否要执行synchronized语句块,也就是说只有在 sSoleInstance 为null的时候才执行synchronized语句块,因此锁机制引起的效率问题只会碰到一次
public class SingletonClass {
private static SingletonClass sSoleInstance;
//private constructor.
private SingletonClass(){
//Prevent form the reflection api.
if (sSoleInstance != null){
throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
}
}
public static SingletonClass getInstance() {
//Double check locking pattern
if (sSoleInstance == null) { //Check for the first time
synchronized (SingletonClass.class) { //Check for the second time.
//if there is no instance available... create new one
if (sSoleInstance == null) sSoleInstance = new SingletonClass();
}
}
return sSoleInstance;
}
}
表面上看,添加了锁同步之后我们不再担心并发的问题。但是在底层系统中,为了提高性能,java编译器和处理器可能会对指令做重排序。
重排序可以分为三种:
(1)编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
(2)指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
(3)内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
我们可以直接参考一下JSR 133 中对重排序问题的描述:
(1) (2)
先看上图中的(1)源码部分,从源码来看,要么指令 1 先执行要么指令 3先执行。如果指令 1 先执行,r2不应该能看到指令 4 中写入的值。如果指令 3 先执行,r1不应该能看到指令 2 写的值。但是运行结果却可能出现r2==2,r1==1的情况,这就是“重排序”导致的结果。上图(2)即是一种可能出现的合法的编译结果,编译后,指令1和指令2的顺序可能就互换了。因此,才会出现r2==2,r1==1的结果。
3. 使用volatile防止指令重排
public class SingletonClass {
private static volatile SingletonClass sSoleInstance;
//private constructor.
private SingletonClass(){
//Prevent form the reflection api.
if (sSoleInstance != null){
throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
}
}
public static SingletonClass getInstance() {
//Double check locking pattern
if (sSoleInstance == null) { //Check for the first time
synchronized (SingletonClass.class) { //Check for the second time.
//if there is no instance available... create new one
if (sSoleInstance == null) sSoleInstance = new SingletonClass();
}
}
return sSoleInstance;
}
}
这样,SingletonClass 就完全是一个线程安全的单例类了。
防止反序列化
有些时候,在一些分布式系统中。我们所写的单例类有可能需要实现 Serializable 接口。并将对象存储在数据流中,方便后续将数据流反序列化为实例对象。这样创建出来的对象跟使用 getInstance方法创建的对象是否是同一个对象呢 ?
public class SingletonTester {
public static void main(String[] args) {
try {
SingletonClass instance1 = SingletonClass.getInstance();
ObjectOutput out = null;
out = new ObjectOutputStream(new FileOutputStream("filename.ser"));
out.writeObject(instance1);
out.close();
//deserialize from file to object
ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser"));
SingletonClass instance2 = (SingletonClass) in.readObject();
in.close();
System.out.println("instance1 hashCode=" + instance1.hashCode());
System.out.println("instance2 hashCode=" + instance2.hashCode());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
输出结果:
从结果中可以两个对象的hash值还是不一样,这样就违反的单例的原则。
解决办法:
重写 readResove()方法。该方法在从stream中读取数据并反序列化时被调用,我们只要重写它并返回 getInstance返回的单例即可
public class SingletonClass implements Serializable {
private static volatile SingletonClass sSoleInstance;
//private constructor.
private SingletonClass(){
//Prevent form the reflection api.
if (sSoleInstance != null){
throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
}
}
public static SingletonClass getInstance() {
if (sSoleInstance == null) { //if there is no instance available... create new one
synchronized (SingletonClass.class) {
if (sSoleInstance == null) sSoleInstance = new SingletonClass();
}
}
return sSoleInstance;
}
//Make singleton from serialize and deserialize operation.
protected SingletonClass readResolve() {
return getInstance();
}
}
总结:
到目前为止,我们所写的单例类 SingletonClass 已经是线程安全,不会被反射调用,并且也不会被反序列化了。但是其实 SingletonClass 还不能说是100%完美的单例类:如果开发者使用自定义ClassLoader去加载这个类,还是有可能得到不同的实例对象。话说这还是极端情况,所以上面实现的单例类已经能够满足大多数的使用场景了 ^_^