近日在拜读drools的源码的时候,在org.drools.coro.common包底下遇到一个非常有意思的类 UpgradableReentrantReadWriteLock。字面意思就是可升级的读写锁,基于ReentrantReadWriteLock的基础上给出了自己的读写锁升级机制,实现的方式并不复杂,但个人的确是非常有意思的。
private final booleanshouldTryAtomicUpgrade;
private final ReentrantReadWriteLock lock=newReentrantReadWriteLock();
private final ThreadLocal lockCounters = newThreadLocal() {
protected int[] initialValue() {
return new int[] {0,0};
}
};
private AtomicBoolean tryingLockUpgrade=newAtomicBoolean(false);
private final IntegerlowPriotityMonitor=42;
private final IntegerhighPriorityMonitor=43;
lockCounters = newThreadLocal() {
protected int[] initialValue() {
return new int[] {0,0};
}
};
private AtomicBoolean tryingLockUpgrade=newAtomicBoolean(false);
private final IntegerlowPriotityMonitor=42;
private final IntegerhighPriorityMonitor=43;
类的成员非常简单
1.变量boolean shouldTryAtomicUpgrade在类的无参构造方法中默认为false
public UpgradableReentrantReadWriteLock() {
this(false);
}
public UpgradableReentrantReadWriteLock(booleanshouldTryAtomicUpgrade) {
this.shouldTryAtomicUpgrade= shouldTryAtomicUpgrade;
}
如果采用了默认的无参构造方法,则这个类可能只是一个普通的基于ReentrantReadWriteLock类的读写锁。只有在构造方法中设置为true,这个类的使命才能完整实现。
2.ReentrantReadWriteLocklock则是整个类加锁解锁的核心,这个类最终的加锁解锁都是依赖于这个类的。关于这个类,这里暂且不详细解释。
3. ThreadLocal<int [] >lockCounters变量通过threadlocal根据不同的thread存放当前持有锁的数量,直接初始化为大小为2的int数组,根据注释
// lockCounter[0] == readCounter;
// lockCounter[1] == upgradedReadCounter;
标明了当前线程持有锁的数量。下标0代表当前线程的持有的读锁数量,下标1代表被升级的读锁数量。在升级时,会将当前的所有读锁解锁,转化为写锁。一般情况下为0,代表这个线程并没有持有由读锁升级而来的写锁。如果不为0,则说明已经持有由读锁升级而来的写锁,只需给下标1加1,在接下来升级的读锁解除时,相应的根据这里的数据重新加上相应数量的读锁。
4. AtomicBoolean tryingLockUpgrade则是作为升级锁的信号量,默认false,如果有线程在加写锁的时候持有读锁,则会直接将当前读锁升级为写锁,在升级完毕之前,将这个变量置为true。结束之后重新改为false;
5. private finalIntegerlowPriotityMonitor=42;
private final Integer highPriorityMonitor=43;
这两个变量是升级锁的核心信号量,作为临界资源来调度线程之前的锁动作。根据注释,
// all threads except the (only) one who is trying an atomic lock upgrade must have low priority
所有在试图升级锁的线程都会通过lowPriotityMonitor来阻塞别的试图加锁的线程。
highPriorityMonitor则会保证在升级锁时,别的线程已经不占有任何锁。
由于是在ReentrantReadWriteLock的基础上实现的,所以这个类的成员并不冗杂,则接下来是具体方法的分析。
public void readLock() {
int[] lockCounter =lockCounters.get();
if(increaseUpgradedReadCounter(lockCounter))return;
if(shouldTryAtomicUpgrade&&tryingLockUpgrade.get() && lockCounter[0] ==0) {
try{
synchronized(lowPriotityMonitor) {
lowPriotityMonitor.wait();
}
} catch(InterruptedException e) {
throw newRuntimeException(e);
}
}
lock.readLock().lock();
increaseReadCounter(lockCounter);
}
该方法根据字面意思很容易就可以明白意思,而在drools中的管理规则的KnowledgeBase在默认情况下读取规则前都会调用该方法来加读锁。
首先通过threadlocal得到当前线程的lockCounter数组来知悉当前线程持有锁的情况。
if (increaseUpgradedReadCounter(lockCounter)) return;
接下来调用increaseUpgradedReadCounter方法,具体的代码如下,
private boolean increaseUpgradedReadCounter(int[] lockCounter) {
if(lockCounter[1] ==0) {
return false;
}
lockCounter[1]++;
return true;
}
如果该线程是持有过升级过写锁的线程,则会直接自加保证解除写锁时重新加读锁数量的准确性后返回。
if (shouldTryAtomicUpgrade && tryingLockUpgrade.get() && lockCounter[0] ==0) {
try{
synchronized(lowPriotityMonitor) {
lowPriotityMonitor.wait();
}
} catch(InterruptedException e) {
throw newRuntimeException(e);
}
}
如果在构造方法中直接将shouldTryAtomicUpgrade直接设置了false,那么上面这段没有了直接执行的必要。
但如果设置了,根据成员注释中解释,则一旦有别的线程处于锁的升级过程中,由于lowPriotityMoniter而阻塞,导致等待别的线程锁升级完毕,才会在接下里的步骤中继续加读锁。
在方法的加读锁完毕的最后会直接给lockCounter[0]加一,代表持有读锁数量加1;
public voidreadUnlock() {
int[] lockCounter =lockCounters.get();
if(decreaseUpgradedReadCounter(lockCounter))return;
lock.readLock().unlock();
decreaseReadCounters(lockCounter);
notifyUpgradingThread();
}
解除读锁的具体步骤简洁明了,对应加读锁的步骤可以说是一一对应。但是最后一行的notifyUpgradingThread()却似乎显得很突兀。这个方法具体代码如下,
private void notifyUpgradingThread() {
if (shouldTryAtomicUpgrade && tryingLockUpgrade.get()) {
synchronized (highPriorityMonitor) {
if (lock.getReadLockCount() < 2 && !lock.isWriteLocked()) {
highPriorityMonitor.notifyAll();
}
}
}
}
这个在接下来的锁升级一并提起,更好理解。
整个读锁的加和解除清晰明了,但是加写锁的流程非常有意思。
public void writeLock() {
if(lock.isWriteLockedByCurrentThread()) {
lock.writeLock().lock();
return;
}
int[] lockCounter =lockCounters.get();
if (lockCounter[0] >0) {
lockCounter[1] = lockCounter[0];
if(shouldTryAtomicUpgrade&&tryingLockUpgrade.compareAndSet(false,true)) {
atomicLockUpgrade(lockCounter[0]);
} else{
for(inti = lockCounter[0]; i >0; i--)lock.readLock().unlock();
notifyUpgradingThread();
lowPriorityWriteLock();
}
} else{
lowPriorityWriteLock();
}
}
在整个加读锁之前,首先调用ReeantrantReadWriteLock的isWriteLockedByCurrentThread()直接判断当前线程是否已经取得了写锁,如果有,则直接加锁,否则继续接下来的代码。
接下来取得threadlocal中当前线程持有lockcounter数组来去的当前线程的持有锁情况。如果没有任何读锁,则直接调用lowPriorityWriteLock()方法来加读锁。
private voidlowPriorityWriteLock() {
if(shouldTryAtomicUpgrade&&tryingLockUpgrade.get()) {
synchronized(lowPriotityMonitor) {
try{
lowPriotityMonitor.wait();
} catch(InterruptedException e) {
throw newRuntimeException(e);
}
}
}
lock.writeLock().lock();
}
这个加写锁的过程简单明了,在shouldTryAtomicUpgrade为true的情况下,只要等待其他企图升级锁的线程升级完毕即可,如果为false,那就是单纯的加锁。
但是在另一个情况,就是当前线程如果已经持有写锁,则会将lockCounter[1]的值设为lockCounter[0]的值,也就是说将当前普通读锁数量变为升级锁的数量以便解除时返回升级前持有的读锁数量。如果在接下来在设置了shouldTryAtomicUpgrade为true的情况下,则会进入将当前的读锁升级为写锁的过程,在进入之前通过cas设置tryingLockUpgrade为true,用以提示别的线程当前有线程正在执行锁升级。
private void atomicLockUpgrade(intreadHoldCount) {
for(inti = readHoldCount; i >1; i--)lock.readLock().unlock();
synchronized(highPriorityMonitor) {
if(lock.getReadLockCount() > readHoldCount ||lock.isWriteLocked()) {
try{
highPriorityMonitor.wait();
} catch(InterruptedException e) {
throw newRuntimeException(e);
}
}
}
lock.readLock().unlock();
lock.writeLock().lock();
tryingLockUpgrade.set(false);
synchronized(lowPriotityMonitor) {
lowPriotityMonitor.notifyAll();
}
}
在升级锁的第一步,将当前所有的读锁全部解锁,直到剩下一个读锁。接下来通过highPriorityMonitor来阻塞,用以等待别的读锁写锁来释放完毕,才会进入,否则会一直等待。
这个时候就可以回过去看之前的读锁解除的时候最后调用的notifyUpgradingThread
方法。
private voidnotifyUpgradingThread() {
if(shouldTryAtomicUpgrade&&tryingLockUpgrade.get()) {
synchronized(highPriorityMonitor) {
if(lock.getReadLockCount() <2&& !lock.isWriteLocked()) {
highPriorityMonitor.notifyAll();
}
}
}
}
在解除锁的最后,如果当前当前有线程正在升级锁由于有别的线程正在持有锁而阻塞,就会尝试判断正在尝试的线程的读锁是否是唯一的读锁,并且并没有别的线程持有写锁,则唤醒正在等待的升级锁的线程,如果这一步并没有成功唤醒,在最后一个解除读锁的线程接触后,升级锁的线程在没有别的写锁的情况下将会被唤醒。
在被唤醒后,解除当前线程持有的最后一个读锁,而加上写锁,完成升级过程。显然这一步的原子化保证是升级的关键,可以见到highPriorityMonitor这个信号量保证了在升级之前,要升级的锁会是唯一当前保有的读锁,以及没有别的写锁存在。而其他试图加锁的线程由于tryingLockUpgrade的原因而被lowPriotityMoniter而阻塞,直到在升级完毕之后统一唤醒。
这样,升级锁的在两个信号量的协调下,原子性被保证了;
public voidwriteUnlock() {
if(lock.getWriteHoldCount() ==1) {
int[] lockCounter =lockCounters.get();
if(lockCounter[1] >0) {
for(inti = lockCounter[1]; i >0; i--) {
lock.readLock().lock();
}
lockCounter[1] =0;
}
}
lock.writeLock().unlock();
notifyUpgradingThread();
}
解除写锁的过程相应的也很简单,如果当前解除的写锁是最后一个被解除的,将根据lockCounter中读锁在升级时被解除的数量,重新依次加上读锁。并给代表升级锁数量的位置置零,在接触写锁,最后尝试唤醒别的正在企图升级锁而因为当前读锁而阻塞的线程。