Java中有许多设计模式,总体分为3大类:创建型模式、结构型模式和行为型模式。
分类 | 设计模式 | 关注点 |
---|---|---|
创建型模式 | 工厂模式、抽象工厂模式、单例模式、建造者模式、原型模式 | 关注于对象的创建,同时隐藏创建逻辑 |
结构性模式 | 适配器模式、过滤器模式、装饰模式、享元模式、代理模式、外观模式、组合模式、桥接模式 | 关注类和对象之间的组合 |
行为型模式 | 责任链模式、命令模式、中介者模式、观察者模式、状态模式、策略模式、模板模式、空对象模式、备忘录模式、迭代器模式、解释器模式、访问者模式 | 关注对象之间的通信 |
创建型模式最常见也最简单的就是单例模式。单例模式,顾名思义就是一个类只能有一个对象(实例)。
单例模式总结有3个特点:
-
单例类只能有一个对象实例。
-
该类必须自己创建的唯一的实例。
-
该类必须向系统中所有其他对象提供这个实例。
单例模式的初代版本(懒汉模式):
public class Singleton {
private Singleton() { // 构造方法私有化
}
private static Singleton instance = null; // 单例对象
// 静态工厂方法
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
上面代码有以下3点解释:
- 一个类进行new操作时会默认执行其构造方法,要使其只能有一个对象自然不能随便new,故要将构造方法私有化。
- getInstance是获取单例对象的方法,其是静态工厂方法,返回值为单例对象。
- 上面方法中如果单例对象初值设为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;
}
}
该版本解释以下几点:
- synchronized同步锁必须使用类锁,锁住整个类。
- 判断条件检测了两次,保证只会创建一个对象实例。
当两个线程同时访问时,第一次检测都符合条件,故继续执行,但有同步锁,故线程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修饰的共享变量有两点重要的特性:
- 保证不同线程对这个变量操作的内存可见性。
- 禁止指令重排。
所以这里的改进办法就是只需在对象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 ,不会出现对象“残缺”的现象。