简介
前面介绍的ReentrantLock
,只要有线程加锁了其他线程就只能进行等待,但是如果多个线程都只是读取数据并没有修改数据的话,完全可以不用加锁。而ReentrantReadWriteLock
就是采用了读写分离的模式,根据操作类型分别加读锁或写锁,大大提高了读操作的效率,实现了读读并发、读写互斥的效果。
使用
public class ReadWriteLockDemo {
private static final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
private static final ReentrantReadWriteLock.ReadLock readLock = lock.readLock();
private static final ReentrantReadWriteLock.WriteLock writeLock = lock.writeLock();
private static int data;
public static void main(String[] args) {
new Thread(() -> {
readLock.lock();
try {
System.out.println("读锁操作 " + data);
} finally {
readLock.unlock();
}
}, "t1").start();
new Thread(() -> {
writeLock.lock();
try {
System.out.println("写锁操作");
data = 10;
} finally {
writeLock.unlock();
}
}, "t1").start();
}
}
ReentrantReadWriteLock
的使用方法很简单,先创建一个ReentrantReadWriteLock
对象,然后分别创建一个
读锁ReadLock
和写锁WriteLock
对象,加锁时调用lock
方法,解锁时调用unlock
方法。
读锁
加锁流程
// ReentrantReadWriteLock.ReadLock类
public void lock() {
sync.acquireShared(1);
}
// AQS类
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
ReadLock
类的lock
方法调用的是AQS
的acquireShared
方法,这个方法的大致意思是先尝试去获取共享锁,如果成功了就结束,如果失败了就把当前线程添加到等待队列中,不断进行重试和等待。
尝试加锁
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 != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1; // 第二步
}
return fullTryAcquireShared(current); // 第三步
}
分析这个方法之前先介绍下这里面一些属性和方法的含义:
firstReader
:第一个获取读锁的线程firstReaderHoldCount
:第一个获取读锁的线程持有的读锁数量(重入数量)cachedHoldCounter
:上一个持有读锁的线程持有的读锁数量readHolds
:通过ThreadLock
存储的本线程持有的读锁数量
ReentrantReadWriteLock
是通过state
属性来存储读写锁数量的,高16
位表示共享锁数量,低16
位表示独占锁数量
exclusiveCount(state)
:获取独占锁(写锁)数量sharedCount(state)
:获取共享锁(读锁)数量
再回到tryAcquireShared
方法,第一步首先获取独占锁数量,如果不是0
表示有线程加了写锁,然后判断持有锁的线程是不是当前线程,不是的话说明其他线程加了写锁,当前线程加读锁就失败了,直接返回-1
。
如果没有其他线程加写锁就进入第二步,首先调用readerShouldBlock
方法判断读锁是否要进行阻塞,源码如下:
// ReentrantReadWriteLock.NonfairSync类
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
这里的四个表达式意思分别是:
head
节点不是null
,阻塞队列初始化时会创建一个空节点作为head
节点,真正阻塞等待的节点是从第二个节点开始;- 第二个节点不是
null
; - 第二个节点不是以共享模式进行阻塞的,也就是独占模式;
- 第二个节点的线程不是
null
; 简言之,如果返回true
表示队列中第二个节点(也就是有资格获取锁的下一个节点)要加写锁。第二步条件中进行了取反操作就直接进入第三步了;如果不满足这四个表达式的任意一种情况就继续判断第二个条件。
第二个条件判断的是共享锁的数量是不是达到了最大值,达到了就执行第三步,否则继续判断第三个条件;
第三个条件尝试CAS
修改state
的值,对共享锁数量进行加1
。
如果这三个条件都成功了,也就意味着当前线程获取到了读锁,那么继续进行后续操作。
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
}
首先判断r==0
,也就是共享锁数量是否为0
,如果成立表示之前没有其他线程加读锁,当前线程就是获取到读锁的第一个线程,然后把firstReader
标记为当前线程,并把持有锁数量设为1
。
else if (firstReader == current) {
firstReaderHoldCount++;
}
如果已经有线程获取到读锁了,就判断firstReader
是不是当前线程,如果是表示发生了重入,直接执行firstReaderHoldCount++
修改持有锁数量。
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
如果第一个持有锁的线程不是当前线程,首先获取上一个获取到读锁的线程持有锁的数量,如果是null
或者上一个线程不是当前线程,就把当前线程标记为最后一个线程,readHolds.get()
获取本线程持有锁数量然后赋值给cachedHoldCounter
。
如果上一个获取读锁的线程是当前线程,并且持有锁的数量变成了0
,就更新本线程的持有锁数量。
最后通过rh.count++
把锁数量加1。
第三步及后续流程将在下一篇文章中进行介绍。