Alluxio设计LockPool的主要目的是:
作为保存锁的一个资源池、不让任何正在使用的锁entries被evict掉、超过某个配置的高水位时后台线程evict那些没有使用的资源。
这个池子设计的很好,我们从这个LockPool的设计里也可以学习到如何自己设计一个资源池。
关键词:
存储Lock的池子(底层是个Map)、负载因子、evict线程、低水位、高水位。
定义LockPool里存储的实体资源类:Resource。
看到Resource类里有两个关键成员变量:一个是可重入的读写锁、另外一个是原子整数类型的引用次数。
其中读写锁是资源的实体,引用次数代表了当前实体被多少个线程使用。只有引用次数为0时资源才能被清理线程清理掉。
/**
* Resource containing the lock and other information to be stored in the pool.
*/
private static final class Resource {
private final ReentrantReadWriteLock mLock;
private volatile boolean mIsAccessed;
private AtomicInteger mRefCount;
private Resource(ReentrantReadWriteLock lock) {
mLock = lock;
mIsAccessed = false;
mRefCount = new AtomicInteger(1);
}
}
LockPool存储Resource实体的数据结构定义
public class LockPool<K> implements Closeable {
// 存储Resource的Map,根据key获取对应的锁资源,会被初始化成ConcurrentHashMap类型,线程安全
private final Map<K, Resource> mPool;
// 通过key对应的Resource在mPool里不存在,使用这个mDefaultLoader去生成Resource对象放进mPool映射里.
private final Function<? super K, ? extends ReentrantReadWriteLock> mDefaultLoader;
// 低水位
private final int mLowWatermark;
// 高水位
private final int mHighWatermark;
// 下面的成员变量都跟evictor相关,清理无用Resource
private final Lock mEvictLock = new ReentrantLock();
private final Condition mOverHighWatermark = mEvictLock.newCondition();
private final ExecutorService mEvictor;
private final Future<?> mEvictorTask;
}
上面是成员变量。看下依据key(一般是inode id)获取Resource的源码,见LockPool#getResource方法:
// 参数是泛型,一般外层传的是Long类型的inode id,可以理解成文件id
private Resource getResource(K key) {
// key不允许为null
Preconditions.checkNotNull(key, "key can not be null");
// 使用Map接口的compute方法对(k,v)进行重映射。
// 1、如果key存在于mPool中,则置资源对象的mIsAccessed为true,并把mRefCount自增1后返回。
// 2、如果mPool中不存在key,则new Resource返回。 关于mDefaultLoader的逻辑我们后面看
Resource resource = mPool.compute(key, (k, v) -> {
if (v != null && v.mRefCount.incrementAndGet() > 0) {
// If the entry is to be removed, ref count will be INT_MIN, so incrementAndGet will < 0.
v.mIsAccessed = true;
return v;
}
return new Resource(mDefaultLoader.apply(k));
});
// 如果当前池子里的资源数超过了高水位
if (mPool.size() > mHighWatermark) {
if (mEvictLock.tryLock()) {
try {
// Condition对象发出信号,唤醒await的evictor线程,进行清理工作。
mOverHighWatermark.signal();
} finally {
mEvictLock.unlock();
}
}
}
// 返回当前资源。
return resource;
}
看下mDefaultLoader的逻辑,也就是说怎么把一个(k,v)添加到mPool里的:
mDefaultLoader是个Function, 第一个泛型是输入类型,第二个泛型是期望的输出类型。
private final Function<? super K, ? extends ReentrantReadWriteLock> mDefaultLoader;
在InodeLockManager类里,初始化LockPool时,会传入mDefaultLoader参数。如下所示:
private final LockPool<Long> mInodeLocks =
new LockPool<>((key) -> new ReentrantReadWriteLock(),
Configuration.getInt(PropertyKey.MASTER_LOCK_POOL_INITSIZE),
Configuration.getInt(PropertyKey.MASTER_LOCK_POOL_LOW_WATERMARK),
Configuration.getInt(PropertyKey.MASTER_LOCK_POOL_HIGH_WATERMARK),
Configuration.getInt(PropertyKey.MASTER_LOCK_POOL_CONCURRENCY_LEVEL));
因此mDefaultLoader是:
(key) -> new ReentrantReadWriteLock()
结合Map的compute函数,当mPool里不存在key时,就用key初始化一个ReentrantReadWriteLock,然后put(key, new ReentrantReadWriteLock())。
常用方法:
get
tryGet
getRawReadWriteLock
LockPool的使用
MetadataSyncLockManager、InodeLockManager。
这里我们先只关注InodeLockManager。
InodeLockManager主要职责就是负责管理inode locking相关事项。
其内部有两个LockPool,一个是用于inode lock的LockPool<Long> mInodeLocks
、
另一个是用于edge locks的LockPool<Edge> mEdgeLocks
。
看下加锁逻辑:InodeLockManager#lockInode。
返回的是个RWLockResource类对象。
// LockMode有读、写两种模式
public RWLockResource lockInode(InodeView inode, LockMode mode, boolean useTryLock) {
// 从LockPool里根据inode id去get锁资源。
return mInodeLocks.get(inode.getId(), mode, useTryLock);
}