单例模式详解

单例模式详解

一、单例模式分类

单例模式按照加载时间可以分为两种:

  • 懒汉式
  • 饿汉式

二、各种单例模式详解

2.1 饿汉式
public class Singleton {
    private static Singleton singleton = new Singleton();

    private Singleton() {
    }
    
    public static Singleton getSingleton() {
        return singleton;
    }
}

​ 饿汉式单例模式在程序运行时,对象就会被创建。每次调用getSingleton方法时,都会返回同一个对象。饿汉式单例不存在线程安全问题。

2.2 懒汉式
public class Singleton implements Serializable{
    private static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton() {
        if (null == singleton) {
            singleton = new Singleton();
            return singleton;
        }
        return singleton;
    }
}

​ 在单线程中,该单例模式没有什么问题,但是在多线程程序中,该单例模式就存在线程安全问题。当第一次有多个线程调用getSingleton时,可能会产生多个singleton对象。为了解决该问题最直观的办法就是在getSingleton加锁,使在多线程情况下该方法串行化执行,保证线程安全。代码如下:

public class Singleton {
    private static Singleton singleton;

    private Singleton() {
    }

    public static synchronized Singleton getSingleton() {
        if (null == singleton) {
            singleton = new Singleton();
            return singleton;
        }
        return singleton;
    }
}

​ 该方法可以解决线程安全问题,但是由于初始化对象和每次获取对象都要通过锁,导致性能大大下降。而实际我们只是希望在初次初始化对象时保证线程安全,在之后获取对象不存在线程安全问题,所以就产生了双重检查锁。代码如下:

public class Singleton implements Serializable {
    private static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton() {
        if (null == singleton) {
            synchronized (Singleton.class) {
                if (null == singleton) {
                    singleton = new Singleton();
                    return singleton;
                }
            }
        }
        return singleton;
    }
}

​ 但是仔细分析,该方法还是有问题,因为new Singleton()这个初始化对象可以分为三个操作,1、分配内存空间;2、初始化对象;3、设置实例化对象指向分配的地址。在一些JIT编译器中,为了达到优化目的,可能会对这三个操作进行重排序。当2和3顺序颠倒后,就可能发生线程安全问题,原因是当初始化对象后,但是对象还没有指向分配的内存地址,而此时有另一个线程运行到第一个if(null==singleton)这行代码,就会判断为false,然后就会去获取对象,从而发生错误。虽然这种几率很小,并且并不是所有的编译器都会对上述操作进行重排序,但是该代码始终是存在问题的代码,所以就需要去解决。

重排序:重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。 --《java并发编程的艺术》

​ 通过对该种情况分析,有两种解决思路:

  1. 禁止重排序
  2. 在实例化操作完成前,不允许其他线程看到该对象实例化的具体过程(也就是在实例化完成前,其他线程读取的情况会一直为null)。

具体实现:

第一种情况我们理所当然会想到volatile关键字,因为它可以禁止指令重排序。具体实现代码如下:

public class Singleton {
	private volatile static Singleton singleton;

	private Singleton() {
	}

	public  static Singleton getSingleton(){
		if (null == singleton) {
			synchronized (Singleton.class) {
				if (null == singleton) {
                    singleton = new Singleton();
                    return singleton;
				}
			}
		}
		return singleton;
	}
}

第二种方案不容易想到具体代码实现,《java并发编程的艺术》中是指可以通过静态工厂实例化对象来实现(章节:3.8.3),具体实现代码:

public class Singleton {
  private volatile static Singleton singleton;

  public Singleton() {
  }
}
public class SingletonFactory {
	private static class SingletonHolder {
		public static Singleton singleton = new Singleton();
	}
	public static Singleton getSingleton() {
		return SingletonHolder.singleton;
	}
}

​ 该种方法主要是基于JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁,这个锁可以同步多个线程对同一个类的初始化。这种方法其实和饿汉式类似,只是通过静态工厂来实现再需要该对象时再进行加载。但是上面的代码并完善,因为是由两个类来实现单例,单例对象的构造方法也是公共的(能够new出新的对象)。因此最好将单例实现放在一个类中,并能达到懒加载的效果,代码如下:

public class Singleton {
	private static class SingletonHolder {
		private static final Singleton INSTANCE = new Singleton();
	}

	private Singleton () {}
	public static Singleton getSingleton() {
		return SingletonHolder.INSTANCE;
	}
}
2.3 枚举实现单例

​ 严格的来说,枚举也是懒汉式单例,因为只有在枚举类被调用后才会实例化该单例。具体代码如下:

public enum Singleton {
    INSTANCE;

    public void doEverything() {
    }
}

枚举类实现的单例不仅是线程安全的,而且不会被序列化和反射破坏,这是其他形式的单例不能达到的,这也是枚举实现单例备受推崇的原因。

三、破坏单例模式的各种情况

​ 单例能够减少重复创建对象,提高性能。但是单例有时是能够被破坏的,下面我们来了解单例可能被破坏的情况。

3.1 反射破坏单例
public class Singleton {
    private static Singleton singleton = new Singleton();

    private Singleton() {
    }

    public static Singleton getSingleton() {
        return singleton;
    }
}

public class DestroySingleton {
    public static void main(String[] args) throws Exception {
        Class clazz = Class.forName("com.yuan.Singleton");
        Constructor con = clazz.getDeclaredConstructor();
        con.setAccessible(true);
        Singleton singleton1 = (Singleton) con.newInstance();
        Singleton singleton2 = Singleton.getSingleton();
        System.out.println(singleton1 == singleton2);
    }
}

输出结果:false

​ 结果表明,通过反射将会实例化一个新的对象,从而破坏单例模式。但是当采用枚举实现单例时,由于枚举类实现反射会报错,从而可以避免单例破坏。

3.2 序列化破坏单例
public class Singleton implements Serializable{
    private static Singleton singleton = new Singleton();

    private Singleton() {
    }

    public static Singleton getSingleton() {
        return singleton;
    }
}

public class DestroySingleton {
    public static void main(String[] args) throws Exception {
        //write Object
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\file.txt"));
        oos.writeObject(Singleton.getSingleton());
        //read Object
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\file.txt"));
        Singleton singleton = (Singleton) ois.readObject();
        System.out.println(singleton == Singleton.getSingleton());
    }
}

输出结果:false

​ 通过代码知道,序列化会实例化一个新对象。但是当单例对象添加readResolve方法时,可以避免单例被破坏

立即加载:

public class Singleton implements Serializable{
    private static Singleton singleton = new Singleton();

    private Singleton() {
    }

    public static Singleton getSingleton() {
        return singleton;
    }

    /**
     * 添加该方法可以避免单例被破坏
     */
    public Object readResolve() {
        return Singleton.getSingleton();
    }
}

public class DestroySingleton {
    public static void main(String[] args) throws Exception {
        //write Object
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\file.txt"));
        oos.writeObject(Singleton.getSingleton());
        //read Object
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\file.txt"));
        Singleton singleton = (Singleton) ois.readObject();
        System.out.println(singleton == Singleton.getSingleton());
    }
}

输出结果:true

懒加载:

public class Singleton implements Serializable{
    private volatile static Singleton singleton;

    private Singleton() {
    }

    public static Singleton getSingleton() {
        if (null == singleton) {
            synchronized (Singleton.class) {
                if (null == singleton) {
                    singleton = new Singleton();
                    return singleton;
                }
            }
        }
        return singleton;
    }

    /**
     * 添加该方法可以避免单例被破坏
     */
    public Object readResolve() {
        return Singleton.getSingleton();
    }
}

public class DestroySingleton {
    public static void main(String[] args) throws Exception {
        //write Object
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("E:\\file.txt"));
        oos.writeObject(Singleton.getSingleton());
        //read Object
        ObjectInputStream ois = new ObjectInputStream(new FileInputStream("E:\\file.txt"));
        Singleton singleton = (Singleton) ois.readObject();
        System.out.println(singleton);
        System.out.println(Singleton.getSingleton());
        System.out.println(singleton == Singleton.getSingleton());
    }
}

​ 其实就是java约定,在实例化后会再判断该实例是否有该方法,如果有该方法则会调用该方法将返回结果返回。这里要注意的是立即加载模式下readResolve方法可以直接返回singleton这个类变量,但是在懒加载中这种做法不建议,因为如果程序启动没有获取过对像实例而是直接通过序列化来获得,就会返回一个空值。

四、总结

​ 单例实现的方式有很多,有线程安全也有不安全的,所以需要根据实际业务具体分析,再决定采用。主要考虑的并发、反射以及序列化。

posted @ 2020-07-23 17:44  何故愁为河边柳  阅读(181)  评论(0编辑  收藏  举报