算算正式工作也有一年了,准备边学习,边把一些常用的设计模式整理成博客,用我自己理解的方式描述出来.设计模式就是前人对工作中经常遇到的情况得到的经验总结,主要针对于代码的复用,提高代码的可靠性.
单例模式是非常常用的一种设计模式,在Spring容器中创建的Bean默认是单例的,Servlet也是单例的.单例模式理解起来简单.但是懒汉单例模式实现起来却不是那么简单的.
一.什么叫单例模式
JVM所管理的内存也就是堆中只存在某个类的唯一一个实例,通过把构造函数私有化, 通过一个静态方法提供唯一实例。单例模式经常被分为饿汉单例模式以及懒汉单例模式.饿汉单例模式是在类初始化的时候创建自己的实例.懒汉模式是在需要实例的时候再创建实例.
二.单例模式优点与缺点
优点:内存中只存在一个实例所以减少了内存开销,针对一些创建和销毁对象时开销比较的情况单例模式可以减小系统性能开销.
缺点:多线程下并发问题.
三.单例模式在jvm垃圾回收的时候的处理
在网上看到有人提到了关于单例模式会不会被垃圾回收.我能证明的就是在Hotspot虚拟机中是不会被回收的.Hotspot虚拟机采用的可达性分析算法.单例类Class的referenceon永远指向之前的实例.而方法区中的Class对象不会被垃圾回收.所以单例是一直可达的,因此单例模式在Hotspot虚拟机中不会被回收.
四.单例模式的代码实现
/*饿汉单例模式 */
/*饿汉单例模式 */
public class HungryManSingleton {
private final static HungryManSingleton instance = new HungryManSingleton();
private HungryManSingleton() {
super();
}
public static HungryManSingleton getInstance(){
return instance;
}
}
/*懒汉单例模式 (多线程下不安全)*/
/* 懒汉单例模式 */
public class LazyManSingleton {
private static LazyManSingleton instance = null;
private LazyManSingleton() {
super();
}
public static LazyManSingleton getInstance() {
if (instance == null) {
synchronized (LazyManSingleton.class) {
if (instance == null) {
instance = new LazyManSingleton();
}
}
}
return instance;
}
}
这里懒汉单例模式相对于饿汉单例模式是在第一次调用getInstance()方法的时候再创建,个人觉得更好的节省内存空间.
不过懒汉单例模式会有并发的问题,这个并发的问题就是在多个线程进入getInstance()中第一个线程在new对象还没完成的时候,另外的线程也会判断instance == null然后创建对象,这个时候内存重复创建了多个对象,不过多创建的对象会在后面被JVM垃圾回收.解决这个并发问题简单做法就是在getInstance方法上面加上sychronized,但是除了第一次创建的时候需要解决并发的问题,所以在getInstance()方法前面加上synchronized并不好。那么换个思路我们在在方法内部new实例的时候再进行锁,这样降低了锁的范围.这里需要两次判断,在获取锁的时候再判断一次.你以为这样就解决了并发的问题了吗?
果然,这样还是会产生并发的问题.有人就不理解了为什么两次检查还会出现并发的问题.我们来梳理一下问题在哪里.对于JVM来说instance=new LazyManSingleton();其实分成N个原子步骤来完成的.最重要的3个步骤是1.在堆里分配内存,2初始化构造器.3.将对象在堆中的内存地址赋值给instance变量.Java虚拟机执行指令的时候有个优化的过程,可能回乱序执行,先执行1和3,再执行2.当一个线程执行先完2的时候,这时候instance已经有了值,其他线程就会返回instance,在2.类的构造器还没有执行的时候使用instance就会产生错误了.
volatile就能解决这个问题.因为volatile有两个重要功能1.禁止指令重排序,2.保证变量修改的可见性.所以在修饰instance的时候使用volatile修饰即可.
懒汉单例模式 (线性安全)
/* 懒汉单例模式 */
public class LazyManSingleton {
private volatile static LazyManSingleton instance = null;
private LazyManSingleton() {
super();
}
public static LazyManSingleton getInstance() {
if (instance == null) {
synchronized (LazyManSingleton.class) {
if (instance == null) {
instance = new LazyManSingleton();
}
}
}
return instance;
}
}
五.单例模式的序列化
当对单例模式进行反序列化的时候,堆内存会多出一个实例,这样就破坏了单例模式.
序列化单例模式对象
public static void main(String[] args) throws FileNotFoundException, IOException, ClassNotFoundException{
HungryManSingleton instance = HungryManSingleton.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("d:/test.txt"));
oos.writeObject(instance);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("d:/test.txt"));
Object obj = ois.readObject();
System.out.println(obj == instance);
}
输出结果是false,显然反序列化的时候出现了两个对象.
解决办法:在HungryManSingleInstance中添加readResovle()方法,ObjectInput执行readObject()方法的时候,如果反序列化的类有readResovle()方法回去执行readResovle()方法;
单例模式序列化,使用readResolve()
public class HungryManSingleton implements Serializable{
private final static HungryManSingleton instance = new HungryManSingleton();
private HungryManSingleton() {
super();
}
public static HungryManSingleton getInstance(){
return instance;
}
public Object readResolve(){
return instance;
}
}
六.枚举实现单例
大家看到前面那么多,感觉光是一个单例模式是不是不是那么简单.需要考虑那么多问题.那么有没有更简单的方法去实现单例呢?当然有的,那就是枚举.利用枚举的特性,轻而易举的就实现了单例.不用考虑并发问,序列化这些头疼的问题了.
public enum EnumInstance {
INSTANCE;
}
七.单例模式的扩展--多例模式
也许有时候会有这样的需求,一个类需要固定个数的对象.这时候就可以把单例模式扩展成多例模式.
/*单例模式扩展----多例模式 */
public class MultiInstance {
private static int MAX_INSTANCE_COUNT = 3;
private static List<MultiInstance> instancePool = new ArrayList<MultiInstance>();
static{
for(int i = 0 ; i < MAX_INSTANCE_COUNT ; i ++ ){
instancePool.add(new MultiInstance());
}
}
private MultiInstance() {
super();
}
public static MultiInstance getInstance() {
return instancePool.get(new Random().nextInt(3));
}
}