持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第4天,点击查看活动详情
使用
在业务开发过程中,我们对数据的操作大多时读多写锁的场景,但是当某个数据变更时,此时该数据不允许被其他线程进行读写,只能等待线程写操作完成后,其他线程才能进行读写;
final Map<K, V> m =
new HashMap<>();
final ReadWriteLock rwl =
new ReentrantReadWriteLock();
// 读锁
final Lock r = rwl.readLock();
// 写锁
final Lock w = rwl.writeLock();
// 读缓存
V get(K key) {
r.lock();
try { return m.get(key); }
finally { r.unlock(); }
}
// 写缓存
V put(K key, V value) {
w.lock();
try { return m.put(key, v); }
finally { w.unlock(); }
}
}
使用规则
- 允许多个线程同时读共享变量;
- 只允许一个线程写共享变量;
- 如果一个写线程正在执行写操作,此时禁止读线程读共享变量。
原理
分析规则
首先读写互斥,那么必然存在一个共享变量进行cas操作保证互斥;同一时刻只允许一个线程进行写操作,其他线程需要等待,必然存在一个队列保存处于等待的线程.这个思想其实与AQS一致; 所以现在结合AQS来分析其实现;
获取读锁
protected final int tryAcquireShared(int unused) {
/*
* Walkthrough:
* 1. If write lock held by another thread, fail.
* 2. Otherwise, this thread is eligible for
* lock wrt state, so ask if it should block
* because of queue policy. If not, try
* to grant by CASing state and updating count.
* Note that step does not check for reentrant
* acquires, which is postponed to full version
* to avoid having to check hold count in
* the more typical non-reentrant case.
* 3. If step 2 fails either because thread
* apparently not eligible or CAS fails or count
* saturated, chain to version with full retry loop.
*/
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
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);
}
state
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 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;
state在共享锁模式下,被分为两部分,高16位代表读锁次数,低16位代表获取写锁次数(包含重入次数);
抢占逻辑
- 抢占成功,累加state中读锁次数.记录当前线程重入次数(采用ThreadLocal线程隔离的思想保存HoldCounter)
- 抢占不成功,加入到clh队列中;
firstReader 如何能保证始终是一个获取读锁的线程
关于firstReader的可见性从这么几个方面来分析: 前置条件state=0;
- 当线程t1先进行cas操作成功时,那么此时state必定大于0;
- 线程t2进行通过原值为o去cas操作时必定失败; 其实存在三条happens-before规则;
- 程序顺序性规则 state=0 在cas 之前;
- volatile变量规则,对volatile的写操作happens-before后续的读操作
- 传递性规则 A happens-before B,B happens-before C,那么A happens-before C;
抢占写锁逻辑
抢占写锁与互斥锁逻辑一致不在进行分析;
特殊场景
假设存在三个线程进行抢占读写锁;t1获取读锁成功,t2获取写锁,t3获取读锁
初始状态:
公平锁模式
线程t3抢占读锁
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
再公平锁模式下,只要队列中存在等待的node节点,那么该线程就进入clh队列,阻塞并等待唤醒
非公平锁模式
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // writers can always barge
}
final boolean readerShouldBlock() {
/* As a heuristic to avoid indefinite writer starvation,
* block if the thread that momentarily appears to be head
* of queue, if one exists, is a waiting writer. This is
* only a probabilistic effect since a new reader will not
* block if there is a waiting writer behind other enabled
* readers that have not yet drained from the queue.
*/
return apparentlyFirstQueuedIsExclusive();
}
}
当且仅当第一个node节点是独占模式的线程时才被阻塞;这么做的目的是避免抢占锁的线程出现线程饥饿;试想一下:其他线程频繁获取读锁不释放,导致获取写锁的线程一直无法被唤醒,进而造成线程饥饿死锁''
线程饥饿
线程饥饿是一种因为长期无法获取到共享资源或CPU而导致线程无法被执行的现象''
锁升级
先是获取读锁,然后再升级为写锁,对此还有个专业的名字,叫锁的升级。可惜 ReadWriteLock 并不支持这种升级;
原因:
一个线程获取到读锁,然后又获取写锁,那么其他获取读锁的线程势必也能进行,那么这样会造成同时获取写锁的线程存在多个,与写锁线程互斥的原则相违背;
锁降级
先是获取写锁,然后再降级为读锁;
class CachedData {
Object data;
volatile boolean cacheValid;
final ReadWriteLock rwl =
new ReentrantReadWriteLock();
// 读锁
final Lock r = rwl.readLock();
//写锁
final Lock w = rwl.writeLock();
void processCachedData() {
// 获取读锁
r.lock();
if (!cacheValid) {
// 释放读锁,因为不允许读锁的升级
r.unlock();
// 获取写锁
w.lock();
try {
// 再次检查状态
if (!cacheValid) {
data = ...
cacheValid = true;
}
// 释放写锁前,降级为读锁
// 降级是可以的
r.lock(); ①
} finally {
// 释放写锁
w.unlock();
}
}
// 此处仍然持有读锁
try {use(data);}
finally {r.unlock();}
}
}
从上述代码可以看出,先获取写锁成功后,在未释放写锁时获取读锁也能成功; 与之相对应的源码为:
protected final int tryAcquireShared(int unused) {
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
}
当写锁次数大于0说明存在获取写锁的线程,当获取线程的线程与当前线程相等时才进行下一步操作,否则获取读锁失败;