借鉴double-checked locking(双重检测锁)的思想解决了一个业务上小的并发问题
简单介绍一下double-checked locking:
双重检测锁是一种用于实现单例模式的锁机制,它在多线程环境下确保只有一个实例被创建。
在JAVA中,双重检测锁通常使用volatile关键字和synchronized关键字来实现。下面是一个简单的双重检测锁的实现示例:
public class Singleton {
private volatile static Singleton instance;
private Singleton() {}
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
在这个示例中,getInstance()方法首先检查instance是否已经被实例化,如果没有,则进入同步块。在同步块内部再次检查instance是否为null,如果是,则创建一个新的实例。使用volatile关键字可以确保instance在多线程环境下的可见性,而synchronized关键字可以确保在多线程环境下只有一个线程可以进入同步块创建实例。
为什么会有双重检测锁呢?
简单来说,就是我来获取对象的时候,不管这个对象创没创建,我都要先加锁才能完成这个操作,导致很慢。所有就有了双重检测锁。
实习中小的并发问题:
业务场景:
我们的业务就是从上游平台那里拿到一些订单,再从代理商那里的三方接口平台里拿到支付凭证,把订单和支付凭证匹配一起推给下游平台去支付。就是一个中间商赚差价的过程。
调用代理商的接口获取支付凭证的时候,它的Token设置的过期时间是30分钟,Token过期每刷新获取一次都要收取费用。如果在第三十分钟时Token刚好过期的时候,进来了一百的请求,权限校验失败,这一百的订单拿不到支付凭证,如果直接让这一百个请求失败的话,就会提高我们系统的失败率。如果不管,这一百的线程都会去同时刷新获取Token,那我们不得亏死。
解决方案
用ReentrantLock给刷新Token的逻辑方法加锁。让这一百个线程只执行一次更新Token的逻辑。
请求权限校验失败的话,三方接口报错的话就去判断Token是否过期。我们每一次更新Token的时候都会记录对应的Token的过期时间,这里我们用当前线程的时间与Token的过期时间来进行比较,判断Token是否过期。
Token过期,就执行refresh方法来刷新Token。
对获取更新的Token逻辑代码加锁,然后再做一次判断,这里我们还是用当前线程的时间和Token的过期时间来比较。如果当前线程的时间大于Token的过期时间,就说明Token还没有被获取更新,执行获取更新Token的操作。如果当前线程的时间小于Token的过期时间,就说明Token已经被之前的线程获取更新过了,就直接返回成功,执行自己的任务。
保证这一百个线程只执行一次获取更新Token的操作
为了防止线程获取Token失败,这个锁阻塞线程,所以给这个锁设置了200毫秒的过期时间,并且采用了自旋的机制。如果锁超时了,再去做一次判断,用当前线程的时间和Token的过期时间来比较。如果Token还没有获取更新成功,就去自旋重试三次,尽可能的保证Token被获取更新成功。
如果自旋三次还没有成功的话,就说明三方接口可出现系统挂了的类似的情况。让这一百个请求返回失败。三方系统挂了,我总不能还一直去获取更新Token,获取一次收一次费,我靠,那不寄了。