读写锁
-
同时允许多个读线程获取这个锁(读锁)
读写锁允许多个读线程同时获取到读锁(共享模式),但是当写线程获取到了写锁时其他任何读写操作(同一个线程依旧可以继续获取读锁,详细看下文,锁降级)均会被阻塞(排他模式)。很多场景是读大大多于写操作的,所以使用读写锁能更大程度的增强程序的吞吐量。 -
按位切割维护两把锁
一个private volatile int state
变量维护了两把锁:读锁、写锁
private volatile int state
是一个int变量,有32bit:高16位维护读锁,低16位维护写锁。 -
使用场景
用于读多于写的场景,读写锁能够比排他锁提供更好的并发性、吞吐量!
使用示例:
/**
* @author zhangsh
*
* 读操作大于写操作的场景,使用读写锁提高吞吐量
*/
public class Cache {
static Map<String, Object> map = new HashMap<String, Object>();
static ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
static Lock readLock = readWriteLock.readLock();
static Lock writeLock = readWriteLock.writeLock();
public static final Object get(String key) {
readLock.lock();
try {
return map.get(key);
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
}
return null;
}
public static final void put(String key, Object value) {
writeLock.lock();
try {
map.put(key, value);
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
public static final void remove(String key) {
writeLock.lock();
try {
map.remove(key);
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
public static final void clear() {
writeLock.lock();
try {
map.clear();
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
}
}
}
当多个线程操作这个集合(Map)时,一个写线程获取到写锁是会阻塞其他线程,当写操作完成时,其他读操作的线程继续执行,保证了写操作的可见性,避免脏读。
ReentrantReadWriteLock中有三个关键的成员变量:
/**
* 静态内部类
*
* 组合sync,用sync实现读锁
*/
private final ReentrantReadWriteLock.ReadLock readerLock;
/**
* 静态内部类
*
* 组合sync,用sync实现写锁
*/
private final ReentrantReadWriteLock.WriteLock writerLock;
/**
*abstract static class Sync AQS的资料sync实现底层同步器.
*
* Sync有两个子类,公平同步器FairSync;非公平同步器NonFairSync
*/
final Sync sync;
ReentrantReadWriteLock.sync
/**
Read vs write count extraction constants and functions. Lock state is
* logically divided into two unsigned shorts: The lower one
* representing the exclusive (writer) lock hold count, and the upper
* the shared (reader) hold count.
*
* 以下是读写锁的计数器提取后常量、函数。
*
* 锁的state状态(int 32bit)被逻辑上,通过位运算,分为两个无符号short数(16bit):
* 这低16bit代表互斥的排他锁的持有状态(写锁),
* 高16bit代表共享锁的持有状态(读锁)。
*
*
*/
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;
}
.........
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
/**
*/
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
/**
* 用来记录每个获取读锁的线程id以及线程重入情况。
* 当前线程持有的读锁的重入次数,每个线程获取的读锁状态保存在ThreadLocal中
* 仅在构造器和readObject时完成初始化。当一个读锁的重入数量为0时清除
*/
private transient ThreadLocalHoldCounter readHolds;
/**
* 共享锁的计数器缓存(记录最后一个获取读锁成功的线程情况:线程ID,线程重入次数)
*/
private transient HoldCounter cachedHoldCounter;
/**
*用來引用首个获取共享读锁的线程(CAS 0——>1).
*/
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
ReentrantReadWriteLock特性
- 公平性非公平性可选择
使用Sync的两个子类FairSync、NonfairSync,实现获取锁的公平性、非公平性。
/**
* Nonfair version of Sync
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge 不去考虑FIFO规则,直接CAS尝试
}
final boolean readerShouldBlock() {
//头结点之后是否存在一个独占式的结点(写操作),如果存在就要阻塞进入排队中
//也就是在线程获取读锁之前,如果有写锁等待,那么会阻塞
return apparentlyFirstQueuedIsExclusive();
}
}
/**
* Fair version of Sync
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();//判断头结点后继结点是否是当前线程结点。也就是不许插队,尝试插队就阻塞。
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
总结:
在ReentrantReadWriteLock中实现了公平锁与非公平锁,主要区别就是:当一个线程尝试获取公平锁时,会检查这个结点是否是头结点的后继者,也就是不允许插队,插队就阻塞!
与ReentrantLock中的公平锁、非公平锁类似,默认使用非公平锁。非公平锁有更高的吞吐率,更低的线程切换消耗!
-
可重入
一个线程获取读锁后,这个线程可以再次获取这个读锁,但是这个线程不能获取对应写锁。
一个线程获取写锁后,这个线程可以再次获取这个写锁,同时它也可以获取对应的读锁。 -
锁降级
见后文。
写锁的获取
与ReentrantLock类似,继承AQS,实现如下模板方法:
protected boolean tryAcquire(int arg);
protected boolean tryRelease(int arg);
protected int tryAcquireShared(int arg);
protected boolean tryReleaseShared(int arg);
protected boolean isHeldExclusively() ;
写锁属于排他锁,自然需要实现tryAcquire、tryRelease
ReentrantReadWriteLock.WriteLock.lock()
public void lock() {
sync.acquire(1);//调用sync父类AQS.acquire
}
AbstractQueuedSynchronizer.acquire(int arg)
AQS请参看AQS文章AQS详解
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
ReentrantReadWriteLock.Sync.tryAcquire(int acquires)
/**
* 写锁的获取
* @param acquires
* @return
*/
protected final boolean tryAcquire(int acquires) {
/*
* 1.当读锁被其他线程占用则失败;写锁被其他线程占用则失败。(写锁的排他性决定了获取成功的前提是:读写锁都空闲)
* 当读锁或写锁任意一个被占有时,读锁被任意线程持有,写锁的获取失败 ;
* 当读锁或写锁任意一个被占有时,读锁未被占用,写锁被占用,并且占用写锁的是其他线程,该线程获取锁失败.
* 也就是说一个线程试图获取锁时,如果有任何一个线程已经占用了写锁,或者读锁,则失败。看来,写锁的获取完全是排他的获取。
*
* 2.如果计数超过阈值,则失败(重入次数检查)
*
*
* 3.如果1、2通过,则判断是否满足公平要求,然后使用CAS获取写锁
*/
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
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;
if (w + exclusiveCount(acquires) > MAX_COUNT)// 写锁 重入的次数超过阈值
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);// 写锁重入
return true;// true成功
}
// 读写锁完全free:
// 1.若是公平锁,writerShouldBlock保证了当前线程之前没有等待结点,从而保证FIFO。若是非公平锁,直接忽略这个判断
// 2.CAS更新状态
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}
总结:
1.根据state的高低位,确认读锁写锁占用情况,满足条件尝试CAS获取锁:
读写锁中至少一个被占用:
(1)读锁被占用,返回false,继续执行AQS同步队列等待逻辑。
(2)写锁被其他线程占用,返回false,继续执行AQS同步队列等待逻辑。
(3)写锁被当前线程占用,检查重入限制,通过检查再次重入,state++,返回TRUE;
读写锁均空闲:
若是公平模式,则考虑是否插队,插队返回FALSE;
没有插队或者是非公平模式,则CAS获取写锁;成功则返回TRUE,否则返回FALSE;
2.失败则进入AQS同步队列逻辑。参看AQS文章。
tryLock()方法与lock()方法中的tryAcquire执行逻辑非常类似,就是只去尝试获取锁,失败直接返回不会进入AQS同步队列中
写锁的释放
ReentrantReadWriteLock.WriteLock.unlock()
public void unlock() {
sync.release(1);
}
AbstractQueuedSynchronizer.release(int arg)
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
ReentrantReadWriteLock.Sync.tryRelease(int releases)
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())// !当前锁是否排他模式被占有?
throw new IllegalMonitorStateException();
int nextc = getState() - releases;// state计数器准备释放一次release
boolean free = exclusiveCount(nextc) == 0;// 获取排他模式下的计数器个数
if (free)// 当前写锁是否空闲
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
总结
锁的释放都大同小异,就是释放同步状态,并唤醒后继者。
读锁的获取释放
当写锁没有被其他非当前线程持有时,读锁通过每次增加值,达到多个线程获取锁的共享效果。同时维护读锁被获取到的数量与读锁被某个线程重入的数量(ThreadLocal)。
ReentrantReadWriteLock.ReadLock.lock()
public void lock() {
sync.acquireShared(1);
}
AbstractQueuedSynchronizer.acquireShared(int arg)
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
ReentrantReadWriteLock.Sync.tryAcquireShared(int unused)
protected final int tryAcquireShared(int unused) {
/*
* 此方法实现用于读锁。
* 1.写锁被其他线程占有,失败
* 2.当前是否满足排队策略?如果满足排队策略,并且不需要等待,那么CAS.注意此步骤没有检查重入性获取。
* 3.如果2因为CAS失败或者饱和异常或者没有资格,那么绑定版本号循环再试
*/
Thread current = Thread.currentThread();
int c = getState();
// 如果:写锁被占用 && 当前线程不是占有锁的线程——>写锁被其他线程占用时,读锁的获取被阻塞
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;// -1表示失败
int r = sharedCount(c);
// 如果:读线程不需要被阻塞(当头结点的后继结点为独占模式 将会阻塞,为了不产生脏读,要考虑之前进入的写操作) &&
// 读锁提取数没有饱和 && CAS(注意此处 c+SHARED_UNIT)
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {// 读锁的提取数为0(当前线程占用之前,读锁未被占用)
firstReader = current;// 当前线程称为第一个读线程
firstReaderHoldCount = 1;
} else if (firstReader == current) {// (这个锁已经被其他读线程共享了)当前线程是否是第一个共享读写线程
firstReaderHoldCount++;// 首读线程计数+1,代表重入!
} else {// 读锁之前已经被共享了,当前线程不是获取读锁的首个线程
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))// cachedHoldCounter不为空||
// 当前线程不是缓存的线程
cachedHoldCounter = rh = readHolds.get();
// 不为空且是当前线程,并且cahce的锁记录为0,说明当前仅有首线程获取到了锁(可能其他线程已经释放了读锁)
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;// 锁重入情况
}
return 1;
}
// CAS失败后再次尝试读锁获取
return fullTryAcquireShared(current);
}
总结
1.当写锁被其他线程占用,返回false,进入AQS同步队列中等待(doAcquireShared)。
2.如果写锁没有被其他线程占用,满足公平性要求,低于共享锁最大共享次数,尝试CAS获取共享锁。
3.CAS获取共享锁成功,记录当前线程、重入次数等信息,返回
4.CAS获取共享锁失败,进入fullTryAcquireShared(),循环重试获取锁。
另外:对于读锁,每个线程各自获取读锁的次数只能选择保存在ThreadLocal中
锁降级:写锁降级为读锁
public void processData() {
readLock.lock();
if (!update) {// 如果未更新完成
readLock.unlock();// 必须先释放读锁
writeLock.lock();// 锁降级:1.获取写锁
try{
if (!update) {
// 准备数据....略
update = true;
}
readLock.lock();// 锁降级:2.获取读锁。写锁获取后这线程可以继续获取读锁,反之不行
}catch (Exception e) {
e.printStackTrace();
}finally {
writeLock.unlock(); 锁降级:3.释放写锁
}
}
try{
// 读取更新后的数据.........
}finally{
readLock.unlock();
}
}
}
降级:
同一个线程下,获取写锁——>获取读锁——>释放写锁(降级为读锁完成)——>释放读锁。
降级的目的:
为了保证吞吐性,已经可见性,线程A先获取写锁,更新数据,接着需要读取更新后的数据。在这个“更新数据”与 "读取更新后的数据"之间不允许其他线程对数据进行修改,通过直接降级为读锁阻塞了其他线程对数据的更新。同时读锁更大程度的提供了吞吐性。