设计模式之单例模式 (一)

单例模式是一个老生常谈的模式了,从我一开始学习JAVA的时候就听说过了,但为什么要有单例模式,又或是怎样的类可以或是需要做成单例模式的呢?

在我的工作中接触到不少单例模式的例子,例如一个工厂类,或是对DAO层封装好了的工具类,它们都有一个共性就是这些类没有自己的状态,无论实例化多少次它们都是一样的,如果不做成单例模式的话应用里就有可能会有很多很多一摸一样的实例,这样会造成内存的浪费,GC工作量的增加等。

单例模式还分懒汉式和饿汉式,在这里我只说一说比较常用的懒汉式吧,毕竟饿汉式我认为还是比较不太实用的,毕竟没用到的时候也会实例化一个对象。

懒汉式单例:

package test;

public class Singleton {
	// 构造函数私有化
	private Singleton() {}

	// 静态实例
	private static Singleton singleton;

	// 静态方法返回一个实例
	public static Singleton getInstance() {
		if(null == singleton) {
			singleton = new Singleton();
		}
		return singleton;
	}

}

1. 构造函数私有化。这样,在客户端就无法创造实例了。

2. 静态实例。带有static的属性在每一个类中都是唯一的。

3. 静态方法返回一个实例。注意一定要是静态方法,如果不是静态方法,那就需要实例化一个对象才能使用这个方法,这与单例模式互相矛盾了。。。与实际不符。

但是上述的单例模式还是存在很多问题的,比如在并发的情况下,有多个线程同时进入getInstance方法,同时进入if判断并判断到静态实例为null,这样就会实例化多个Singleton出来,单例模式就崩塌了。。。

为了避免并发情况下的错误,单例模式可以是这样的:

package test;

public class Singleton {
	// 构造函数私有化
	private Singleton() {}

	// 静态实例
	private static Singleton singleton;

	// 静态方法返回一个实例 
	public static synchronized Singleton getInstance() {
		if(null == singleton) {
			singleton = new Singleton();
		}
		return singleton;
	}

}

这次我们给返回实例的方法加上synchronized,将整个方法同步,虽然可行,但好像问题也很大,设想在一个高并发的环境下,有一个线程访问了这个方法,其他线程都得处于无谓的等待状态,这种设计太烂了。

其实同步的地方只需在实例还未实例化的时候,在实例化之后就没有必要再进行同步控制了,所以可以将上面的单例模式改成很多地方都出现过的单例模式的版本,称为双重加锁

package test;

public class Singleton {
	// 构造函数私有化
	private Singleton() {}

	// 静态实例
	private static Singleton singleton;

	// 静态方法返回一个实例
	public static Singleton getInstance() {
		if(null == singleton) {
			synchronized(Singleton.class) {
				if(null == singleton) {
					singleton = new Singleton();
				}
			}
		}
		return singleton;
	}

}

这样的单例模式看起来就比较完美了,例如有两个线程判断到实例为null,有一个线程进入到synchronized里,第二个线程等待进入,第一个线程实例化完成后释放锁,第二个线程进入synchronized,但里面还有一层判断,此时实例不为null,则不会进行实例化。在实例不为null的时候就不会进入同步块了,这样就节省了很多线程的无谓等待。

但是,上述的单例模式在我这种初学者眼中像是完美的状态,但如果深入到JVM的层面上来说,还是存在一些问题,因为JVM创建一个新的对象是分三步走的。

1. 分配内存

2. 初始化构造器

3. 将对象指向分配的内存的地址上

在1 2 3 这种步骤下上述的单例模式是没有问题的,但JVM会对字节码进行调优(调整指令的执行顺序),所以如果2 3的执行顺序对调,变成 1 3 2,那么如果JVM先给对象分配了内存,而还没开始初始化构造器时,有一个线程进入getInstance方法,会判断实例不为null,此时会直接返回一个引用,这时是在初始化构造器之前的,所以会产生一个莫名的错误。

我们在语言方面无法避免这个错误的发生,所以就有了另一种比较标准的单例模式:

package test;

public class Singleton {
	// 构造函数私有化
	private Singleton() {}

	// 静态方法返回一个实例
	public static Singleton getInstance() {
		return SingletonInstance.Instance;
	}
	//静态内部类中有静态属性创建实例
	private static class SingletonInstance {
		static Singleton Instance = new Singleton();
	}
}

上述的单例模式主要是利用了类中的静态属性JVM只会在第一次加载类时初始化的特点,所以静态属性只会初始化一次,符合单例。并发的情况下,初始化静态属性时JVM会强行同步这一过程,所以无需考虑并发访问的问题。

当然,上面双重加锁也还是有解决的方法,就是将静态实例属性加上关键字volatile,标识这个属性是不需要优化的。大概就是禁止了JVM自动的指令重排优化,也能解决问题。

在《Effective JAVA》一书中作者推荐使用枚举来写单例模式,这种方法既能解决多线程并发的问题,也能保证反序列化之后还是单例的问题。

关于反序列化的问题,我们给上述标准的单例模式实现序列化:

package test;

import java.io.Serializable;

public class Singleton implements Serializable {
	// 构造函数私有化
	private Singleton() {
	}

	// 静态方法返回一个实例
	public static Singleton getInstance() {
		return SingletonInstance.Instance;
	}

	// 静态内部类中有静态属性创建实例
	private static class SingletonInstance {
		static Singleton Instance = new Singleton();
	}
}

然后在另一个test里对此单例进行反序列化(输入流读取对象)

package test;

import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class testT {
	@SuppressWarnings("resource")
	public static void main(String[] args) throws Exception {
		ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\objFile.obj"));
		Singleton instance = Singleton.getInstance();
		out.writeObject(instance);
		out.close();

		ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\objFile.obj"));
		Singleton instance1 = (Singleton) in.readObject();
		in = new ObjectInputStream(new FileInputStream("D:\\objFile.obj"));
		Singleton instance2 = (Singleton) in.readObject();
		System.out.println("obj1 hashcode:" + instance1.hashCode());
		System.out.println("obj2 hashcode:" + instance2.hashCode());
		in.close();
	}
}

此时控制台打印的结果是:


这就说明了经过反序列化之后对象不再是单例模式,为了解决这个问题,书中提到需在单例类中实现readResolve方法。

package test;

import java.io.Serializable;

public class Singleton implements Serializable {
	// 构造函数私有化
	private Singleton() {
	}

	// 静态方法返回一个实例
	public static Singleton getInstance() {
		return SingletonInstance.Instance;
	}

	// 静态内部类中有静态属性创建实例
	private static class SingletonInstance {
		static Singleton Instance = new Singleton();
	}
	//实现readResolve
	private Object readResolve() {
		return SingletonInstance.Instance;
	}
}

此时控制台打印的结果为:


此时解决了反序列化不是单例的问题。至于为什么要实现readResolve,这个方法并不是Serializable接口的方法,在此没有对其进行深究,但估计应该是在反序列化时会调用这个方法吧。

猜你喜欢

转载自blog.csdn.net/qq_41737716/article/details/80510539