ReentrantReadWriteLock
ReentrantReadWriteLock具有ReentrantLock的特性,支持重入和公平性设置,但是对读写进行了分离。
读操作采用共享锁,写操作采用独占锁,即一个资源可以有多个线程同时进行读操作,但是只能有一个线程进行写操作。
在读多写少的情况下ReentrantReadWriteLock可以极大的提高吞吐量。
ReentrantReadWriteLock:
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
/** 读锁 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 写锁 */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** 同步器实例 */
final Sync sync;
/** 父类同步器 */
abstract static class Sync extends AbstractQueuedSynchronizer {... ...}
/** 非公平锁同步器 */
static final class NonfairSync extends Sync {... ...}
/** 公平锁同步器 */
static final class FairSync extends Sync {
/** 构造 */
public ReentrantReadWriteLock() {
this(false);
}
/** 构造 */
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
/** 工厂方法获取 写锁 */
public ReentrantReadWriteLock.WriteLock writeLock() {
return writerLock;
}
/** 工厂方法获取 读锁 */
public ReentrantReadWriteLock.ReadLock readLock() {
return readerLock;
}
... ...
}
ReentrantReadWriteLock内部实现了三个同步器,与ReentrantLock不同的是,NonfairSync和FairSync的加锁方法都是调用父类Sync的tryAcquire方法,子类中只实现了获取公平策略的方法writerShouldBlock和readerShouldBlock。
Sync:
abstract static class Sync extends AbstractQueuedSynchronizer {
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
/** 共享数量 读锁 高16位*/
static int sharedCount(int c) {
return c >>> SHARED_SHIFT;
}
/** 独占数量 写锁 低16位*/
static int exclusiveCount(int c) {
return c & EXCLUSIVE_MASK;
}
/** 重入计数器 */
static final class HoldCounter {
int count = 0;
final long tid = Thread.currentThread().getId();
}
/** 重入计数器ThreadLocal */
static final class ThreadLocalHoldCounter
extends ThreadLocal {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
/** 重入计数器ThreadLocal实例 */
private transient ThreadLocalHoldCounter readHolds;
/** 最近一个成功获取读锁的线程的计数。
这省却了ThreadLocal查找 缓存*/
private transient HoldCounter cachedHoldCounter;
/** 针对只有一个读锁的优化处理 线程 */
private transient Thread firstReader = null;
/** 针对只有一个读锁的优化处理 重入计数器 */
private transient int firstReaderHoldCount;
Sync() {
// 重入计数器容器
readHolds = new ThreadLocalHoldCounter();
// 保证readHolds的可见性,因为state 是volatile修饰的
setState(getState());
}
// 读公平策略 交由子类实现
abstract boolean readerShouldBlock();
// 写公平策略 交由子类实现
abstract boolean writerShouldBlock();
... ...
}
/**非公平锁同步器*/
static final class NonfairSync extends Sync {
//写公平策略 ,非公平锁中直接返回false
final boolean writerShouldBlock() {
return false;
}
//读公平策略 ,判断同步队列中队头等待的节点是否是独占节点,
//也就判断前面是否有写锁在等待被唤醒。
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
/**公平锁同步器*/
static final class FairSync extends Sync {
//写公平策略 ,判断队列中是否有等待的节点
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
//读公平策略 ,判断队列中是否有等待的节点
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
ReadLock和WriteLock使用同一个同步器,这样就需要解决state不够用的问题,因为state既要标示读锁的数量,又要标示写锁的数量,所以将state变量一分为二,高位16位表示读锁的数量,低位16位表示写锁的数量。
读锁存在多个,state的高16位记录锁的数量,重入次数存在每个持有线程的ThreadLocal中,即ThreadLocalHoldCounter。
写锁加锁流程:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState(); // s1
int w = exclusiveCount(c);// 写锁数量
if (c != 0) { // s2
// 有读锁存在或者 独占线程非当前线程
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)// 检查重入次数
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
} // s3
// writerShouldBlock 子类实现 主要实现公平策略
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);//持有线程
return true;
}
s1:c!=0转入s2,c==转入s3
s2:c!=0说明存在锁
w==0说明写锁的数量为0,那么一定存在读锁,有读锁存在不允许获取写锁因为如果一个线程在读另外一个线程写入,会出现数据不一致引起脏读,返回false。
w不等于0说明存在写锁,只有线程重入才能获取锁,否则返回false,因为写锁是独占锁,不允许两个线程同时写。
如果重入次数大于MAX_COUNT,抛出Error,直接退出程序了。
进入重入逻辑,将state设为c+1,返回true,获取锁成功。
s3:c==0说明不存在锁,首次获取写锁
调用公平性策略方法,如果此时锁为公平锁并且同步队列中有等待的节点,就直接返回false。
如果此时锁为非公平锁,获取成功,返回true。
写锁解锁逻辑与ReentrantLock一样,这里不再累述。
读锁加锁流程:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 存在写锁(独占锁) 并且请求线程非写锁的持有线程
// s1
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);// 读锁计数
// s2
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// s3
if (r == 0) {// 只有一个读锁,不动用readHolds
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {//重入
firstReaderHoldCount++;
} else {//多个读锁,启用ThreadLocal记录重入次数 s4
HoldCounter rh = cachedHoldCounter;// 访问缓存
if (rh == null || rh.tid != current.getId())
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
// 自旋重试 s5
return fullTryAcquireShared(current);
}
s1: exclusiveCount(c)!=0 说明存在写锁并且持有该锁的线程不是当前线程,直接返回-1,获取锁失败。因为如果一个线程在写,这时候另外一个线程读取很有可能读取不一致的脏数据。
s2: 调用子类的公平性策略&&r < MAX_COUNT&&CAS设置State,如果三项操作都返回true转入s3,否则转入s5。
s3: r == 0说明没有读锁,是首次获取读锁,直接将firstReader设置为当前线程,重入次数firstReaderHoldCount设为1,获取锁成,返回1。
r!=0且firstReader等于当前线程,说明是唯一的一个线程在重入,直接将firstReaderHoldCount累加,获取锁成,返回1。
r!=0且firstReader不等于当前线程,转入s4。
s4: cachedHoldCounter总是记录最后一次获取锁的线程信息,这样减少了查询ThreadLocal的次数,也提高了后续解锁的效率。
如果cachedHoldCounter为当前线程留下的并且重入次数count为0,先将cachedHoldCounter其放入ThreadLocal中。
如果cachedHoldCounter不是当前线程留下的,从线程的ThreadLocal中获取HoldCounter并将其赋给cachedHoldCounter。
累加cachedHoldCounter中的重入计数器。
获取锁成,返回1。
s5: fullTryAcquireShared逻辑与tryAcquireShared大致相同,使用for(;;)保证compareAndSetState(c, c + SHARED_UNIT)操作成功,因为可能有其他线程在争用,这里自旋等待其他线程操作完毕。
读锁解锁方法:
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) { // s1
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else { // s2
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != current.getId())
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {// 完全释放读锁
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;// 重入退出
}
// s3
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
s1: 如果firstReader(加锁时的第一个线程)不是当前线程转入s3,当前线程没有重入直接firstReader=null,重入则将重入计数器firstReaderHoldCount减1,转入s3。
s2、取出缓存计数器,如果cachedHoldCounter是当前线留下的就从ThreadLock中取出,count <= 1说明这次释放将是完全释放,因此将重入计数器从ThreadLock中删除。重入次数减1,转入s3
s3、state的高位16位操作得到nextc,for(;;)循环中保障state设置成功,如果nextc为0说明读锁全部释放。
锁升级与降级
锁升级是指持有读锁的线程,在读锁未释放的时候再申请写锁
锁降级是指持有写锁的线程,在写锁未释放的时候再申请读锁
一个小栗子:
/** 锁升级 */
public void upgrade() {
try {
readLock.lock();// 获取读锁
try {// 持有读锁
writeLock.lock();// 再获取写锁
} finally {
writeLock.unlock();// 释放写锁
}
} finally {
readLock.unlock();// 最后释放读锁
}
}
/** 锁降级 */
public void downgrading() {
try {
writeLock.lock();// 获取写锁
try {// 持有写锁
readLock.lock();// 再获取读锁
} finally {
readLock.unlock();// 释放读锁
}
} finally {
writeLock.unlock();// 最后释放写锁
}
}
通过上面源码的分析:
在"读锁加锁流程s1处"可以看到持有写锁的线程可以再去获取读锁。
在"写锁加锁流程s2处"可以看到有读锁存在是不允许去获取写锁的。
小结
- 读锁为共享锁,可以运行多个线程持有同一把读锁;写锁为独占锁,只允许一个线程持有,在读多写少的情况下ReentrantReadWriteLock可以极大的提高吞吐量
- 获取读锁的条件:a、没有写锁存在;b、有写锁存在,但是进入读锁的线程是持有写锁的线程。
- 获取写锁的条件,a、没有写锁存在(重入除外);b、没有读锁存在。
- ReentrantReadWriteLock支持锁降级,不支持锁升级。
码字不易,转载请保留原文连接https://www.jianshu.com/p/2c5c19114463