浅析单例模式
单例是什么?
单例是指某个类在整个系统中只有且仅有一个实例对象,即第一次创建后,后续创建的实例都是返回第一次创建时的实例。
单例有什么用?
单例模式保证类在系统中只有一个实例,在某些应用中很实用,如定义一个全局的ID计数器,定义系统的运行环境等,都需要保证系统只存在一个实例对象。
单例模式的特点
- 单例类必须只有一个实例
- 单例类必须只有私有构造函数
- 单例类必须对外提供获取唯一的实例的接口(或者说方法)
单例模式如何实现?
单例模式的实现方式有很多种,常见的有懒汉式和饿汉式实现方式,除此之外还有利用枚举实现、利用静态内部类实现等,以下将一一介绍:
饿汉式单例模式
这种实现方式是在类加载的时候就创建出唯一的实例对象,所以称之为“饿汉”,如下实现代码:
/**
* Hungry Singleton
* @author Horizon
*
* @Date 2019年6月4日下午8:13:21
*/
public class Singleton {
// 静态变量,因需提供静态工厂方法给外部访问,直接new出来,在类加载时就创建实例
private static Singleton instance = new Singleton();
// 私有构造函数,禁止外部直接构造
private Singleton() {}
// 静态工厂方法,为外部提供唯一的实例
public static Singleton getInstance() {
return instance;
}
}
这种方式的好处就是简单方便,而且能够在多线程并发获取实例时,保证实例对象的唯一性,即不存在线程安全问题。但缺点就是如果该类未被使用到时,实例对象也会存在系统中,占用系统的资源。
懒汉式单例模式
懒汉式实现就是为了解决饿汉式在类加载时就创建类实例的缺点,采用了延迟加载思想,在需要用到的时候再创建。
线程不安全实现(一)
/**
* Lazy Singleton, Thread Unsafe-1
* @author Horizon
*
* @Date 2019年6月4日下午8:29:20
*/
public class Singleton {
// 静态变量
private static Singleton instance;
// 私有构造
private Singleton() {}
// 静态工厂方法
public static Singleton getInstance() {
if (instance == null) { // 1
instance = new Singleton(); // 2
}
return instance; // 3
}
}
以上的实现代码在单线程运行下可以实现单例,但如果是在多线程环境下运行的话,就可能会创建出两个以上实例的情况。
比如:假设有2个线程同时执行getInstance方法,Thread1先进行第1步的判断为空,进入if语句内部,还未执行第2步时,执行权给到了Thread2,此时Thread2进行if判断还是为空,执行第2、3步后返回了实例,此时切回Thread1继续执行第2、3步返回实例,这样两个线程就返回了两个不同的实例,违背了单例模式的定义。
线程不安全实现(二)
/**
* Lazy Singleton Thread Unsafe-2
* @author Horizon
*
* @Date 2019年6月4日下午8:45:34
*/
public class Singleton {
// 静态变量
private static Singleton instance;
// 私有构造
private Singleton() {}
// 静态工厂方法
public static Singleton getInstance() {
if (instance == null) { // 1
synchronized (Singleton.class) { // 2
if (instance == null) { // 3
instance = new Singleton(); // 4
}
}
}
return instance; // 5
}
}
以上实现代码采用了“双重检锁”,第1步的存在是为了当实例被创建后,再次访问时避免加锁操作;当有多个线程通过第1步,对第2步锁的竞争只有一个线程能获得锁,其余的都将阻塞在第2步,第3步的存在是为了让被阻塞在第2步的线程被唤醒后进行第二次判断,如果前面已有线程执行了第4步创建了实例,那么后续的线程就不应该执行第4步了。
双重检锁线程安全实现
以上的实现代码看似已经没有问题了,但其实还存在一个比较隐蔽的问题,这涉及到JVM指令重排序问题,第4步的执行在JVM层面被分为3个步骤完成:
- 为实例对象分配内存空间
- 执行构造,初始化等操作
- 变量引用指向实例对象
以上对象创建的第2、3步可能会被重排序,即先执行3再执行2,那么当线程执行了3后,instance已经有了引用,此时另一个线程来到第一个if判断后不为空就返回了instance,此时的instance是未执行第2步的操作的,而当原来线程返回执行第2步后返回时,就出现了两个不同的实例了。
这就是为什么变量要使用volatile修饰的原因了,因为volatile修饰的变量可以禁止指令的重排,这样就不会在对象还未实例化完成就被引用了。
双重检锁线程安全代码实现:
/**
* Lazy Singleton Thread Safe
* @author Horizon
*
* @Date 2019年6月4日下午9:21:34
*/
public class Singleton {
// 静态变量,volatile修饰
private static volatile Singleton instance;
// 私有构造
private Singleton() {}
// 静态工厂方法
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
枚举实现单例
/**
* Enum Singleton Thread Safe
* @author Horizon
*
* @Date 2019年6月4日下午9:46:35
*/
public enum Singleton {
INSTANCE;
}
借助JDK1.5中添加的枚举来实现单例模式。不仅能避免多线程同步问题,而且还能防止反序列化重新创建新的对象
静态内部类实现单例
/**
* Static Inner class, Thread Safe
* @author Horizon
*
* @Date 2019年6月4日下午9:51:49
*/
public class Singleton {
// 私有构造
private Singleton() {}
// 静态内部类
private static class SingletonHelp {
// 单例实例
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
// 此方法被调用时才会加载Handler类并创建单例实例
return SingletonHelp.INSTANCE;
}
}
这种实现方式结合了“饿汉”使用类加载时创建实例和“懒汉”延迟加载的特点,在加载单例类时,静态内部类并不会加载,因此不会创建单例实例,此为延迟加载的特点,等到getInstance方法被调用时,静态内部类被加载并创建了单例实例,此为类加载时创建实例的特点。
为什么反序列化会破坏单例模式?
反序列化会通过反射调用单例类的构造函数,这样就破坏了单例类私有构造函数的作用了,可以通过在单例类中自定义readResolve方法返回单例实例,这样在反序列化时会调用自定义的readResolve方法,关于为什么会调用,可以去看看ObjectInputStream的readOrdinaryObject方法的实现。
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
// 私有构造
private Singleton() {}
// 静态内部类
private static class SingletonHelp {
// 单例实例
private static final Singleton INSTANCE = new Singleton();
}
public static Singleton getInstance() {
// 此方法被调用时才会加载Handler类并创建单例实例
return SingletonHelp.INSTANCE;
}
// 此方法防止反序列化破坏单例模式,可将此方法注释测试看看
private Object readResolve() {
return SingletonHelp.INSTANCE;
}
public static void main(String[] args) throws Exception {
Singleton instance = getInstance();
System.out.println(instance);
try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("D:\\tmp.out"));
ObjectInputStream in = new ObjectInputStream(new FileInputStream("D:\\tmp.out"))) {
out.writeObject(instance);
Singleton s = (Singleton) in.readObject();
System.out.println(s);
}
}
}
上一篇: 鼠标拖动事件
下一篇: 鼠标拖动页面控件的重要技巧