相比于ReentrantLock具有完全互斥排他的效果,也就是同一时间只有一个线程在执行ReentrantLock.lock()方法后面的任务。
虽然这样做保证了实例变量的线程安全性,但并行效率是比较地下的;所以读写锁的出现,将读、写加锁操作分开处理,可以加快运行效率;
相比于 ReentrantLock 适用于一般场合,ReadWriteLock 适用于读多写少的情况,合理使用可以进一步提高并发效率
读写锁有两个锁,一个是读操作相关的锁,称为共享锁;另一个是写操作相关的锁,也叫排他锁。多个读锁之间不互斥,读锁与写锁互斥,
写锁与写锁互斥。
这两把锁中,第 1 把锁是写锁,获得写锁之后,既可以读数据又可以修改数据,而第 2 把锁是读锁,获得读锁之后,只能查看数据,不能修改数据。读锁可以被多个线程同时持有,所以多个线程可以同时查看数据。
在读的地方合理使用读锁,在写的地方合理使用写锁,灵活控制,可以提高程序的执行效率
ReentrantReadWriteLock使用了一个16位的状态来表示写入锁的计数,并且使用了另一个16位的状态来表示读取锁的计数。
在读取锁上的操作将使用共享的获取方法与释放方法,在写入锁的操作将使用独占的获取方法与释放方法
/**
* 读写锁
*
* 读读共享、写写互斥、读写互斥、写读互斥
*/
public class ReentrantReadWriteLockExample {
private final ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
public void read() {
try {
readWriteLock.readLock().lock();
System.out.println("获取读锁:" + Thread.currentThread().getName()
+ " " + System.currentTimeMillis());
Thread.sleep(10000);
} catch (Throwable t) {
t.printStackTrace();
} finally {
readWriteLock.readLock().unlock();
}
}
@Test
public void testRR() {
ReentrantReadWriteLockExample example = new ReentrantReadWriteLockExample();
Thread t1 = new Thread(() -> {
example.read();
});
t1.setName("T1");
t1.start();
Thread t2 = new Thread(() -> {
example.read();
});
t2.setName("T2");
t2.start();
//获取读锁:T1 1596420044601
//获取读锁:T2 1596420044605
}
public void write() {
try {
readWriteLock.writeLock().lock();
System.out.println("获取写锁:" + Thread.currentThread().getName()
+ " " + System.currentTimeMillis());
Thread.sleep(10000);
} catch (Throwable t) {
t.printStackTrace();
} finally {
readWriteLock.writeLock().unlock();
}
}
@Test
public void testWW() throws InterruptedException {
ReentrantReadWriteLockExample example = new ReentrantReadWriteLockExample();
Thread t1 = new Thread(() -> {
example.write();
});
t1.setName("T1");
t1.start();
Thread t2 = new Thread(() -> {
example.write();
});
t2.setName("T2");
t2.start();
Thread.sleep(10000);
//获取写锁:T1 1596420243034
//获取写锁:T2 1596420253039
}
@Test
public void testRW() throws InterruptedException {
ReentrantReadWriteLockExample example = new ReentrantReadWriteLockExample();
Thread t1 = new Thread(() -> {
example.read();
});
t1.setName("T1");
t1.start();
Thread t2 = new Thread(() -> {
example.write();
});
t2.setName("T2");
t2.start();
Thread.sleep(10000);
//获取读锁:T1 1596420330107
//获取写锁:T2 1596420340111
}
}
//
//
//
从以上例子中可以看到
在ReentrantReadWriteLock实现中,state的含义:
高16位表示读锁的重入次数(共享模式,多个线程持有,因此 重入次数=每个线程重入次数之和)
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
因为是在高16存读锁的重入次数,因此每重入1次,就需要加一个SHARED_UNIT,即65535
将高16位向左移动16位也就得到了读锁的重入次数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
低16位表示写锁的重入次数(独占模式,每次也就一个线程持有)
因此将高16位抹去就是写锁的重入次数
static final int SHARED_SHIFT = 16;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
提供HoldCounter结构,保存每个读线程以及其重入次数
/**
* A counter for per-thread read hold counts.
* Maintained as a ThreadLocal; cached in cachedHoldCounter
*/
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
通过ThreadLocal来保证线程间变量的隔离,也就是每个线程都有自己相对应的HoldCounter对象
private transient ThreadLocalHoldCounter readHolds;
/**
* ThreadLocal subclass. Easiest to explicitly define for sake
* of deserialization mechanics.
*/
static final class ThreadLocalHoldCounter
extends ThreadLocal {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
通过cachedHoldCounter来缓存最近一个获取读锁成功的线程的HoldCounter,这样可以避免ThreadLocal查找
也就是每次先从用这个字段判断,如果不行的话在从ThreadLocal中去查询
private transient HoldCounter cachedHoldCounter;
firstReader字段记录第一个获取读锁成功的线程,firstReaderHoldCount也就是相应的HoldCount信息;
因为第一个线程比较特殊,它是共享模式下第一个将state从 “0” 变成 “1”,也就是只要线程持有读锁,firstReader就不为空,
如果 firstReader = null,就说明没有线程持有读锁
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
将上面这几个关键变量搞清楚了,理解ReentrantReadWriteLock就比较容易了
Sync提供共享模式、独占模式基础实现
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;
// 如果写锁重入次数超过65535
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
// 读锁重入
setState(c + acquires);
return true;
}
// writerShouldBlock这个就是模版方法,实现类有公平锁和非公平锁,用于判断是否需要抢占资源
// 如果是非公平锁,始终返回false,表示可以抢占锁资源
// 如果是公平锁,需要判断等待队列中是否有等待线程,如果有的话,就不能抢占资源,只能排队等待获取资源
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 到这里就肯定是能获取锁资源了,就直接设置该锁对象被自己(current thread)独占
setExclusiveOwnerThread(current);
return true;
}
//
tryAcquire是用于独占模式下的获取资源的方法,在这里也就是用于写锁,主要流程
用于独占模式下的锁资源释放,这里也就是写锁的资源释放
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 剩余重入次数
int nextc = getState() - releases;
// 计算得到写锁的剩余重入次数,如果等于0,说明写锁已经完全释放
// 相关exclusiveOwnerThread字段设置为null
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
释放资源比较简单,用总的资源量(总重入次数) - 当前需要释放的资源量 得到剩余资源量,如果 剩余资源量等于0就说明可以完全释放写锁了
在共享模式下尝试获取资源,这里也就是针对读锁
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 {
// cachedHoldCounter存的是最近一个线程的HoldCounter
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
// 如果从cachedHoldCounte缓存中拿不到,那就从ThreadLocal中取
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
// 表明获取资源成功,这里返回1表明对应读锁(共享模式下)还有资源可用,主要用于共享模式下,等待队列里的传播性,也就是唤醒后继节点获取资源
// 如果返回0表示此次能获取资源,但没有多余资源了
return 1;
}
// 自旋+CAS获取读锁
return fullTryAcquireShared(current);
}
//
这个方法是在共享模式下获取资源,在ReentrantReadWriteLock也就是获取读锁资源,有以下几个步骤
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
// 存在写锁
if (exclusiveCount(c) != 0) {
// 写锁不是当前线程的话,返回获取资源失败
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
} else if (readerShouldBlock()) { // 公平性
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
} else {
// 和之前一样,先尝试从缓存cachedHoldCounter中取HoldCounter,
// 找不到在从ThreadLocal中获取
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
// 读锁的最大重入次数是65535
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 开始CAS设置
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 如果是第一个获取读锁的线程
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) { // 第一个获取读锁的线程重入
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
// 始终记录的是最新线程的holdCounter
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
//
整个方法还是比较常规的,总的来说就是通过自旋+CAS来保证在多线程竞争竞争下获取到资源;对于记录每个线程的读锁重入次数,用到了一些属性
共享模式下的资源释放,这里指读锁资源的释放
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
// 如果已经等于0了,说明已经完全释放了这把读锁
return nextc == 0;
}
}
//
在读锁这种情况下使用了自旋来保证可以正释放资源,因为读锁是可以被多个线程持有,
因此CAS操作也是可用失败的
尝试获取写锁
final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
// 如果重入次数c!=0, 必须是重入获取写锁才行
if (c != 0) {
int w = exclusiveCount(c);
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
if (!compareAndSetState(c, c + 1))
return false;
// 设置当前线程为写锁的独占线程
setExclusiveOwnerThread(current);
return true;
}
//
尝试获取读锁
final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;;) {
int c = getState();
// 如果存在写锁,必须是同一个线程才行(锁降级)
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return false;
int r = sharedCount(c);
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (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 true;
}
}
}
//
//
通过自旋+CAS保证获取到锁资源
获取读锁的重入次数
final int getReadHoldCount() {
if (getReadLockCount() == 0)
return 0;
Thread current = Thread.currentThread();
if (firstReader == current)
return firstReaderHoldCount;
HoldCounter rh = cachedHoldCounter;
if (rh != null && rh.tid == getThreadId(current))
return rh.count;
int count = readHolds.get().count;
if (count == 0) readHolds.remove();
return count;
}
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
// 写锁模式的公平性
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
// 读锁模式的公平性
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
只要等待队列存在等待节点就不能抢占资源,只能按照FIFO的方法乖乖的排队去获取资源,也就是不允许插队
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
// 写锁模式的非公平性
final boolean writerShouldBlock() {
return false; // writers can always barge
}
// 读锁模式的非公平性
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
// 等待队列中的第一个节点是否是想要获取写锁的线程
// 如果是的话,让它先去尝试抢夺锁,避免"无限制"的饥饿状态
return apparentlyFirstQueuedIsExclusive();
}
}
/**
* Returns {@code true} if the apparent first queued thread, if one
* exists, is waiting in exclusive mode. If this method returns
* {@code true}, and the current thread is attempting to acquire in
* shared mode (that is, this method is invoked from {@link
* #tryAcquireShared}) then it is guaranteed that the current thread
* is not the first queued thread. Used only as a heuristic in
* ReentrantReadWriteLock.
*/
// AbstractQueuedSynchronizer#apparentlyFirstQueuedIsExclusive
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
//
需要注意的一点是 在读锁模式的非公平性判断中,为了避免需要写锁的线程无限制的等待,
如果等待队列第一个节点是想要获取写锁的线程,那这里读锁就不去争抢锁资源了,乖乖的排队获取资源,就可以避免写锁线程"饥饿状态"
为啥写锁线程会出现"饥饿状态"?
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquireShared(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public boolean tryLock() {
return sync.tryReadLock();
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.releaseShared(1);
}
public Condition newCondition() {
throw new UnsupportedOperationException();
}
}
//
可以看到底层实现都是委托给Sync处理
public static class WriteLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -4992448646407690164L;
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
sync.acquire(1);
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public boolean tryLock( ) {
return sync.tryWriteLock();
}
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
public boolean isHeldByCurrentThread() {
return sync.isHeldExclusively();
}
public int getHoldCount() {
return sync.getWriteHoldCount();
}
}
//
可见写锁的底层实现也是委托给Sync实现
来看一个例子
class CachedData {
Object data;
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// Must release read lock before acquiring write lock
// 在获取写锁之前,必须首先释放读锁
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// Recheck state because another thread might have
// acquired write lock and changed state before we did.
// 这里需要再次判断数据的有效性,因为在释放读锁和获取写锁的空隙之内,可能有其他线程修改了数据。
if (!cacheValid) {
data = "hhhhhh";
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
// 在不释放写锁的情况下,直接获取读锁,这就是读写锁的降级
// 注意:这种情况是在一个线程内
rwl.readLock().lock();
} finally {
// 释放了写锁,但是依然持有读锁
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
// do something for the data start...
// use(data); // will consume long time
System.out.println(data);
// do something for the data end...
} finally {
// 释放读锁
rwl.readLock().unlock();
}
}
}
//
//
锁降级是指同一个线程,先获取写锁,后面可以再获取读锁
为什么需要锁的降级?
为什么不支持锁的升级?
AQS在内部维护一个等待线程队列,其中记录了某个线程请求的是独占方法还是共享访问,在ReentrantReadWriteLock中,
当锁可用时,如果位于队列头部的线程执行写入操作(想要获取写锁),那么线程会得到这个锁,如果位于队列头部的线程执行读取访问操作(想要获取读锁),
那么队列中的第一个写入线程之前的所有线程都将获得这个锁;对于第二种情况,为了防止写锁线程发生"饥饿问题"
读写锁的获取规则
总结为:读读共享、读写互斥、写读互斥、写写互斥
升降级策略:只能从写锁降级为读锁,不能从读锁升级为写锁
以上是个人理解,如有问题请指出,谢谢!