之前我们讲过可重入锁五、详解ReentrantLock-CSDN博客 从这篇博文中我们可以了解到,基于lock的锁底层都是利用aqs这个抽象类的。
那么在读写锁中,其本质也是利用aqs,与可重入锁之间的区别的就是在实现抽象方法的时候,具体的逻辑不一样。可以理解为两者骨架一样,但是具体细节逻辑有区别,尤其是在tryAcquire和tryRelease的逻辑不一样。
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class ReentrantReadWriteLockDemo {
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private String message;
public void writeMessage(String message) {
lock.writeLock().lock(); //获取写锁,然后加锁
try {
System.out.println("Writing message: " + message);
this.message = message;
} finally {
lock.writeLock().unlock();
}
}
public void readMessage() {
lock.readLock().lock(); //获取读锁,然后加锁
try {
System.out.println("Reading message: " + message);
} finally {
lock.readLock().unlock();
}
}
}
首先从ReentrantReadWriteLock的构造函数入手,看看在构造函数中到底做了什么
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();//公平和非公平
readerLock = new ReadLock(this); //创建读锁
writerLock = new WriteLock(this); //创建写锁
}
可以看到第一步创建了sync,其实就是一个aqs。然后判断公平和非公平,这个我们在可重入锁的博文中讲过。
然后创建了读、写Lock对象。注意,这里这两个对象将this当做入参,我们细看一下这个两个对象到底是什么:
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
可以看到ReadLock对象和WriteLock对象其实本质上就是对Sync对象的封装。但是这里需要注意的是,ReadLock和WriteLock对象引用的是同一个sync对象。因此这里得到一个结论,ReentrantReadWriteLock,ReadLock,WriteLock 都是针对同一个sync对象进行操作。
下面我们看一下如何加读锁,核心函数为 ReadLock.lock():
public void lock() {
sync.acquireShared(1); //获取共享。入参为1,表示只获取一个令牌
}
//这个就是aqs的模板方法,主要是约定流程
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0) //开始尝试进行处理,具体逻辑在子类中
doAcquireShared(arg); //如果加锁失败,那么就进入等待,内部和可重入一样
}
其核心在于tryAcquireShared是如何处理的,怎么才算加上了锁,怎么才算未加上锁:
@ReservedStackAccess
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null ||
rh.tid != LockSupport.getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
尝试分析一下:
1、先获取当前锁状态,也就是获取state的值
2、通过exclusiveCount方法来获取是否有写锁。其原理就是state的高16位为读锁,低16位表示写锁。读锁被一个或多个线程持有时,state的高16位会增加;当读锁被释放时,高16位会减少。写锁被一个线程持有时,state的低16位会增加;当写锁被释放时,低16位会减少。
3、如果说写锁不是0,那么就以为这有线程加了写锁,然后判断加写锁的线程是否为自己,如果不是自己,那么加锁失败。这里注意,既然已经判断了写锁不是0,直接返回加读锁失败不就好了吗,为什么还需要判断加写锁的那个线程是否为自己呢?? 这里涉及到锁升级:
如果一个线程加了写锁,那么这个线程还可以加读锁。但是如果一个线程加了读锁,那么他不能加写锁。这个很好理解,因此写锁是共享的,一个线程加了读锁,那么同一个时刻还有其他线程也加了读锁,所以这个线程不能加写锁。但是反过来,如果一个线程加了写锁,那么一定就保证了当前只有一个线程获取到了锁,那么他就可以加读锁。
4、如果说没有写锁,或者说写锁是自己加的,那么利用cas进行加读锁,将高16位进行加1。
加写锁的核心代码为Write.lock()方法。这方法底层调用的标准流程和ReentrantLock一样,都是调用aqs的模板方法acquire()。具体的区别就是如何实现tryAcquire()方法。
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
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;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
尝试分析一下:
1、同样先获取到锁状态。然后计算出写锁的状态。
2、如果说当前有线程加锁:
如果w==0,意味当前有线程加了读锁,那么直接失败。
如果w!=0,意味着当前有线程加了写锁,但是【锁持有】线程不是当前线程,那么也失败
如果有线程加了写锁,且是自己加的,这里还有一个最大限制,然后直接设置锁状态,无需cas。返回加锁成功
3、如果没有线程加锁:
判断当前线程是否应该等待(这个与公平锁和非公平锁有关),如果等待,直接加锁失败,如果不等待尝试cas,如果cas失败,那么就直接加锁失败,如果成功,那么就将【锁持有】线程设置为自己。这个有点类似于ReentrantLock。