文章目录
一. 问题背景
公司的电商项目,创建订单的过程中,涉及加载sku信息,又涉及到加载sku库存信息。加载sku库存信息先去缓存查库存,查不到再去数据库,获取到数据再写进缓存。加载sku库存信息这里用到了锁机制,类似于单例懒加载的双重检查机制。锁是用了Redis分布式锁。 笔者对单例懒加载、Redis分布式锁都不了解,因此来研究一下。
参考自:
二. 什么是单例模式?
在整个应用系统中,保证一个类只有一个实例对象,实现这种功能的方式叫单例模式。
三. 为什么要用单例模式?
- 节省公共资源。 比如数据库连接,频繁创建、销毁数据库链接会占用很多资源。用一个数据库连接池来管理数据库连接,只要数据库连接池是单例的,那么当我用完数据库连接后就释放并交给连接池管理。等到其他人要用数据库连接则再从连接池里面拿。这样就可以避免经常创建、销毁数据库连接。除了连接池,还有日志管理、打印机、应用配置都可以用单例模式。
- 单例模式方便控制。 比如日志管理,多个线程同时写日志,你写一下我写一下,日志都还没写完整就被另一个线程接着写,会造成日志互窜。用单例模式控制日志的正确性,只能一个一个按照顺序来写,而单例模式只有一个人来向日志里写入方便控制,避免了这种多人干扰的问题出现。
四. 实现单例模式的思路
- 构造私有。 要保证一个类不能多次被实例化,就要阻止对象被new出来,所以需要把类的所有构造方法私有化。
- 以静态方法返回实例。 外界不能通过new获得对象,所以我们要弄一个类方法让外界获取对象实例。
- 确保对象实例只有一个。 只对类进行一次实例化,以后都直接获取第一次实例化的对象。
五. 懒汉模式
懒汉模式的意思是,我先不创建类的实例对象,等需要用的时候再创建。
5.1 最简单的懒汉模式
/**
* @description: 非线程安全的懒加载单例
* @author: ganzalang
*/
@Data
public class UnsafeLazyLoadSingleton {
private static UnsafeLazyLoadSingleton singleton;
private UnsafeLazyLoadSingleton() {
}
public static UnsafeLazyLoadSingleton getInstance() {
if (singleton == null) { // 1
singleton = new UnsafeLazyLoadSingleton(); // 2
}
return singleton; // 3
}
}
解释:如上所示,这是最简单的懒汉模式。但是它也有缺点。假如现在有2个线程分别为A、B,当A执行到3处,B执行到2,那么A线程得到return出来的是B线程new出来的。这并不是我们想要的,我们要的是A线程getInstance出来的对象,而不是B线程的。
两个线程同时进入方法造成了问题,我们想到的是给方法加锁
5.2 给方法加锁
/**
* @description: 给方法加锁
* @author: ganzalang
*/
@Data
public class SynchronizedMethodLazyLoadSingleton {
private static SynchronizedMethodLazyLoadSingleton singleton;
private SynchronizedMethodLazyLoadSingleton() {
}
public static synchronized SynchronizedMethodLazyLoadSingleton getInstance() {
if (singleton == null) {
singleton = new SynchronizedMethodLazyLoadSingleton();
}
return singleton;
}
}
解释:如上所示,虽然给方法加锁可以避免前面造成的问题。但是如果频繁地访问这个对象,那么这种频繁加锁和释放锁方式就会产生严重的效率问题。
我们需要再优化一下
5.3 双重检查锁定
/**
* @description: 双重检查锁定
* @author: ganzalang
*/
@Data
public class DoubleCheckLazyLoadSingleton {
private static DoubleCheckLazyLoadSingleton singleton;
private DoubleCheckLazyLoadSingleton() {
}
public static DoubleCheckLazyLoadSingleton getInstance() {
if (singleton == null) {
synchronized (DoubleCheckLazyLoadSingleton.class) {
if (singleton == null) {
singleton = new DoubleCheckLazyLoadSingleton();
}
}
}
return singleton;
}
}
解释:如上所示,虽然加了锁和双重判断,但其实这个类是线程不安全的。现象是线程A进入同步块创建实例的时候,线程B会返回一个没有初始化的Instance对象。原因是JIT编译器会优化,会在编译时会发生“重排序”的状况。
5.4 指令重排序
编译器遇到
singleton = new xxxxx();
会分为如下三个步骤:
memory = allocate() // 1. 分配对象的内存空间
ctorInstance(memory) // 2. 初始化对象
instance = memory; // 3. 设置instance指向刚分配的内存地址
Java语言规范没有规定编译器的优化不会改变单线程的执行结果,但是并没有对多线程做出这样的保证。多线程情况下有时候编译器会对指令进行重排序优化,它可能把2和3颠倒过来,如下:
memory = allocate() // 1. 分配对象的内存空间
ctorInstance(memory) // 2. 初始化对象
instance = memory; // 3. 设置instance指向刚分配的内存地址
那么再分析双重检查锁定的情况:
/**
* @description: 双重检查锁定
* @author: ganzalang
*/
@Data
public class DoubleCheckLazyLoadSingleton {
private static DoubleCheckLazyLoadSingleton singleton;
private DoubleCheckLazyLoadSingleton() {
}
public static DoubleCheckLazyLoadSingleton getInstance() {
if (singleton == null) { // 1
synchronized (DoubleCheckLazyLoadSingleton.class) { // 2
if (singleton == null) { // 3
singleton = new DoubleCheckLazyLoadSingleton(); // 4
}
}
}
return singleton; // 5
}
}
当线程A执行到4时,编译器优化
new xxx()
的3个指令,先执行了instance = memory
。然后线程B执行到1时,发现singleton不为null,那么就获得了singleton对象。然而这个singleton对象其实还没有经过ctorInstance(memory)
,这是不正确的。
解决双重检查锁定的2个方案:把singleton变成volatile类型禁止指令重排序
5.5 禁止指令重排序
给singleton对象加
volatile
/**
* @description: 加volatile禁止指令重排序
*/
public class VolatileDoubleCheckLazyLoadSingleton {
private volatile static VolatileDoubleCheckLazyLoadSingleton singleton;
private VolatileDoubleCheckLazyLoadSingleton() {
}
public static VolatileDoubleCheckLazyLoadSingleton getInstance() {
if (singleton == null) { // 1
synchronized (DoubleCheckLazyLoadSingleton.class) { // 2
if (singleton == null) { // 3
singleton = new VolatileDoubleCheckLazyLoadSingleton(); // 4
}
}
}
return singleton; // 5
}
}
用volatile修饰singleton,编译器不会对
new xxx()
的指令进行重排序,详情可参考《深入理解Java虚拟机》
/**
* @description: 加volatile禁止指令重排序
*/
public class VolatileDoubleCheckLazyLoadSingleton {
private volatile static VolatileDoubleCheckLazyLoadSingleton singleton;
private VolatileDoubleCheckLazyLoadSingleton() {
}
public static VolatileDoubleCheckLazyLoadSingleton getInstance() {
if (singleton == null) { // 1
synchronized (DoubleCheckLazyLoadSingleton.class) { // 2
if (singleton == null) { // 3
singleton = new VolatileDoubleCheckLazyLoadSingleton(); // 4
}
}
}
return singleton; // 5
}
}
用volatile修饰singleton,编译器不会对
new xxx()
的指令进行重排序,详情可参考《深入理解Java虚拟机》