单例模式顾名思义就是该类只有一个实例对象,通过将构造函数私有化,使得类无法在外部通过构造函数实例化对象,对象的实例化由内部的代码控制达到控制单个实例对象的目的,类中提供公有的获取实例方法可以获取到该类唯一的实例化对象。
单例模式根据对象实例化的时间分为饿汉单例模式与懒汉单例模式。
- 饿汉单例模式是指在类初始化的时候就已经初始化生成单例对象。
- 懒汉单例模式是指在你想要获取实例对象的时候才实例化对象,属于懒加载。 饿汉单例模式示例:
public class HungrySingleton {
private static HungrySingleton hungrySingleton = new HungrySingleton();
private HungrySingleton() {
}
public static HungrySingleton getInstance() {
return hungrySingleton;
}
}
懒汉单例模式示例:
public class LazySingleton {
private static LazySingleton lazySingleton;
private LazySingleton() {
}
public static synchronized LazySingleton getInstance() {
if (lazySingleton == null){
lazySingleton = new LazySingleton();
}
return lazySingleton;
}
}
可以看到,饿汉单例模式虽然线程安全,但是当代码中包含大量的饿汉单例类的时候会在项目启动时就开始实例化所有对象,这无疑会增加整个项目启动的时间。
懒汉单例模式是在调用获取实例方法的时候才实例化对象,那么在实际情况中可能会有多个线程同时调用该获取实例方法,那么就会引发线程安全问题,破坏单例的性质,因此我们增加了同步关键字来保证其线程安全性,虽然线程安全的问题解决了,但是每个线程在获取实例的时候都需要去竞争锁导致效率下降,下面就开始对懒汉单例模式进行优化。
首先我们可以考虑,该线程安全问题是由于实例化的地方引起的那么就不需要对整个方法加锁,只需要对实例化的地方加锁不就可以提升效率,所以代码就变为下面的样子:
public class LazySingleton {
private static LazySingleton lazySingleton;
private LazySingleton() {
}
public static LazySingleton getInstance() {
synchronized(LazySingleton.class){
if (lazySingleton == null){
lazySingleton = new LazySingleton();
}
}
return lazySingleton;
}
}
可是这样也有问题,现在每个线程进来都要去同步代码块里去判断单例对象有没有被实例化,如果我们已经知道单例对象实例化过了,是不是就不需要再进入到同步代码块了呢?,所以可以对代码继续优化:
public class LazySingleton {
private static LazySingleton lazySingleton;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (lazySingleton == null) {
synchronized (LazySingleton.class) {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
}
}
return lazySingleton;
}
}
这就是DCL(Double-Check Locking)双检查锁机制来保证的线程安全,现在看起来所有问题都解决了,但是lazySingleton = new LazySingleton()语句正常的执行过程应该是这样的:
- 申请一块内存空间
- 在空间内实例化对象
- lazySingleton引用指向该内存地址 但是在通常情况下,CPU和编译器为了提升程序执行的效率,会按照一定的规则允许进行指令优化。也就是说有可能它的执行顺序为1-3-2。 假如现在有A、B两个线程需要获取单例实例,A线程进入到同步代码块判断lazySingleton为null开始实例化,此时A的执行顺序为1-3,刚刚执行完步骤3,线程B此时判断lazySingleton不为空则返回了该对象,虽然此时lazySingleton不为空,但是该引用指向的内存地址中并没有完整单例对象,那报错肯定是必然的,要解决该问题就是确保线程A对lazySingleton的修改必须要实时刷新到主内存,并且要防止指令重排,确保线程B要么拿到null等待进入同步代码块,要么拿到的是一个完整的对象,就需要加上volatile关键字,修改后的代码为:
public class LazySingleton {
private static volatile LazySingleton lazySingleton;
private LazySingleton() {
}
public static LazySingleton getInstance() {
if (lazySingleton == null) {
synchronized (LazySingleton.class) {
if (lazySingleton == null) {
lazySingleton = new LazySingleton();
}
}
}
return lazySingleton;
}
}
此时的懒汉单例模式就完整了。