- 读写锁:通过分离读锁和写锁,使它能提供比排它锁更好的并发性和吞吐量;
- jdk5之后提供的读写锁,相比于等待通知机制的实现方式,编程方式更简单;
目录
ReentrantReadWriteLock
特性 | 说明 |
公平性选择 | 支持非公平和公平的锁获取方式,吞吐量非公平优于公平 |
重进入 | 支持重进入:读线程获取读锁之后,能再次获取读锁;写线程获取写锁之后,能再次获取写锁,也可以获取读锁 |
锁降级 | 获取写锁-->获取读锁-->释放写锁,写锁能降级成为读锁 |
通过实现ReadWriteLock接口,并实现了自身的监控状态的一些方法:类图如下
展示内部工作状态的方法:
方法 | 描述 |
getReadLockCount() | 返回读锁被获取的次数 |
getReadHoldCount() | 返回当前线程获取读锁的次数 |
isWriteLocked() | 判断写锁是否被获取 |
getWriteHoldCount() | 返回当前线程获取写锁的次数 |
实现分析
- 读写状态的设计
- 写锁的获取与释放
- 读锁的获取与释放
- 锁降级
读写状态的设计
通过一个整型变量维护多种状态,该变量被切分成2个部分,高16位表示读,低16位表示写。
0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
|----------------------------------高16位,读状态-----------------------|------------------------------低16位,写状态----------------------------|
如何确定写和读的状态?通过位运算,假设当前状态为s,写状态:s&0x0000FFFF,读状态:s>>>16,当写状态增加1,s+1,当读状态增加1,s+(1<<16),即:s+0x00010000.
写锁的获取与释放
写锁是一个支持重进入的排他锁。如果当前线程在获取写锁时,已经有读锁获取或该线程不是已经获取写锁的线程,则当前线程进入等待状态。
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
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.读锁是否存在;
读锁存在,则写锁不能被获取:读写锁要确保写锁的操作对读锁可见,因此只有等待其它读线程释放了读锁,写锁才能被当前线程获取;写锁被获取后,其它读写线程的后续访问被阻塞。
读锁的获取与释放
读锁是一个支持重进入的共享锁。能够被多个线程同时获取,如果当前线程获取读锁,则增加读状态。
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
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);
}
在获取读锁过程中,如果其他线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态;如果当前线程获取了写锁,则当前线程增加读状态,成功获取读锁。
锁降级
锁降级指写锁降级成为读锁。指把持住写锁,再获取到读锁,随后释放写锁的过程。(中间不能有释放的过程)
中间读锁的获取为什么是必要的?主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设有另外一个线程获取写锁,并修改了数据,则当前线程无法感知到其它线程对数据的更新;若当前线程获取读锁,遵循锁降级的步骤,其它线程将被阻塞,直到当前线程使用数据并释放读锁之后,其它线程才能获取写锁进行数据更新。