一.概述
ReentrantReadWriteLock是在AQS的基础上实现的一个可重入锁。该锁具备重入锁的可重入性
、可中断获取锁
等特征,但是与ReentrantLock
不一样的是,它的内部维护了一把读锁和一把写锁,读锁是共享锁,写锁是排他锁。这样就保证了写数据时的线程安全性,又保证了读数据时的多线程并发,比较适合读取数据较多而写数据较少的并发场景。写锁是独占的,写时不能有其他线程写也不能读;所有的独锁都释放完之前也不能加写锁。
ReentrantReadWriteLock
实现了ReadWriteLock
接口,其中在ReentrantReadWriteLock
中分别声明了以下几个静态内部类:
WriteLock
与ReadLock
(维护的一对读写锁):单从类名我们可以看出这两个类的作用,就是控制读写线程的锁Sync
及其子类NofairSync
与FairSync
:ReentrantReadWriteLock(读写锁)
是支持公平锁与非公平锁的。Sync中的ThreadLoclHoldCounter
及HoldCounter
:涉及到锁的重进入。
二.使用示例
使用ReentrantReadWriteLock进行TreeMap的读写操作。
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
class ReentrantReadWriteLockTest {
private final Map<String, String> m = new TreeMap<String, String>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();//获取读锁
private final Lock w = rwl.writeLock();//获取写锁
//读取Map中的对应key的数据
public String get(String key) {
r.lock();
try {
return m.get(key);
} finally {
r.unlock();
}
}
//往Map中写数据
public String put(String key, String value) {
w.lock();
try {
return m.put(key, value);
} finally {
w.unlock();
}
}
}
三.原理
1.读写锁需要维护3个数据:
- 写锁的可重入次数;
- 读锁的个数;
- 读锁的可重入次数。
那既然需要维护三个数据,,同步队列中只有一个int
类型的state
变量来表示当前的同步状态。那么其内部是怎么将两个读写状态分开,并且达到控制线程的目的的呢?
在ReentrantReadWriteLock
中的同步队列,其实是将同步状态分为了两个部分,其中高16位
表示读状态(所有读线程获取锁次数)
,低16位
表示写状态
,具体情况如下图所示:
这里大家需要注意的是,在实际的情况中,读状态与写状态是不能被不同线程同时赋值的。因为根据ReentrantReadWriteLock的设计来说,读写操作线程是互斥的。上图中这样表示,只是为了帮助大家理解同步状态的划分
。
获取读写状态:
- 读状态:想要获取读状态,只需要将当前同步变量
无符号右移16位
- 写状态:我们只需要将当前同步状态(这里用S表示)进行这样的操作
S&0x0000FFFF)
,也就是S&(1<<16-1)
。
- 当
state=0
时,读线程和写线程都不持有锁。 - 当
state!=0
,sharedCount(c)!=0
时表示读线程持有锁。 - 当
state!=0
,exclusiveCount(c)!=0
时表示写线程持有锁。
四.HoldCounter和ThreadLocalHoldCounter
取出state高16位的对应的数值表示是所有线程获得读锁的次数,但是如何获得单个线程获得共享锁的次数呢?内部类Sync为同步器维护了一个读锁计数器,专门统计每个线程获得读锁的次数。Sync内部有两个内部类分别为HoldCounter和ThreadLocalHoldCounter:
abstract static class Sync extends AbstractQueuedSynchronizer {
static final class HoldCounter {
//计数器,用于统计线程重入读锁次数
int count = 0;
// Use id, not reference, to avoid garbage retention
//线程TID,区分线程,可以唯一标识一个线程
final long tid = getThreadId(Thread.currentThread());
}
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
//重写初始化方法,在没有进行set的情况下,获取的都是该HoldCounter值
public HoldCounter initialValue() {
return new HoldCounter();
}
}
private transient ThreadLocalHoldCounter readHolds;
private transient HoldCounter cachedHoldCounter;
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
Sync() {
//本地线程读锁计数器
readHolds = new ThreadLocalHoldCounter();
setState(getState()); // ensures visibility of readHolds
}
}
- firstReader和firstReaderHoldCount
如果只有一个线程获取了读锁,就不需要使用本地线程变量readHolds,当前线程就是第一个获得读锁的线程firstReader,使用firstReaderHoldCount存储线程重入次数。 - readHolds
第一个获得读锁的线程使用firstReaderHoldCount存储读锁重入次数,后面的线程就要使用ThreadLocal类型变量readHolds了,每个线程拥有自己的副本,用来保存自己的重入数。 - cachedHoldCounter
缓存计数器,是最后一个获取到读锁的线程计数器,每当有新的线程获取到读锁,这个变量都会更新。如果当前线程不是第一个获得读锁的线程,先到缓存计数器cachedHoldCounter查看缓存计数器是否指向当前线程,不是再去readHolds查找,通过缓存提高效率。