读写锁(ReentrantReadWriteLock)就是读线程和读线程之间不互斥。
读读不互斥,读写互斥,写写互斥
1.类继承层次
public interface ReadWriteLock {
Lock readLock();
Lock writeLock();
}
image.png
使用方法:
ReadWriteLock readWriteLock = new ReentrantReadWriteLock();
Lock readLock = readWriteLock.readLock();
readLock.lock();
// 进行读取操作
readLock.unlock();
Lock writeLock = readWriteLock.writeLock();
writeLock.lock();
// 进行写操作
writeLock.unlock();
也就是说,当使用 ReadWriteLock 的时候,并不是直接使用,而是获得其内部的读锁和写锁,然后分别调用lock/unlock
2.基本原理
从下面的构造方法可以看出,readerLock和writerLock实际共用同一个sync对象。sync对象同互斥锁一样,分为非公平和公平两种策略,并继承自AQS。
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
this.readerLock = new ReentrantReadWriteLock.ReadLock(this);
this.writerLock = new ReentrantReadWriteLock.WriteLock(this);
}
state:同互斥锁一样,读写锁也是用state变量来表示锁状态的。只是state变量在这里的含义和互斥锁完全不同。在内部类Sync中,对state变量进行了重新定义.也就是把 state 变量拆成两半,低16位,用来记录写锁。但同一时间既然只能有一个线程写,为什么还需要16位呢?这是因为一个写线程可能多次重入。例如,低16位的值等于5,表示一个写线程重入了5次。高16位,用来“读”锁。例如,高16位的值等于5,既可以表示5个读线程都拿到了该锁;也可以表示一个读线程重入了5次。
为什么要把一个int类型变量拆成两半,而不是用两个int型变量分别表示读锁和写锁的状态呢?
- 这是因为无法用一次CAS 同时操作两个int变量,所以用了一个int型的高16位和低16位分别表示读锁和写锁的状态
- 当state=0时,说明既没有线程持有读锁,也没有线程持有写锁;当state != 0时,要么有线程持有读锁,要么有线程持有写锁,两者不能同时成立,因为读和写互斥。这时再进一步通过sharedCount(state)和exclusiveCount(state)判断到底是读线程还是写线程持有了该锁。
3.ReadLock和WriteLock
在ReentrantReadWriteLock的两个内部类ReadLock和WriteLock中,是如何使用state变量的。
public static class ReadLock implements Lock, java.io.Serializable {
// ...
public void lock() {
sync.acquireShared(1);
}
public void unlock() {
sync.releaseShared(1);
}
// ...
}
public static class WriteLock implements Lock, java.io.Serializable {
// ...
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
// ...
}
acquire/release、acquireShared/releaseShared 是AQS里面的两对模板方法。互斥锁和读写锁的写锁都是基于acquire/release模板方法来实现的。读写锁的读锁是基于acquireShared/releaseShared这对模板方法来实现的。
- 读锁的公平实现:Sync.tryAccquireShared()+FairSync中的两个重写的子方法。
- 读锁的非公平实现:Sync.tryAccquireShared()+NonfairSync中的两个重写的子方法。
- 写锁的公平实现:Sync.tryAccquire()+FairSync中的两个重写的子方法。
- 写锁的非公平实现:Sync.tryAccquire()+NonfairSync中的两个重写的子方法。
static final class NonfairSync extends Sync {
// 写线程抢锁的时候是否应该阻塞
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();
} }
对于公平,比较容易理解,不论是读锁,还是写锁,只要队列中有其他线程在排队(排队等读锁,或者排队等写锁),就不能直接去抢锁,要排在队列尾部。
对于非公平,读锁和写锁的实现策略略有差异。
写线程能抢锁,前提是state=0,只有在没有其他线程持有读锁或写锁的情况下,它才有机会去抢锁。或者state != 0,但那个持有写锁的线程是它自己,再次重入。写线程是非公平的,即writerShouldBlock()方法一直返回false。
对于读线程,假设当前线程被读线程持有,然后其他读线程还非公平地一直去抢,可能导致写线程永远拿不到锁,所以对于读线程的非公平,要做一些“约束”。当发现队列的第1个元素是写线程的时候,读线程也要阻塞,不能直接去抢。即偏向写线程。
4.WriteLock
写锁是排它锁,实现类似互斥锁。
tryLock()实现分析(非阻塞)
lock()方法(阻塞)
tryLock和lock方法不区分公平/非公平。ReentrantReadWriteLock的FairSync 和 NonfairSync 区别是 writerShouldBlock()、readerShouldBlock()两个方法,是在抢锁的时候用到的。FairSync 中的writerShouldBlock()方法,其中是写线程是有优先级的
unlock()实现分析
5.ReadLock
trylock
public boolean tryLock() {
return this.sync.tryReadLock();
}
@ReservedStackAccess
final boolean tryReadLock() {
// 获取当前线程
Thread current = Thread.currentThread();
int c;
int r;
do {
// 获取state值
c = this.getState();
// 如果是写线程占用锁或者当前线程不是排他线程,则抢锁失败
if (exclusiveCount(c) != 0 && this.getExclusiveOwnerThread() != current) {
return false;
}
// 获取读锁state值
r = sharedCount(c);
// 如果获取锁的值达到极限,则抛异常
if (r == 65535) {
throw new Error("Maximum lock count exceeded");
}
// 使用CAS设置读线程锁state值
} while(!this.compareAndSetState(c, c + 65536));
// 如果r=0,则当前线程就是第一个读线程
if (r == 0) {
this.firstReader = current;
// 读线程个数为1
this.firstReaderHoldCount = 1;
// 如果写线程是当前线程
} else if (this.firstReader == current) {
// 如果第一个读线程就是当前线程,表示读线程重入读锁
++this.firstReaderHoldCount;
} else {
// 如果firstReader不是当前线程,则从ThreadLocal中获取当前线程的读锁 个数,并设置当前线程持有的读锁个数
ReentrantReadWriteLock.Sync.HoldCounter rh = this.cachedHoldCounter;
if (rh != null && rh.tid == LockSupport.getThreadId(current)) {
if (rh.count == 0) {
this.readHolds.set(rh);
}
} else {
this.cachedHoldCounter = rh = (ReentrantReadWriteLock.Sync.HoldCounter)this.readHolds.get();
}
++rh.count;
}
return true;
}
lock
public void lock() {
this.sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (this.tryAcquireShared(arg) < 0) {
this.doAcquireShared(arg);
}
}
@ReservedStackAccess
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = this.getState();
if (exclusiveCount(c) != 0 && this.getExclusiveOwnerThread() != current) {
return -1;
} else {
int r = sharedCount(c);
if (!this.readerShouldBlock() && r < 65535 && this.compareAndSetState(c, c + 65536)) {
if (r == 0) {
this.firstReader = current;
this.firstReaderHoldCount = 1;
} else if (this.firstReader == current) {
++this.firstReaderHoldCount;
} else {
ReentrantReadWriteLock.Sync.HoldCounter rh = this.cachedHoldCounter;
if (rh != null && rh.tid == LockSupport.getThreadId(current)) {
if (rh.count == 0) {
this.readHolds.set(rh);
}
} else {
this.cachedHoldCounter = rh = (ReentrantReadWriteLock.Sync.HoldCounter)this.readHolds.get();
}
++rh.count;
}
return 1;
} else {
return this.fullTryAcquireShared(current);
}
}
}
readerShouldBlock()在公平和非公平中实现。
unlock()实现分析
tryReleaseShared()的实现:
Thread current = Thread.currentThread();
....
@ReservedStackAccess
protected final boolean tryReleaseShared(int unused) {
int c;
do {
c = this.getState();
nextc = c - 65536;
} while(!this.compareAndSetState(c, nextc));
return nextc == 0;
}
因为读锁是共享锁,多个线程会同时持有读锁,所以对读锁的释放不能直接减1,而是需要通过一个for循环+CAS操作不断重试。这是读锁的tryReleaseShared和写锁tryRelease的根本差异所在。
网友评论