ReadWriteLock 使用示例
1.使用场景
前两天同事发给我一个工具类,封装了调用第三方接口的方法。
第三方接口调用需要带上访问凭据accessToken ,需要先调用login接口传入username password获取该token.
他在方法内部,每个业务请求之前都login获取新的accessToken ,我认为这样做不优雅,因为accessToken 不是一次性的,是有有效时间的。
所以使用 ReadWriteLock 对工具类的进行了改造。
2.改造的目的
减少调用login接口的次数,无须每次调用业务接口都获取一次token; 那么Token就得缓存起来,进而不同线程独读写共享变量 就有并发问题:例如,读取缓存token时,无需加锁,各个线程之间的读是并发的 互不影响;但是当token失效时,就必须重新申请,这时就得刷新token的缓存,就涉及到了并发读写。所以,用到了ReadWriteLock 。最终,线程之间并发读 不受影响,但是同一时刻只有一个线程可以写,而且写的时候 不会有线程能读到token。还需要注意一个细节,有可能有多个线程同时触发了 申请token的条件,但最终只会有一个会成功申请token.如果有对ReadWriteLock的思考,欢迎留言讨论;
package com.example.demo;
import java.util.Date;
import java.util.Random;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
/**
* @author wang
* @date 2022/9/23 10:37
*/
public class ReadWriteLockDemo {
private static final ReentrantReadWriteLock READ_WRITE_LOCK = new ReentrantReadWriteLock();
private static final Lock READ_LOCK = READ_WRITE_LOCK.readLock();
private static final Lock WRITE_LOCK = READ_WRITE_LOCK.writeLock();
//初始化Token; 这里一般是spring 容器舒适化过程中,先发一个请求获取token
private AccessToken accessToken = new AccessToken(UUID.randomUUID().toString(), new Date());
static int i = 0;
private void login(AccessToken expireToken) {
WRITE_LOCK.lock();
try {
//如果this.accessToken 的值依然是 过期请求的值,才重新申请
if (expireToken.equals(this.accessToken)) {
//模拟http请求 获取 accessToken
System.out.println(Thread.currentThread().getName() + " 申请了新的Token");
//可以观察到 token申请期间 不会有doGet return相关数据
Thread.sleep(1000);
this.accessToken = new AccessToken(UUID.randomUUID().toString(), new Date());
} else {
System.out.println(Thread.currentThread().getName() + "其他线程已经更新过accessToken , 跳过申请token ");
}
} catch (InterruptedException e) {
throw new RuntimeException(e);
} finally {
WRITE_LOCK.unlock();
}
}
/**
* 业务请求,需要用到 Token
*/
public void doGet() throws InterruptedException {
boolean isExpire;
READ_LOCK.lock();
try {
//携带token 发起业务请求;由于不关注业务请求的返回值,只关注请求返回中 是否提示token失效;下面这行就代表发起业务请求
isExpire = accessToken.isExpire();
//如果token失效,需要重新登录
} finally {
READ_LOCK.unlock();
}
if (isExpire) {
System.out.println(Thread.currentThread().getName() + " token 过期 重新申请");
login(this.accessToken);
//重新发送一次业务请求
doGet();
} else {
System.out.println(Thread.currentThread().getName() + " 业务请求成功! return 相关数据");
i++;
}
}
public static void main(String[] args) throws InterruptedException {
ReadWriteLockDemo readWriteLockDemo = new ReadWriteLockDemo();
for (int i = 0; i < 100; i++) {
new Thread(() -> {
try {
readWriteLockDemo.doGet();
Thread.sleep(new Random().nextInt(3));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}).start();
Thread.sleep(500);
}
System.out.println(i);
}
}
package com.example.demo;
import java.time.LocalDateTime;
import java.util.Date;
/**
* @author wang
* @date 2022/9/23 11:14
*/
public class AccessToken {
public AccessToken(String value, Date createTime) {
this.value = value;
this.createTime = createTime;
}
private String value;
private Date createTime;
public boolean isExpire() {
try {
//模拟业务请求响应时间
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
//假设token有效期是 2 秒; 一般情况下,是发送业务请求之后 返回提示 token是否失效;
return (System.currentTimeMillis() / 1000) - (createTime.getTime() / 1000) > 2;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
}