文章目录
- 1、读写锁(ReadWriteLock)简介
- 2、读写锁(ReadWriteLock)接口说明
- 2.1、读写锁(ReadWriteLock)使用示例
- 3、读写锁(ReadWriteLock)特性
- 4、读写锁(ReadWriteLock)实现分析
- 4.1、读写状态的原理
- 4.2、写锁的获取与释放
- 4.2.1、写锁的获取
- 4.2.1、写锁的释放
- 4.3、读锁的获取与释放
- 4.3.1、读锁的获取
- 4.3.1、读锁的释放
- 4.4、锁降级
- 总结
1、读写锁(ReadWriteLock)简介
ReentrantReadWriteLock是Lock的另一种实现方式,同一时间允许多个读线程访问,但是在写线程访问时,所有的读写线程都被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁使并发性比一般的排它锁有了很大的提升。
一般情况下,读写锁的性能都会比排它锁好,因为大多场景下读操作要多于写操作。在读操作多于写操作的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。Java并发包中提供的读写锁的实现是ReentrantReadWriteLock,本文就针对该读写锁的实现做一个全面的学习。
2、读写锁(ReadWriteLock)接口说明
public interface ReadWriteLock {
/**
* Returns the lock used for reading.
*
* @return the lock used for reading
*/
Lock readLock();
/**
* Returns the lock used for writing.
*
* @return the lock used for writing
*/
Lock writeLock();
}
ReadWriteLock 仅仅定义了获取读锁(readLock() )和写锁(writeLock() )的两个方法,而该接口的实现ReentrantReadWriteLock,除了实现接口中的方法外,还提供了一些便于监控内部工作状态的方法。
//ReentrantReadWriteLock.java
/**
* 获取当前读锁被获取的次数
*/
public int getReadLockCount() {
return sync.getReadLockCount();
}
/**
* 获取当前线程获取写锁的次数
*/
public int getWriteHoldCount() {
return sync.getWriteHoldCount();
}
/**
* 获取当前线程获取读锁的次数
*/
public int getReadHoldCount() {
return sync.getReadHoldCount();
}
/**
* 判断当前写锁对象是否被获取
*/
public boolean isWriteLocked() {
return sync.isWriteLocked();
}
/**
* 判断当前线程是否获取了写锁
*/
public boolean isWriteLockedByCurrentThread() {
return sync.isHeldExclusively();
}
/**
* 返回正在等待获取写锁的线程的集合。返回的线程集合没有特定的顺序
*/
protected Collection<Thread> getQueuedWriterThreads() {
return sync.getExclusiveQueuedThreads();
}
/**
* 返回正在等待获取读锁的线程的集合
*/
protected Collection<Thread> getQueuedReaderThreads() {
return sync.getSharedQueuedThreads();
}
/**
* 获取正在等待获取读锁或写锁线程的集合
*/
protected Collection<Thread> getQueuedThreads() {
return sync.getQueuedThreads();
}
/**
* 获取正在等待获取读锁或写锁线程的数量
*/
public final int getQueueLength() {
return sync.getQueueLength();
}
2.1、读写锁(ReadWriteLock)使用示例
接下来使用一个缓存示例,学习一下读写锁的使用方式。缓存中使用一个非线程安全的HashMap 作为缓存对象的实现,同时使用读写锁来保证对缓存对象cacheMap操作的线程安全。
/**
*
*
* @author kaifeng
* @date 2018/12/9
*/
public class ReadWriteLockCache {
private static Map<String, Object> cacheMap = new HashMap<String, Object>();
private static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private static Lock readLock = rwl.readLock();
private static Lock writeLock = rwl.writeLock();
/**
* 根据指定key获取值
*
* @param key 指定key
*/
public static Object get(String key) {
readLock.lock();
try {
return cacheMap.get(key);
} finally {
readLock.unlock();
}
}
/**
* 更新指定key对应的值,并返回旧值
*/
public static Object put(String key, Object value) {
writeLock.lock();
try {
return cacheMap.put(key, value);
} finally {
writeLock.unlock();
}
}
/**
* 清空所有缓存内容
*/
public static void clear() {
writeLock.lock();
try {
cacheMap.clear();
} finally {
writeLock.unlock();
}
}
}
1、读操作 get(String key) 中,需要获取读锁,使并发访问该方法时不会被阻塞。
2、写操作 put(String key, Object value) 和 clear() 中,在更新 cacheMap之前必须先获取写锁,当获取写锁后,其它线程对读写锁的获取都会被阻塞,只有写锁释放后,其它读写操作才能继续。
3、cacheMap 使用读写锁提高了读操作的并发性,也保证了写操作对所有读写操作的可见性,同时使编程方式更简化了
3、读写锁(ReadWriteLock)特性
1)支持公平和非公平的获取锁的方式;
2)支持可重入。读线程在获取了读锁后还可以获取读锁;写线程在获取了写锁之后既可以再次获取写锁又可以获取读锁;
3)允许锁降级。从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁。但是,从读取锁升级到写入锁是不允许的;
4)支持锁中断。读取锁和写入锁都支持锁获取期间的中断;
5)Condition支持。仅写入锁提供了一个 Conditon 实现;读取锁不支持 Conditon ,当使用 readLock().newCondition() 时会抛出 UnsupportedOperationException。
4、读写锁(ReadWriteLock)实现分析
4.1、读写状态的原理
同步状态在重入锁的实现中是表示被同一个线程重复获取的次数,即一个整形变量来维护,但是之前的那个表示仅仅表示是否锁定,而不用区分是读锁还是写锁。而读写锁需要在同步状态(一个整形变量)上维护多个读线程和一个写线程的状态。
读写锁对于同步状态的实现是在一个整形变量上通过【按位切割使用】:将变量切割成两部分,高16位表示读,低16位表示写。
当前 同步 状态 表示 一个 线程 已经 获取 了 写 锁, 且 重 进入 了 两次, 同时 也 连续 获取 了 两次 读 锁。 读写 锁 是 如何 迅速 确定 读 和 写 各自 的 状态 呢?
当前同步状态表示一个线程获取了写锁,且重入了两次,同时也连续获取了两次读锁。
那么读写锁时如何快速确定读写各自的状态的呢?
假设当前同步状态值为S,get和set的操作如下:
1、获取写状态:
S&0x0000FFFF : 将高16位全部抹去
2、获取读状态:
S>>>16 : 无符号补0,右移16位
3、写状态加1:
S+1
4、读状态加1:
S+(1<<16)即S + 0x00010000
根据状态的划分能得出一个推论:
如果S不等于0,当写状态(S&0x0000FFFF)等于0时,而读状态(S>>>16)大于0,则表示读锁已被获取。
4.2、写锁的获取与释放
写锁是一个支持可重入的排它锁,如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取或另一个线程已经获取了写锁,则当前线程将进入等待状态。
4.2.1、写锁的获取
下面看一下写锁获取的代码实现:
protected final boolean tryAcquire(int acquires) {
//当前线程
Thread current = Thread.currentThread();
//获取同步状态
int c = getState();
//写线程数量
int w = exclusiveCount(c);
//当前同步状态c != 0,说明其他线程获取了读锁或写锁
if (c != 0) {
//存在读锁或当前线程不是持有写锁的线程
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//判断同一线程获取写锁是否超过最大次数(65535)
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//此时当前线程已持有写锁,现在是重入,所以只需要修改锁的获取次数即可
setState(c + acquires);
return true;
}
//此时c=0,读锁和写锁都没有被获取,writerShouldBlock表示是否阻塞
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//设置锁为当前线程所有
setExclusiveOwnerThread(current);
return true;
}
由源码可知,写锁获取的实现步骤如下:
1、首先获取当前同步状态(c)和获取写锁线程的数量(w)
2、如果同步状态 c != 0,说明已经有其他线程获取了读或写锁,进入判断(3),否则进入(5)
3、如果同步状态 c != 0 并且 w==0 或当前线程不是持有写锁的线程则返回false,当前线程不能获取写锁
4、如果同步状态 c != 0,判断当前线程获取写锁的次数是否超过了最大值,如果超过则抛出异常,否则更新同步状态(累加获取写锁线程的数量),返回 true
5、如果同步状态 c == 0,表示读锁或写锁都没有被获取,则CAS更新同步状态,若CAS成功则返回true,失败则说明锁被别的线程抢去了,返回false。
4.2.1、写锁的释放
protected final boolean tryRelease(int releases) {
//若锁的持有者不是当前线程,抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//写锁的新线程数
int nextc = getState() - releases;
//如果独占模式重入数为0了,说明独占模式被释放
boolean free = exclusiveCount(nextc) == 0;
if (free)
//若写锁的新线程数为0,则将锁的持有者设置为null
setExclusiveOwnerThread(null);
//设置写锁的新线程数
//不管独占模式是否被释放,更新独占重入数
setState(nextc);
return free;
}
写锁的释放过程相对而言比较简单,首先查看当前线程是否为写锁的持有者,如果不是抛出异常。然后检查释放后写锁的持有线程数是否为0,如果为0则表示写锁空闲了,释放锁资源将锁的持有线程设置为null,否则仅仅只是一次重入锁而已,并不能将写锁的线程清空。
4.3、读锁的获取与释放
读锁是一个支持可重入的共享锁,它能够被多个线程同时获取,在写状态为0时,读锁总是会被成功获取,需要的操作仅仅是增加读状态。如果当前线程在获取读锁时,写锁已经被其它线程持有,则当前线程进入等待状态。
类似于写锁,读锁的lock和unlock的实际实现对应Sync的 tryAcquireShared 和 tryReleaseShared方法。
4.3.1、读锁的获取
protected final int tryAcquireShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
// 获取同步状态
int c = getState();
//如果写锁线程数 != 0 ,且独占锁不是当前线程则返回失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 读锁数量
int r = sharedCount(c);
/*
* readerShouldBlock():读锁是否需要等待(公平锁原则)
* r < MAX_COUNT:持有线程小于最大数(65535)
* compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态
*/
// 读线程是否应该被阻塞、并且小于最大值、并且比较设置成功
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
//r == 0,表示第一个读锁线程,第一个读锁firstRead是不会加入到readHolds中
if (r == 0) { // 读锁数量为0
// 设置第一个读线程
firstReader = current;
// 读线程占用的资源数为1
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 当前线程为第一个读线程,表示第一个读锁线程重入,占用资源数加1
firstReaderHoldCount++;
} else { // 读锁数量不为0并且不为当前线程
// 获取计数器
HoldCounter rh = cachedHoldCounter;
// 计数器为空或者计数器的tid不为当前正在运行的线程的tid
if (rh == null || rh.tid != getThreadId(current))
// 获取当前线程对应的计数器
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0) // 计数为0
//加入到readHolds中
readHolds.set(rh);
//计数+1
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}
tryAcquireShared(int unused) 方法中,如果其它线程已经获取了写锁,则当前线程获取读锁失败,进入等待状态。如果当前线程获取了写锁或写锁未被获取,则当前线程增加读状态,成功获取读锁。读锁的每次释放都是减少读状态的值,减少的值时( 1<< 16)
4.3.1、读锁的释放
protected final boolean tryReleaseShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
if (firstReader == current) { // 当前线程为第一个读线程
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1) // 读线程占用的资源数为1
firstReader = null;
else // 减少占用的资源
firstReaderHoldCount--;
} else { // 当前线程不为第一个读线程
// 获取缓存的计数器
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
// 获取当前线程对应的计数器
rh = readHolds.get();
// 获取计数
int count = rh.count;
if (count <= 1) { // 计数小于等于1
// 移除
readHolds.remove();
if (count <= 0) // 计数小于等于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.
return nextc == 0;
}
}
读锁线程释放锁,首先判断当前线程是否为第一个读线程firstReader,若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1,若是,则设置第一个读线程firstReader为空,否则,将第一个读线程占有的资源数firstReaderHoldCount减1;若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器 ),若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器,如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可。无论何种情况,都会进入无限循环,该循环可以确保成功设置状态state
4.4、锁降级
锁降级是指写锁降级为读锁。如果当前线程拥有写锁,然后释放掉,再获取读锁,这种分开完成的过程不能称为锁降级。锁降级是线程持有写锁的情况下再去获取读锁,然后释放掉写锁,仍持有读锁。
public void processData() {
readLock.lock();
if (!update) {
// 必须 先 释放 读 锁
readLock.unlock();
// 锁 降级 从 写 锁 获取 到 开始
writeLock.lock();
try {
if (!update) {
// 准备 数据 的 流程( 略)
update = true;
}
readLock.lock();
} finally {
writeLock.unlock();
} // 锁 降级 完成, 写 锁 降级 为 读 锁
}
try {
// 使用 数据 的 流程( 略)
} finally {
readLock.unlock();
}
}
当数据发生变更后,update 变量被设置为false,此时所有访问 processData() 方法 的线程都能感知到变化,但只有一个线程能够获取到写锁,其他线程会被阻塞在读锁和 写锁的lock() 方法上。当前线程获取写锁完成数据准备之后,再获取读锁,随后释放写 锁,完成锁降级。
总结
1、在线程持有读锁的情况下,该线程不能取得写锁,因为获取写锁的时候,如果发现当前的读锁被占用,立即就会获取失败,不管读锁是不是被当前线程持有。
2、在线程持有写锁的情况下,该线程可以继续获取读锁,获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败。
3、锁降级是合理的,因为当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。
4、一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁。