单例模式(设计模式的一种)
顾名思义, 就是单个实例. 在一个Java进程中, 要求指定的类, 只能有唯一一个实例. 通过特殊技巧, 来保证这里的实例不会有多个. (尝试 new 多个实例的时候, 就会编译报错)
1. 饿汉模式
代码演示:
这样写, 这个类被加载的时候就会初始化已经创建好的静态成员变量, 由于创建时机非常早, 所以叫饿汉. 并且把构造方法设为私有, 那么后续再想 new 这个 Singleton 就会直接编译报错.
2. 懒汉模式
创建实例的时机就不一样了, 只有在第一次使用的时候, 才会创建实例.
这个引用指向唯一实例, 先不着急创建, 把它初始化为 null , 如果是第一次调用 getInstance(), 才会创建实例, 后面再调用, instance 已经不为空了, 也就不会再创建实例了.
这样设定, 仍然能保证该类的实例是唯一一个, 与此同时, 创建实例的时机就不是程序驱动时了, 而是第一次调用 getInstance() 的时候, 这个操作的执行时机就不知道了, 甚至整个程序压根用不到这个方法, 也就把创建的操作省下了.
3. 关于线程安全
上面介绍的只是序幕~ 接下来讨论的就是关键问题:
上面编写的代码 (懒汉模式 和 饿汉模式) 是否是线程安全的??
如果在多线程下, 并发调用 getInstance() , 这两个代码是否是线程安全的呢??
1. 加锁
首先, 对于 饿汉模式 来说, 多个线程读同一个变量, 这是安全的.
而对于 懒汉模式 来说, 多个线程同时运行, 有可能会创建出多个实例, 线程不安全.
要想保证线程安全, 就需要把 if 和 new 打包成一个原子操作. 也就是 加锁 !
2. 省去无意义的加锁
代码到这一步, 还有点小问题~~
如果 instance 已经创建过了, 后面再调用 getInstance() 这个方法, 就只剩读操作了, 对于读操作还要加锁, 此时效率就会降低, 显然这是没必要的~
加锁就意味着产生阻塞, 一旦产生阻塞, 什么时候解锁, 这就不确定了, 所以一旦加锁, 就注定与 "高性能" 无缘了.
所以我们在外面再套一层 if.
可以看到 这两个 if 判断条件是一样的, 对于 单线程/非阻塞 来说, 这是无意义的, 但对于 多线程/可能阻塞的代码里, 这就很有必要了!! 这样的代码称为 "双重校验锁".
3. 指令重排序
那么到这一步, 还有最后一个问题 !!
是由于 指令重排序 引起的线程安全问题.
指令重排序也是编译器优化的一种方式. 调整原有代码的执行顺序, 在原有逻辑不变的情况下, 提高程序的效率.
instance = new SingletonLazy();
这一句代码, 做了三件事:
- 向内存申请一块空间
- 调用构造方法, 创造出这个实例
- 把这块内存地址赋值给 instance.
在多线程下, 指令重排序就可能引入问题了!
总结:
t1 有可能执行完 1 3 就被调度走了, 还没来得及创建实例, 这时 instance 指向的是 未被初始化 的对象, t2 执行判断的时候, instance 已经不为空了, 没有进入 if 触发加锁, 也就没有阻塞, 直接 return 了!! 如果 t2 继续使用 instance 里的属性和方法, 就会出现错误(因为里面的属性都是 未初始化的 "全0" 的值), 就可能会引起代码的逻辑出现问题.
4. volatile 关键字
解决方法: 使用 volatile 关键字!
volatile 有两个功能:
- 保持内存可见性. 每次读取变量必须重新访问内存, 不会优化到 寄存器/缓存 中.
- 禁止指令重排序. 针对被 volatile 修饰的变量相关的读写操作相关的指令, 是不能被重新排序的.
改进之后 :
所以, 关于 懒汉模式 完整的代码 :
class SingletonLazy {
private volatile static SingletonLazy instance = null;
public static SingletonLazy getInstance() {
//如果 instance 为空, 说明是首次调用, 就需要考虑线程安全问题, 要加锁.
//如果 instance 不为空, 就直接返回 instance.
//判断是否加锁
if(instance == null) {
synchronized (SingletonLazy.class) {
//判断是否要创建对象
if (instance == null) {
instance = new SingletonLazy();
}
}
}
return instance;
}
private SingletonLazy() { }
}