浅析Java设计模式——单例模式(1)

浅析Java设计模式——单例模式(1)

Java中有许多设计模式,总体分为3大类:创建型模式、结构型模式和行为型模式。创建型模式最常见也最简单的就是单例模式。单例模式,顾名思义就是一个类只能有一个对象(实例)。
单例模式总结有3个特点:
1. 单例类只能有一个对象实例。
2. 该类必须自己创建的唯一的实例。
3. 该类必须向系统中所有其他对象提供这个实例。

单例模式的初代版本(懒汉模式):
public class Singleton {
	private Singleton() { // 构造方法私有化
	}

	private static Singleton instance = null; // 单例对象

	// 静态工厂方法
	public static Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		}
		return instance;
	}
}
上面代码有以下3点解释:
  1. 一个类进行new操作时会默认执行其构造方法,要使其只能有一个对象自然不能随便new,故要将构造方法私有化。
  2. getInstance是获取单例对象的方法,其是静态工厂方法,返回值为单例对象。
  3. 上面方法中如果单例对象初值设为null,即没有创建,就创建单例对象并返回该值。

以上版本的单例模式其实存在一个问题:不能保证其线程安全。

为什么呢?现在有一这种情况:当Singleton类刚被初始化时,有两个线程同时访问getInstance方法,因为instance的值为空,故这两个线程都满足条件,最终的结果就是new语句被执行了两次。显然这不是我们需要的结果。

那么如何改进呢?这里我们使用同步synchronized来解决。getInstance方法属于临界区,临界区的内容需要互斥访问,将故获取单例对象的方法加上synchronized,保证每次操作只有一个线程访问。

单例模式线程安全版:
public class Singleton {
	private Singleton() { // 构造方法私有化
	}

	private static Singleton instance = null; // 单例对象

	// 静态工厂方法
	public static Singleton getInstance() {
		if (instance == null) {// 第一次检测
			synchronized (Singleton.class) {// 同步锁(锁住整个类)
				if (instance == null) {// 第二次检测
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}
该版本解释以下几点:
  1. synchronized同步锁必须使用类锁,锁住整个类。
  2. 判断条件检测了两次,保证只会创建一个对象实例。
当两个线程同时访问时,第一次检测都符合条件,故继续执行,但有同步锁,故线程1执行,执行完毕 instance 对象已经被创建并返回。这时候线程2进入临界区,先进行第二次检测,不为空故不进行new操作。最终结果保证了只有一个对象实例。

到这里觉得单例模式应该没什么问题了吧。其实加了synchronized同步锁还不能保证绝对的安全。为什么呢?这里和JVM编译器的指令重排有关。
在Java中代码 instance = new Singleton( ); 编译器进行编译成3句JVM指令:

memory = allocate( );//1. 给对象分配内存空间
ctorSingleton(memory);//2. 对象初始化
instance = memory;//3. 将singleton对象指向为其分配的内存空间

这3条指令的执行顺序不是确定的,也就是说会发生指令重排的现象。若现在发生指令重排,3条指令的执行顺序变成如下顺序:

memory = allocate( );//1. 给对象分配内存空间
instance = memory;//3. 将singleton对象指向为其分配的内存空间
ctorSingleton(memory);//2. 对象初始化

这时候当线程1执行完1和3,线程2又抢占到CPU资源,执行检测语句if(singleton == null)。但这时候得到的结果会是什么?false并返回一个没有初始化的对象。因为线程1虽执行了1和3指令,instance 的值已经不为null,但没有完成初始化。线程2过来判断时不满足为null的条件,所以是false。

所以虽然保证是一个对象,但这个对象是“残缺的”。这显然不是我们要的结果。解决办法就是利用关键字volatile。
简单说明一下,被volatile修饰的共享变量有两点重要的特性:
  1. 保证不同线程对这个变量操作的内存可见性。
  2. 禁止指令重排。
所以这里的改进办法 就是只 需在对象singleton前加上它,就可防止指令重排,问题得到解决。
单例模式线程安全改进版:
public class Singleton {
	private Singleton() { // 构造方法私有化
	}

	private volatile static Singleton instance = null; // 单例对象

	// 静态工厂方法
	public static Singleton getInstance() {
		if (instance == null) {// 第一次检测
			synchronized (Singleton.class) {// 同步锁(锁住整个类)
				if (instance == null) {// 第二次检测
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
}
被volatile修饰的变量在被编译成JVM的3条指令时始终保持顺序执行,即防止了指令重排的发生。
现在当线程1先执行,对线程2来说,其会指向一个完成了初始化的对象 instance ,不会出现对象“残缺”的现象。

单例模式的实现方法不止以上这几种,当然利用volatile的方法也还是存在漏洞的。本篇文章先介绍到这儿,关于单例模式更好的实现方法会在下次进行浅析。

猜你喜欢

转载自blog.csdn.net/qq_38190057/article/details/78955591