一、读写锁介绍
现实中有这样一种场景
- 对共享资源有读和写的操作,且写操作没有读操作那么频繁(读多写少)
- 在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源(读读可以并发)
- 但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写操作了(读写,写读,写写互斥)
- 在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。
针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它内部,维护了一对相关的锁,一个用于只读操作,称为读锁;一个用于写入操作,称为写锁
线程进入读锁的前提条件:
- 没有其他线程的写锁
- 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个
线程进入写锁的前提条件:
- 没有其他线程的读锁
- 没有其他线程的写锁
而读写锁有以下三个重要的特性:
- 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
- 可重入:读锁和写锁都支持线程重入。以读写线程为例:读线程获取读锁后,能够再次获取读锁。写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。
- 锁降级:遵循获取写锁、再获取读锁最后释放写锁的次序,写锁能够降级成为读锁。
二、ReentrantReadWriteLock的使用
ReentrantReadWriteLock类结构
ReentrantReadWriteLock是可重入的读写锁实现类。在它内部,维护了一对相关的锁,一个用于读操作,另一个用于写操作。只要没有 Writer 线程,读锁可以由多个 Reader 线程同时持有。也就是说,写锁是独占的,读锁是共享的
。
注意事项
- 读锁不支持条件变量
- 重入时升级不支持:持有读锁的情况下去获取写锁,会导致获取永久等待
- 重入时支持降级: 持有写锁的情况下可以去获取读锁
应用场景
ReentrantReadWriteLock适合读多写少的场景
public class Cache {
static Map<String, Object> map = new HashMap<String, Object>();
static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
static Lock r = rwl.readLock();
static Lock w = rwl.writeLock();
// 获取一个key对应的value
public static final Object get(String key) {
r.lock();
try {
return map.get(key);
} finally {
r.unlock();
}
}
// 设置key对应的value,并返回旧的value
public static final Object put(String key, Object value) {
w.lock();
try {
return map.put(key, value);
} finally {
w.unlock();
}
}
// 清空所有的内容
public static final void clear() {
w.lock();
try {
map.clear();
} finally {
w.unlock();
}
}
上述示例中
- Cache组合一个非线程安全的HashMap作为缓存的实现,同时使用读写锁的读锁和写锁来保证Cache是线程安全的。
- 在读操作get(String key)方法中,需要获取读锁,这使得并发访问该方法时不会被阻塞。
- 写操作put(String key,Object value)方法和clear()方法,在更新 HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而只有写锁被释放之后,其他读写操作才能继续。
- Cache使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性,同时简化了编程方式
三、ReentrantReadWriteLock源码分析
读写状态的设计
设计的精髓:用一个变量如何维护多种状态
- 在 ReentrantLock 中,使用 Sync ( 实际是 AQS )的 int 类型的 state 来表示同步状态,表示锁被一个线程重复获取的次数。
- 但是,读写锁 ReentrantReadWriteLock 内部维护着一对读写锁,如果要用一个变量维护多种状态,需要采用
“按位切割使用”
的方式来维护这个变量,将其切分为两部分:高16为表示读
,低16为表示写
代码分析
- exclusiveCount(int c) 方法,获得持有写状态的锁的次数。
- sharedCount(int c) 方法,获得持有读状态的锁的线程数量。
不同于写锁,读锁可以同时被多个线程持有。而每个线程持有的读锁支持重入的特性,所以需要对每个线程持有的读锁的数量单独计数,这就需要用到 HoldCounter 计数器
HoldCounter 计数器
- 读锁的内在机制其实就是一个共享锁。
- 一次共享锁的操作就相当于对HoldCounter 计数器的操作。
- 获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。
- 只有当线程获取共享锁后才能对共享锁进行释放、重入操作。
3.1 写锁的获取
- 写锁是一个支持重进入的排它锁。
- 如果当前线程已经获取了写锁,则增加写状态。
- 如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程, 则当前线程进入等待状态。
- 写锁的获取是通过重写AQS中的tryAcquire方法实现的。
public final void acquire(int arg) {
// 尝试获取锁资源(看一下,能否以CAS的方式将state 从0 ~ 1,改成功,拿锁成功)
// 成功走人
// 不成功执行下面方法
if (!tryAcquire(arg) &&
// addWaiter:将当前没按到锁资源的,封装成Node,排到AQS里
// acquireQueued:当前排队的能否竞争锁资源,不能挂起线程阻塞
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}、
// state,高16:读,低16:写
00000000 00000000 00000000 00000000
00000000 00000001 00000000 00000000 - SHARED_UNIT
00000000 00000000 11111111 11111111 - MAX_COUNT
00000000 00000000 11111111 11111111 - EXCLUSIVE_MASK
&
00000000 00000000 00000000 00000001
static final int SHARED_SHIFT = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
// 只拿到表示读锁的高16位。
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 只拿到表示写锁的低16位。
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
// 读写锁的写锁,获取流程
protected final boolean tryAcquire(int acquires) {
// 拿到当前线程
Thread current = Thread.currentThread();
// 拿到state
int c = getState();
// 拿到了写锁的低16位标识w
int w = exclusiveCount(c);
// c != 0:要么有读操作拿着锁,要么有写操作拿着锁
if (c != 0) {
// 如果w == 0,代表没有写锁,拿不到!拜拜!
// 如果w != 0,代表有写锁,看一下拿占用写锁是不是当前线程,如果不是,拿不到!拜拜!
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 到这,说明肯定是写锁,并且是当前线程持有
// 判断对低位 + 1,是否会超过MAX_COUNT,超过抛Error
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 如果没超过锁重入次数, + 1,返回true,拿到锁资源。
setState(c + acquires);
return true;
}
// 到这,说明c == 0
// 读写锁也分为公平锁和非公平锁
// 公平:看下排队不,排队就不抢了
// 走hasQueuedPredecessors方法,有排队的返回true,没排队的返回false
// 非公平:直接抢!
// 方法实现直接返回false
if (writerShouldBlock() ||
// 以CAS的方式,将state从0修改为 1
!compareAndSetState(c, c + acquires))
// 要么不让抢,要么CAS操作失败,返回false
return false;
// 将当前持有互斥锁的线程,设置为自己
setExclusiveOwnerThread(current);
return true;
}
通过源码我们可以知道:
- 读写互斥
- 写写互斥
- 写锁支持同一个线程重入
- writerShouldBlock写锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)
思考:有线程读的过程中不允许写,这种设计有什么问题?
3.2 写锁的释放
写锁释放通过重写AQS的tryRelease方法实现
// 写锁的释放锁
public final boolean release(int arg) {
// 只有tryRealse是读写锁重新实现的方法,其他的和ReentrantLock一致
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
// 读写锁的真正释放
protected final boolean tryRelease(int releases) {
// 判断释放锁的线程是不是持有锁的线程
if (!isHeldExclusively())
// 不是抛异常
throw new IllegalMonitorStateException();
// 对state - 1
int nextc = getState() - releases;
// 拿着next从获取低16位的值,判断是否为0
boolean free = exclusiveCount(nextc) == 0;
// 返回true
if (free)
// 将持有互斥锁的线程信息置位null
setExclusiveOwnerThread(null);
// 将-1之后的nextc复制给state
setState(nextc);
return free;
}
3.3 读锁的获取
实现共享式同步组件的同步语义需要通过重写AQS的tryAcquireShared方法和tryReleaseShared方法。
// 读锁加锁操作
public final void acquireShared(int arg) {
// tryAcquireShared,尝试获取锁资源,获取到返回1,没获取到返回-1
if (tryAcquireShared(arg) < 0)
// doAcquireShared 前面没拿到锁,这边需要排队~
doAcquireShared(arg);
}
// tryAcquireShared方法
protected final int tryAcquireShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
// 拿到state
int c = getState();
// 那写锁标识,如果 !=0,代表有写锁
if (exclusiveCount(c) != 0 &&
// 如果持有写锁的不是当前线程,排队去!
getExclusiveOwnerThread() != current)
// 排队!
return -1;
// 没有写锁!
// 获取读锁信息
int r = sharedCount(c);
// 公平锁: 有人排队,返回true,直接拜拜,没人排队,返回false
// 非公平锁:正常的逻辑是非公平直接抢,因为是读锁,每次抢占只要CAS成功,必然成功
// 这就会出现问题,写操作无法在读锁的情况抢占资源,导致写线程饥饿,一致阻塞…………
// 非公平锁会查看next是否是写锁的,如果是,返回true,如果不是返回false
if (!readerShouldBlock() &&
// 查看读锁是否已经达到了最大限制
r < MAX_COUNT &&
// 以CAS的方式,对state的高16位+1
compareAndSetState(c, c + SHARED_UNIT)) {
// 拿到锁资源成功!!!
if (r == 0) {
// 第一个拿到锁资源的线程,用first存储
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 我是锁重入,我就是第一个拿到读锁的线程,直接对firstReaderHoldCount++记录重入的次数
firstReaderHoldCount++;
} else {
// 不是第一个拿到锁资源的
// 先拿到cachedHoldCounter,最后一个线程的重入次数
HoldCounter rh = cachedHoldCounter;
// rh == null: 我是第二个拿到读锁的!
// 或者发现之前有最后一个来的,但是不我,将我设置为最后一个。
if (rh == null || rh.tid != getThreadId(current))
// 获取自己的重入次数,并赋值给cachedHoldCounter
cachedHoldCounter = rh = readHolds.get();
// 之前拿过,现在如果为0,赋值给TL
else if (rh.count == 0)
readHolds.set(rh);
// 重入次数+1,
// 第一个:可能是第一次拿
// 第二个:可能是重入操作
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
// 通过tryAcquireShared没拿到锁资源,也没返回-1,就走这
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
// 拿state
int c = getState();
// 现在有互斥锁,不是自己,拜拜!
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
// 公平:有排队的,进入逻辑。 没排队的,过!
// 非公平:head的next是写不,是,进入逻辑。 如果不是,过!
} else if (readerShouldBlock()) {
// 这里代码特别乱,因为这里的代码为了处理JDK1.5的内存泄漏问题,修改过~
// 这个逻辑里不会让你拿到锁,做被阻塞前的准备
if (firstReader == current) {
// 什么都不做
} else {
if (rh == null) {
// 获取最后一个拿到读锁资源的
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
// 拿到我自己的记录重入次数的。
rh = readHolds.get();
// 如果我的次数是0,绝对不是重入操作!
if (rh.count == 0)
// 将我的TL中的值移除掉,不移除会造成内存泄漏
readHolds.remove();
}
}
// 如果我的次数是0,绝对不是重入操作!
if (rh.count == 0)
// 返回-1,等待阻塞吧!
return -1;
}
}
// 超过读锁的最大值了没?
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 到这,就CAS竞争锁资源
if (compareAndSetState(c, c + SHARED_UNIT)) {
// 跟tryAcquireShared一模一样
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++;
cachedHoldCounter = rh;
}
return 1;
}
}
}
- 读锁共享,读读不互斥
- 读锁可重入,每个获取读锁的线程都会记录对应的重入数
- 读写互斥,锁降级场景除外
- 支持锁降级,持有写锁的线程,可以获取读锁,但是后续要记得把读锁和写锁读释放
- readerShouldBlock读锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)
3.4 读锁的释放
获取到读锁,执行完临界区后,要记得释放读锁(如果重入多次要释放对应的次数),不然会阻塞其他线程的写操作。
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//如果当前线程是第一个获取读锁的线程
if (firstReader == current) {
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--; //重入次数减1
} 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; //重入次数减1
}
for (;;) { //cas更新同步状态
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.
return nextc == 0;
}