读写锁
对共享资源有读和写的操作,且写操作没有读操作那么频繁(读多写少)。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该允许多个线程同时读取共享资源(读读可以并发);但是如果一个线程想去写这些共享资源,就不应该允许其他线程对该资源进行读和写操作了(读写,写读,写写互斥)。在读多于写的情况下,读写锁能够提供比排它锁更好的并发性和吞吐量。
线程进入读锁的条件
- 没有其他线程的写锁
- 没有写请求或者有写请求,但调用线程和持有锁的线程是同一个
线程进入写锁的条件
- 没有其他线程的读锁
- 没有其他线程的写锁
读写操作时互斥的,只有读读的时候可以获取到锁,其他时候读写、写读、写写都是互斥的,只能有一个。
读写锁特性
- 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
- 可重入:读锁和写锁都支持线程重入。以读写线程为例:读线程获取读锁后,能够再次获取读锁。写线程在获取写锁之后能够再次获取写锁,同时也可以获取读锁。
- 锁降级:遵循获取写锁、再获取读锁最后释放写锁的次序,写锁能够降级成为读锁。
类结构
顶层接口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();
}
实现类中定义了一个只读的锁和一个只写的锁,读锁可共享,而写锁只能独占。
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;
/**
* Creates a new {@code ReentrantReadWriteLock} with
* default (nonfair) ordering properties.
*/
public ReentrantReadWriteLock() {
this(false);
}
/**
* Creates a new {@code ReentrantReadWriteLock} with
* the given fairness policy.
*
* @param fair {@code true} if this lock should use a fair ordering policy
*/
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
注意事项
读锁不支持条件变量
重入时升级不支持:持有读锁的情况下去获取写锁,会导致获取永久等待
重入时支持降级: 持有写锁的情况下可以去获取读锁
使用非线程安全的HashMap
class RWDictionary {
private final Map<String, Data> m = new HashMap<String, Data>();
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
public Data get(String key) {
r.lock();
try { return m.get(key); }
finally { r.unlock(); }
}
public String[] allKeys() {
r.lock();
try { return m.keySet().toArray(); }
finally { r.unlock(); }
}
public Data put(String key, Data value) {
w.lock();
try { return m.put(key, value); }
finally { w.unlock(); }
}
public void clear() {
w.lock();
try { m.clear(); }
finally { w.unlock(); }
}
}
上述代码中使用的是非线程安全的HashMap,在更新 HashMap时必须提前获取写锁,当获取写锁后,其他线程对于读锁和写锁的获取均被阻塞,而 只有写锁被释放之后,其他读写操作才能继续。使用读写锁提升读操作的并发性,也保证每次写操作对所有的读写操作的可见性,同时简化了编程方式
锁降级
是指先获取写锁,再获取到读锁,随后释放写锁的过程。锁降级可以帮助我们拿到当前线程修改后的结果而不被其他线程所破坏,防止更新丢失。
使用案例
private final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
private final Lock r = rwl.readLock();
private final Lock w = rwl.writeLock();
private volatile boolean update = false;
public void processData() {
readLock.lock();
if (!update) {
// 必须先释放读锁
readLock.unlock();
// 锁降级从写锁获取到开始
writeLock.lock();
try {
if (!update) {
// TODO 准备数据的流程(略)
update = true;
}
readLock.lock();
} finally {
writeLock.unlock();
}
// 锁降级完成,写锁降级为读锁
}
try {
//TODO 使用数据的流程(略)
} finally {
readLock.unlock();
}
}
获取读锁的作用,是为了保证数据的可见性;防止有其他线程此时也获取写锁并修改数据,导致当前线程无法感知到,而加了读锁的话,其他线程在获取写锁的时候会被阻塞,只有当前线程释放了读写锁后才能执行。
源码分析
类图结构
从类结构图可知,ReenReaderWriterLock实现了ReaderWriterLock接口,在内部有一对读写锁ReaderLock和WriterLock,都是实现Lock的子类;而对于读写锁的加锁解锁逻辑,来源于实现Aqs的Syn,写锁通过tryAcquire和tryRelease方法进行加锁解锁,而读锁则通过tryAcquireShared和tryReleaseShared方法。
比较精髓的部分在于,读写锁的状态控制,在于仅仅使用了一个变量来对两个锁的状态进行记录。采用“按位切割使用”的方式来维护这个变量,将其切分为两部分:高16为表示读,低16为表示写。分割之后,读写锁是如何迅速确定读锁和写锁的状态呢?通过位运算。假如当前同步状态为S,
- 写状态,等于 S & 0x0000FFFF(将高 16 位全部抹去)。 当写状态加1,等于S+1.
- 读状态,等于 S >>> 16 (无符号补 0 右移 16 位)。当读状态加1,等于S+(1<<16),也就是S+0x00010000
根据状态的划分能得出一个推论:S不等于0时,当写状态(S&0x0000FFFF)等于0时,则读状态(S>>>16)大于0,即读锁已被获取。
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;
/** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
exclusiveCount(int c) 静态方法,获得持有写状态的锁的次数。
sharedCount(int c) 静态方法,获得持有读状态的锁的线程数量。不同于写锁,读锁可以同时被多个线程持有。而每个线程持有的读锁支持重入的特性,所以需要对每个线程持有的读锁的数量单独计数,这就需要用到HoldCounter计数器。
读锁
读锁是一个支持重入的共享锁。
HoldCounter 计数器
读锁的内在机制其实就是一个共享锁。一次共享锁的操作就相当于对HoldCounter 计数器的操作。获取共享锁,则该计数器 + 1,释放共享锁,该计数器 - 1。只有当线程获取共享锁后才能对共享锁进行释放、重入操作。
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
/**
* ThreadLocal subclass. Easiest to explicitly define for sake
* of deserialization mechanics.
*/
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
ThreadLocalHoldCounter与HolderCounter进行绑定,后者记录线程锁重入的次数;而ThreadLocalHoldCounter则记录锁重入时的线程对象,这个线程对象是第一次之后的重入线程
获取锁
Thread current = Thread.currentThread();
int c = getState();
// 如果写锁已经被获取并且获取写锁的线程不是当前线程,
//当前线程获取读锁失败返回‐1 判 断锁降级
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
/**
* 读锁是否阻塞 readerShouldBlock()公平与非公平的实现
* r < MAX_COUNT: 持有读锁的线程小于最大数(65535)
* compareAndSetState(c, c + SHARED_UNIT) cas设置获取读锁线程的数量
*/
if (!readerShouldBlock() &&
r < MAX_COUNT &&
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 1;
}
// 尝试通过自旋的方式获取读锁,实现了重入逻辑
return fullTryAcquireShared(current);
}
- 读锁共享,读读不互斥
- 读锁可重入,每个获取读锁的线程都会记录对应的重入数
- 读写互斥,锁降级场景除外
- 支持锁降级,持有写锁的线程,可以获取读锁,但是后续要记得把读锁和写锁读释 放
- readerShouldBlock读锁是否阻塞实现取决公平与非公平的策略(FairSync和 NonfairSync)
释放锁
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.
return nextc == 0;
}
}
写锁
写锁是一个支持重入的排它锁。线程获取写锁的时候会先判断读锁是否已经被获取或者是写锁已经被获取,若是则阻塞等待。
获取锁
protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
//获取锁状态,不为0就是存在读锁或写锁
int c = getState();
//获取写锁的重入次数
int w = exclusiveCount(c);
//状态不为0,有线程获取了读锁或写锁
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
// c!=0 && w==0 表示存在读锁
// 当前存在读锁或者写锁已经被其他写线程获取,则写锁获取失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 超出最大范围 65535
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
//同步state状态
setState(c + acquires);
return true;
}
// writerShouldBlock有公平与非公平的实现, 非公平返回false,会尝试通过cas加锁
//c==0 写锁未被任何线程获取,当前线程是否阻塞或者cas尝试获取锁
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
//设置写锁为当前线程
setExclusiveOwnerThread(current);
return true;
}
- 读写互斥
- 写写互斥
- 写锁支持同一个线程重入
- writerShouldBlock写锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)
释放锁
protected final boolean tryRelease(int releases) {
//若锁的持有者不是当前线程,抛出异常
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
//当前写状态是否为0,为0则释放写锁
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
由源码可知:
- 读读共享
- 读写互斥
- 写读互斥
- 写写互斥
- 写锁支持同一个线程重入,读锁也可重入
- 读写锁是否阻塞实现取决公平与非公平的策略(FairSync和NonfairSync)
- 释放锁时,重入了多少次就要释放多少次,不然会阻塞后续线程