为了实现更细粒度且有效率的控制ReentrantLock,自定义了一个支持细颗粒度和高效复用的锁池。
细粒度指使用一个自定义的key来动态声明一个或一组具体的资源,这个key值应当是动态的,非写死在代码里的,一旦声明了部分资源的所有权,这个key在锁池中必须是唯一的,而key对应的ReentrantLock对象和Lock对应的资源也当是唯一对应关系,key-lock-资源,一一对应。同时为了应对热点资源的高烈度竞争,避免热点锁对象的频繁创建和销毁,借用了缓存复用的思想,锁池采用了2Q也即FIFO-LRU队列的数据结构。
细颗粒度是通过维护锁池的key-value结构实现的,通过控制key来获得限制不同资源的锁。高效复用这一点是通过让锁池实现2Q缓存淘汰机制实现的。
具体实现是,查找锁时首先查找LRU策略锁池,如果LRU锁池中存在这把锁,则将这把锁移动到LRU锁池队尾,如果不存在则去FIFO策略锁池查找,如果FIFO锁池中存在这把锁,则视为2次命中,将这把锁转移到LRU锁池中,不存在则创建这把锁加入到FIFO锁池中。
FIFO锁池重写了LinkedHashMap的put方法,在添加新锁时检查锁池大小是否超过阈值,超过则淘汰队头未被持有的锁,并把新的锁插入队尾。LRU锁池重写了LinkedHashMap的removeEldestEntry方法,基于LinkedHashMap的模板方法实现在缓存命中时将锁移动到队尾,且在添加新锁时检查锁池大小是否超过阈值,超过则淘汰队头未被持有的锁。
考虑到2Q锁池在访问锁时总是会改变锁(也即一个缓存元素)的排序,所以访问锁的过程本身必须加锁,保证获取锁池行为的串行,这会对性能有一定程度的影响,于是新加入了重排序锁分段的设计,通过按业务类型来区分不同的锁池,保证不同业务的锁池在被访问时不会互相影响吞吐性能。
相比基础的淘汰最久未使用LRU策略,2Q策略一定程度避免了缓存污染问题,缓存命中率更高。具体FIFO和LRU锁池的容量阈值大小取决于业务类型,可以根据自身需求调整以实现更高效率。
业务意义:
相比于分布式锁,JVM级别的锁虽然不能在不同JVM之间保证线程安全,但是依然有存在的意义,例如降低分布式锁的竞争烈度,分布式锁的网络IO毕竟更加耗时,如果能在单一实例中先获得本地锁,然后再去竞争分布式锁的资源,可以大大降低分布式锁的竞争烈度,大大降低网络延迟带来的性能损耗。
2Q淘汰策略示意图:
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;
import org.springframework.util.Assert;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantLock;
/**
* @author hejun
* @version v1.0
* @Description 用于更细颗粒度控制ReentrantLock,并支持锁池2Q策略的工具类
* @date 2023/3/2
*/
@Slf4j
public class LockUtil {
// 锁池LRU缓存最大容量
@Getter
private static final int LRU_MAX_SIZE = 64;
// 锁池FIFO缓存最大容量
@Getter
private static final int FIFO_MAX_SIZE = 64;
// 初始锁池名称
private static final String DEFAULT_POOL_NAME = "DEFAULT_LOCK_POOL";
// 支持分段上锁的2Q策略锁池
private static volatile Map<String, TwoQuesLockPool> POOL_MAP = new HashMap<>();
// 初始化默认锁池
static {
POOL_MAP.put(DEFAULT_POOL_NAME, new TwoQuesLockPool(FIFO_MAX_SIZE, LRU_MAX_SIZE));
}
/**
* 获取指定锁
* @param poolName 锁池名称(控制缓存重排序颗粒度)
* @param key 锁名称(控制锁颗粒度)
* @param isFair 是否公平
* @return
*/
public static ReentrantLock getLock(String poolName, String key, boolean isFair){
Assert.notNull(key,"LockUtil.get(String key, boolean isFair) key不能为null");
return getOrAddLockPool(poolName).getLock(key, isFair);
}
/**
* 获取指定锁
* @param poolName 锁池名称(控制缓存重排序颗粒度)
* @param key 锁名称(控制颗粒度)
* @return
*/
public static ReentrantLock getLock(String poolName, String key){
return getLock(poolName, key, false);
}
/**
* 获取指定锁
* @param key 锁名称(控制颗粒度)
* @return
*/
public static ReentrantLock getLock(String key){
return getLock(DEFAULT_POOL_NAME, key, false);
}
/**
* 获得指定名称的锁池,如果没有就创建
* @param pooName
* @return
*/
private static TwoQuesLockPool getOrAddLockPool(String pooName){
TwoQuesLockPool lockPool = POOL_MAP.get(pooName);
// 双检锁的方式检查锁池是否存在
if(lockPool == null){
synchronized (pooName.intern()){
lockPool = POOL_MAP.get(pooName);
if(lockPool == null){
lockPool = new TwoQuesLockPool(FIFO_MAX_SIZE, LRU_MAX_SIZE);
POOL_MAP.put(pooName, lockPool);
}
}
}
return lockPool;
}
/**
* 支持LRU算法的锁池类
* @param <K>
* @param <V>
*/
@Getter
private static class LRULockPool<K,V> extends LinkedHashMap<K,V> {
// 触发回收的缓存池大小,实际可能大于
private int maxSize;
public LRULockPool(int maxSize) {
super(maxSize, 0.75F, true);
if(maxSize < 1){
throw new RuntimeException("maxSize不能小于1");
}
this.maxSize = maxSize;
}
@Override
protected boolean removeEldestEntry(Map.Entry<K,V> eldest) {
boolean removeEldestEntry = false;
if(size() > maxSize){
ReentrantLock lock = (ReentrantLock)eldest.getValue();
// 当LRU锁池容量超限,且队头的锁未被持有时,返回true,走模板方法逻辑删除头部节点
if (!lock.isLocked()){
removeEldestEntry = true;
}else {
// 当头部锁被持有时,手动删除最接近头部且未被持有的锁
Iterator<Map.Entry<K, V>> iterator = this.entrySet().iterator();
if(iterator.hasNext()){
// 指向头部锁后的节点
iterator.next();
while (iterator.hasNext()){
Map.Entry<K, V> eldestEntry = iterator.next();
ReentrantLock eldestLock = (ReentrantLock) eldestEntry.getValue();
if(!eldestLock.isLocked()){
super.remove(eldestEntry.getKey());
break;
}
}
}
}
}
return removeEldestEntry;
}
}
/**
* 支持FIFO策略的锁池类
* @param <K>
* @param <V>
*/
@Getter
private static class FIFOLockPool<K,V> extends LinkedHashMap<K,V> {
// 触发回收的缓存池大小,实际可能大于
private int maxSize;
public FIFOLockPool(int maxSize) {
super(maxSize, 0.75F, true);
if(maxSize < 1){
throw new RuntimeException("maxSize不能小于1");
}
this.maxSize = maxSize;
}
@Override
public V put(K key, V value) {
V put = super.put(key, value);
if(size() > maxSize){
// 当锁池达到最大容量,清除最靠近头部(最近最少使用)且未被持有的锁
Iterator<Map.Entry<K, V>> iterator = this.entrySet().iterator();
while (iterator.hasNext()){
Map.Entry<K, V> eldestEntry = iterator.next();
ReentrantLock eldestLock = (ReentrantLock) eldestEntry.getValue();
if(!eldestLock.isLocked()){
super.remove(eldestEntry.getKey());
break;
}
}
}
return put;
}
}
/**
* 2Q策略锁池类
*/
public static class TwoQuesLockPool{
//LRU锁池大小
@Getter
private int lruMaxSize = 64;
//FIFO锁池大小
@Getter
private int fifoMaxSize = 64;
//LRU锁池
private LRULockPool<String,ReentrantLock> lruLockPool;
//FIFO锁池
private FIFOLockPool<String,ReentrantLock> fifoLockPool;
//同步锁
private final ReentrantLock mutex = new ReentrantLock();
public TwoQuesLockPool(int fifoMaxSize, int lruMaxSize){
this.fifoMaxSize = fifoMaxSize;
this.lruMaxSize = lruMaxSize;
this.lruLockPool = new LRULockPool<>(this.lruMaxSize);
this.fifoLockPool = new FIFOLockPool<>(this.fifoMaxSize);
}
public TwoQuesLockPool(){
this.lruLockPool = new LRULockPool<>(this.lruMaxSize);
this.fifoLockPool = new FIFOLockPool<>(this.fifoMaxSize);
}
/**
* 获取指定锁,默认优先获取LRU缓存中的锁
* @param key 锁名称(控制颗粒度)
* @param isFair 是否公平
* @return
*/
public ReentrantLock getLock(String key, boolean isFair){
ReentrantLock lock = null;
// LRU锁池get会影响排序,需加锁
mutex.lock();
try {
lock = lruLockPool.get(key);
if (lock == null){
lock = fifoLockPool.get(key);
if(lock != null){
lruLockPool.put(key, fifoLockPool.remove(key));
}else {
lock = new ReentrantLock(isFair);
fifoLockPool.put(key, lock);
}
}
}catch (Exception e){
log.error(e.getMessage(), e);
throw e;
}finally {
mutex.unlock();
}
return lock;
}
}
}