之前整理过AQS(AbstractQueuedSynchronizer) 锁的基础。 还有重入锁(ReentrantLock),Java常见的多是排他锁,这些锁在同一时刻只允许一个线程进行访问,而读写锁在同一时刻可以允许多个读线程访问,但是在写线程访问时,所有的读线程和其他写线程均被阻塞。读写锁维护了一对锁,一个读锁和一个写锁,通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升。
读写锁能有效提高读比写多的场景下的程序性能,比排它锁好。
对应的特性:公平性选择,重进入(读写锁都支持),锁降级。
本文主要是基于《Java并发编程艺术》来整理的,有差别的两点:firstReader 还有读写锁降级。书上出于篇幅一笔带过的地方,后面加上我自己的理解。所以主要思路是:写锁的获取释放,读锁的获取释放,锁降级。
ReadWriteLock仅定义了获取读锁和写锁的两个方法,即readLock()和writeLock()方法
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();
}
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;
/** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock() {
this(false);
}
/** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
/** 返回用于写入操作的锁 */
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
/** 返回用于读取操作的锁 */
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
abstract static class Sync extends AbstractQueuedSynchronizer {}
static final class NonfairSync extends Sync {}
static final class FairSync extends Sync {}
public static class ReadLock implements Lock, java.io.Serializable {}
public static class WriteLock implements Lock, java.io.Serializable {}
}
读写锁内部类关系如下图所示
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L;
Sync抽象类继承自AQS抽象类,Sync类提供了对ReentrantReadWriteLock的支持。
Sync类的属性如下:
abstract static class Sync extends AbstractQueuedSynchronizer {
// 版本序列号
private static final long serialVersionUID = 6317671515068378041L;
// 高16位为读锁,低16位为写锁
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;
// 本地线程计数器
private transient ThreadLocalHoldCounter readHolds;
// 缓存的计数器
private transient HoldCounter cachedHoldCounter;
// 第一个读线程
private transient Thread firstReader = null;
// 第一个读线程的计数
private transient int firstReaderHoldCount;
}
其中三部分:1 高低位状态(后面写),2. 内部类HoldCounter主要与读锁配套使用。内部类ThreadLocalHoldCounter
/**
* ThreadLocal subclass. Easiest to explicitly define for sake
* of deserialization mechanics.
*/
static final class ThreadLocalHoldCounter
extends ThreadLocal {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
重写了ThreadLocal的initialValue方法,ThreadLocal类可以将线程与对象相关联。没有set方法,直接get初始化后的holdcounter.
ReentrantReadWriteLock,除了接口方法之外,还提供了一些便于外界监控其内部工作状态的方法。如下所示
方法名称 | 描述 |
int getReadLockCount() | 返回当前读锁被获取的次数。该次数不等于获取读锁的线程数,比如:仅一个线程,它连续获取(重进入)了n次读锁,那么占据读锁的线程数是1,但该方法返回n |
int getReadHoldCount() | 返回当前线程获取读锁的次数。该方法在Java 6 中加入到ReentrantReadWriteLock中,使用ThreadLocal保存当前线程获取的次数,这也使得Java 6 的实现变得更加复杂 |
boolean isWriteLocked() | 判断写锁是否被获取 |
int getWriteHoldCount() | 返回当前写锁被获取的次数 |
也是依赖上面提到的Sync实现的。举例
final int getReadLockCount() {
return sharedCount(getState());
}
final boolean isWriteLocked() {
return exclusiveCount(getState()) != 0;
}
接下看读写锁的实现设计。
读写锁同样依赖自定义同步器(AQS)来实现同步功能,而读写状态就是其同步器的同步状态。回想ReentrantLock中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态.怎么办啊?简单理解就是“掰开”用。由于读锁可以同时有多个,肯定不能再用辦成两份用的方法来处理了,但我们有 ThreadLocal,可以把线程重入读锁的次数作为值存在 ThreadLocal 里,就是上面sync类的HoldCounter。
下面是《并发编程的艺术》给出的术语:如果在一个整型变量上维护多种状态,就一定需要“按位切割使用”这个变量,读写锁是将变量切分成了两个部分,高16位表示读,低16位表示写,划分方式如图所示。
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;
假设当前同步状态值为S,get和set的操作如下:
(1)获取写状态: // Reentrant acquire
setState(c + acquires);
(4)读状态加1:
写锁是一个支持重进入的排它锁。如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取写锁时,读锁已经被获取(读状态不为0)或者该线程不是已经获取写锁的线程,则当前线程进入等待状态,获取写锁的代码如下
public void lock() {
sync.acquire(1);
}
public void unlock() {
sync.release(1);
}
到就是调用的独占式同步状态的获取与释放,因此真实的实现就是Sync的 tryAcquire和 tryRelease。
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();//aqs
// 用 state & 65535 得到低 16 位的值
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 将 state + 1
setState(c + acquires);
return true;
}
//当需要判断公平锁(非公平情况下,返回 false)或者CAS设置state失败,需要返回false
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
// 修改成功 state 后,修改锁的持有线程。
setExclusiveOwnerThread(current);
return true;
}
该方法除了重入条件(当前线程为获取了写锁的线程)之外,增加了一个读锁是否存在的判断。如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当前写线程的操作。因此只有等待其他读线程都释放了读锁,写锁才能被当前线程所获取,而写锁一旦被获取,则其他读写线程的后续访问均被阻塞。
写锁的释放比较简单。 每次释放均减少写状态,当写状态为0时表示写锁已被释放,从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。
protected final boolean tryRelease(int releases) {
// 是否持有当前锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 计算 state 值
int nextc = getState() - releases;
// 计算写锁的状态,如果是0,说明是否成功。
boolean free = exclusiveCount(nextc) == 0;
// 释放成功,设置持有锁的线程为 null。
if (free)
setExclusiveOwnerThread(null);
// 设置 state
setState(nextc);
return free;
}
读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会成功的被获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已被其他线程获取,则进入等待状态。
类似于写锁,读锁的lock和unlock的实际实现对应Sync的 tryAcquireShared 和 tryReleaseShared方法。不同的事,因为考虑到获取读锁次数总和等,代码实现起来比写锁复杂。
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)) {
//如果是第一个获取读状态的线程,第一个读锁firstRead是不会加入到readHolds中
if (r == 0) {
// 设置第一个读线程
firstReader = current;
// 读线程占用的资源数为1
firstReaderHoldCount = 1;
// 当前线程为第一个读线程,表示第一个读锁线程重入
} else if (firstReader == current) {
//第一个线程对应获取的读锁数量加1
firstReaderHoldCount++;
} else {
//是别的线程
// cachedHoldCounter 代表的是最后一个获取读锁的线程的计数器。
HoldCounter rh = cachedHoldCounter;
//如果最后一个线程计数器是 null 或者不是当前线程,那么就新建一个 HoldCounter 对象
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);
}
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) { // 自旋
// 获取状态
int c = getState();
if (exclusiveCount(c) != 0) { // 写线程数量不为0
if (getExclusiveOwnerThread() != current) // 不为当前线程
return -1;
} else if (readerShouldBlock()) { // 写线程数量为0并且读线程被阻塞
// Make sure we're not acquiring read lock reentrantly
if (firstReader == current) { // 当前线程为第一个读线程
// assert firstReaderHoldCount > 0;
} else { // 当前线程不为第一个读线程
if (rh == null) { // 计数器为空
//
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) { // 计数器为空或者计数器的tid不为当前正在运行的线程的tid
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
if (sharedCount(c) == MAX_COUNT) // 读锁数量为最大值,抛出异常
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) { // 比较并且设置成功,尝试读锁高位+1
if (sharedCount(c) == 0) { // 读线程数量为0
// 设置第一个读线程
firstReader = current;
// 计数器1
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; // cache for release
}
return 1;
}
}
}
fullTryAcquireShared 与tryAcquireShared逻辑类似,就是通过自旋+CAS获取锁。补充一个图更清晰些
读锁的释放(线程安全的,可能有多个读线程同时释放读锁)减少读状态,减少的值是(1 << 16)
protected final boolean tryReleaseShared(int unused) {
// 获取当前线程
Thread current = Thread.currentThread();
if (firstReader == current) { // 当前线程为第一个读线程
//如果第一个获取读锁的线程只获取了一个锁那么firstReader=null
//否则firstReaderHoldCount--
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 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
// 当前线程获取的读锁小于等于1那么就将remove当前线程的HoldCounter移除
readHolds.remove();
if (count <= 0) // 计数小于等于0,抛出异常
throw unmatchedUnlockException();
}
// 当前线程拥有的读锁数量减1
--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;
}
}
书上这里写的比较少。补充下:读锁获取释放里面相关的点
先说下概念:重入还允许从写入锁降级为读取锁,其实现方式是:先获取写入锁,然后获取读取锁,最后释放写入锁(书上用了把持住当前拥有的写锁,比较形象)。但是,从读取锁升级到写入锁是不可能的。
官网api:https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/ReentrantReadWriteLock.html
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 = ...
cacheValid = true;
}
// Downgrade by acquiring read lock before releasing write lock
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock(); // Unlock write, still hold read
}
}
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
上述示例中,当数据发生变更后,update变量(布尔类型且volatile修饰)被设置为false,此时所有访问processData()方法的线程都能够感知到变化,但只有一个线程能够获取到写锁,而其他线程会被阻塞在读锁和写锁的lock()方法上。当前程获取写锁完成数据准备之后,再获取读锁,随后释放写锁,完成锁降级。
书上说的“锁降级中读锁的获取是否必要呢?答案是必要的。主要原因是保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设此刻另一个线程(记作线程T)获取了写锁并修改了数据,则当前线程无法感知线程T的数据更新。如果当前线程获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进行数据更新。”
这里我认为是容易有歧义的,首先我得承认大神很牛,《并发编程的艺术》是本好书,在聊聊并发上我就看了,受益匪浅。
我理解如下:
关于“那么当前线程无法感知线程T的数据更新”我理解,即使是先释放写锁,然后获取读锁也没有问题,只不过会可能会被其他线程的写锁阻塞一段时间(阻塞期间无法感知,但最终获取的数据不是脏数据)。但是并不意味着,随后的这个读操作看不到之前别的线程的写锁下的写操作,只要写锁被释放数据更新还是可以看到的,最终数据是一致的。
如果考虑文章一开头介绍的读写锁适应场景,一般使用锁降级的前提是读优先于写,如果当新线程请求读锁的时候,当前持有写锁的线程需要马上进行降级,保证所有读锁的顺利获取,阻塞后续写锁。
所以猜测锁降级目的或许是为了减少线程的阻塞唤醒。明显当不使用锁降级,线程2修改数据时,线程1自然要被阻塞,而使用锁降级时则不会。感知”其实是想强调读的实时连续性,但是容易让人误导为强调数据操作,锁降级和“可见性”没有太多关系。
****************************
一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁
参考:http://ifeve.com/java-art-reentrantlock/