文章目录
一、ReentrantReadWriteLock 演示
- 当读操作远远高于写操作时,这时候使用 读写锁 让 读-读 可以并发,提高性能。
- 类似于数据库中的 select … from … lock in share mode
ReentrantReadWriteLock 演示读读、读写、写写场景
提供一个 数据容器类 内部分别使用读锁保护数据的 read() 方法,写锁保护数据的 write() 方法
@Slf4j(topic = "c.DataContainer")
class DataContainer {
private Object data;
private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock r = rw.readLock();
private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
public Object read() {
log.debug("尝试获取读锁...");
r.lock();
try {
log.debug("获取读锁成功,读取");
sleep(1);
return data;
} finally {
log.debug("释放读锁...");
r.unlock();
}
}
public void write() {
log.debug("尝试获取写锁...");
w.lock();
try {
log.debug("获取写锁成功,写入");
sleep(1);
} finally {
log.debug("释放写锁...");
w.unlock();
}
}
}
1. 读读共享
public static void main(String[] args) throws InterruptedException {
DataContainer dataContainer = new DataContainer();
new Thread(() -> {
dataContainer.read();
}, "t1").start();
new Thread(() -> {
dataContainer.read();
}, "t2").start();
}
18:21:50.212 c.DataContainer [t2] - 尝试获取读锁...
18:21:50.212 c.DataContainer [t1] - 尝试获取读锁...
18:21:50.215 c.DataContainer [t1] - 获取读锁成功,读取
18:21:50.215 c.DataContainer [t2] - 获取读锁成功,读取
18:21:51.222 c.DataContainer [t2] - 释放读锁...
18:21:51.222 c.DataContainer [t1] - 释放读锁...
2. 读写互斥
public static void main(String[] args) throws InterruptedException {
DataContainer dataContainer = new DataContainer();
new Thread(() -> {
dataContainer.read();
}, "t1").start();
// 1秒后才能获取写锁
Thread.sleep(100);
new Thread(() -> {
dataContainer.write();
}, "t2").start();
}
18:26:18.596 c.DataContainer [t1] - 尝试获取读锁...
18:26:18.603 c.DataContainer [t1] - 获取读锁成功,读取
18:26:18.700 c.DataContainer [t2] - 尝试获取写锁...
18:26:19.611 c.DataContainer [t1] - 释放读锁...
18:26:19.611 c.DataContainer [t2] - 获取写锁成功,写入
18:26:20.622 c.DataContainer [t2] - 释放写锁...
3. 写写互斥
public static void main(String[] args) throws InterruptedException {
DataContainer dataContainer = new DataContainer();
new Thread(() -> {
dataContainer.write();
}, "t1").start();
// 1秒后才能获取写锁
Thread.sleep(100);
new Thread(() -> {
dataContainer.write();
}, "t2").start();
}
18:29:36.142 c.DataContainer [t1] - 尝试获取写锁...
18:29:36.145 c.DataContainer [t1] - 获取写锁成功,写入
18:29:36.248 c.DataContainer [t2] - 尝试获取写锁...
18:29:37.146 c.DataContainer [t1] - 释放写锁...
18:29:37.146 c.DataContainer [t2] - 获取写锁成功,写入
18:29:38.157 c.DataContainer [t2] - 释放写锁...
4. 注意事项
(1)读锁不支持条件变量,写锁支持条件变量
(2)重入时不支持锁升级
- 即持有读锁的情况下去获取写锁,会导致获取写锁永久等待
r.lock();
try {
// ...
w.lock();
try {
// ...
} finally{
w.unlock();
}
} finally{
r.unlock();
}
(3)重入时支持锁降级
- 即持有写锁的情况下去获取读锁
ReentrantReadWriteLock源码注释中的例子,在更新缓存后执行锁降级
class CachedData {
Object data;
// 是否有效,如果失效,需要重新计算 data
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
Object processCachedData() {
rwl.readLock().lock();
// 当缓存数据失效
if (!cacheValid) {
// 获取写锁前必须释放读锁
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// 双重检测,再次判断是否有其它线程已经获取了写锁、更新了缓存, 避免重复更新
if (!cacheValid) {
Thread.sleep(1000);
data = 1;
cacheValid = true;
}
// 降级为读锁
// 释放写锁之前先获取读写,避免其他线程修改
rwl.readLock().lock();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
rwl.writeLock().unlock();
}
}
// 自己用完数据, 释放读锁
try {
// use(data);
return data;
} finally {
rwl.readLock().unlock();
}
}
}
二、缓存更新策略
1. 先清缓存,再更新数据库
- B先清空缓存,B更新数据库需要一定时间;
- A发现没缓存则查数据库,再把结果更新到缓存;
- B更新完了数据库;
- 以后其他线程再查缓存都是A的旧值;
先清缓存,再更新数据库,这种策略不可取
2. 先更新数据库,再清缓存
- B先更新数据库;
- A第一次查的缓存还是旧值;
- B清空缓存;
- 其他线程再次查询时,发现没有缓存会去查数据库,再把最新结果更新到缓存;
先更新数据库,再清缓存,前期会出现短暂的不一致,后面会纠正为一致。
3. ReetrantReadWriteLock 解决缓存一致性问题
三、ReetrantReadWriteLock 原理
- 读写锁用的是同一个 Sycn 同步器,因此等待队列、state 等也是同一个。
1. 读写锁 加锁原理
(1) 先加写锁成功
- t1 成功上锁,state低16位加1,exclusiveOwnerThread置为当前线程,流程与 ReentrantLock 加锁相比没有特殊之处,不同的是写锁状态占了 state 的低 16 位,而读锁使用的是 state 的高 16 位
(2)后加读写失败 - t2 执行 read.lock(),这时进入读锁的 sync.acquireShared(1) 流程,首先会进入 tryAcquireShared 流程。如果有写
锁占据,那么 tryAcquireShared 返回 -1 表示失败
tryAcquireShared 返回值表示:
-1 表示失败;
0 表示成功,但后继节点不会继续唤醒;
正数表示成功,而且数值是还有几个后继节点需要唤醒;
ReetrantReadWriteLock成功返回 1,失败返回-1;(不会用到0和正数,信号量锁会用到)
(3)加读锁失败后,进入AQS队列
- 加读锁失败后会进入 sync.doAcquireShared(1) 流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为
Node.SHARED 模式而非 Node.EXCLUSIVE 模式,注意此时 t2 仍处于活跃状态(这里还没park,后续才会park)
(4)加入队列后,再次尝试加锁失败就park - t2 会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared(1) 来尝试获取锁
- 如果没有成功,在 doAcquireShared 内 for 循环一次,把前驱节点的 waitStatus 改为 -1,再 for 循环一
次尝试 tryAcquireShared(1) 如果还不成功,那么在 parkAndCheckInterrupt() 处 park
(5)再来2个线程,加读锁和写锁,此时会排队
读写锁加锁源码:
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
public static class WriteLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -4992448646407690164L;
private final Sync sync;
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
public void lock() {
// 进入AQS的acquire方法 -> 内部类Sync的tryAcquire方法
sync.acquire(1);
}
}
public static class ReadLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = -5992448646407690164L;
private final Sync sync;
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
// 进入AQS的acquireShared方法 -> 内部类Sync的tryAcquireShared方法
public void lock() {
sync.acquireShared(1);
}
}
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = 6317671515068378041L;
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
// 获取state的低16位,写锁的 state 计数
int w = exclusiveCount(c);
if (c != 0) {
// c != 0 and w == 0 表示有读锁, 或者 exclusiveOwnerThread 不是自己,则获得锁失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 写锁计数超过低 16 位(2^16 -1), 报异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 写锁重入, 获得锁成功
setState(c + acquires);
return true;
}
// 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞
// 判断写锁是否应该阻塞, 或者 尝试更改计数失败,则获得锁失败
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
// 获得锁成功,将exclusiveOwnerThread设为当前线程
setExclusiveOwnerThread(current);
return true;
}
}
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 如果是其它线程持有写锁, 获取读锁失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 读锁的线程计数
int r = sharedCount(c);
// 如果 读锁没阻塞(但是老二是写锁,读锁会阻塞), 并且 小于读锁计数, 并且 尝试增加读锁计数成功(state的高16位加1),则返回加读锁成功
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;
}
// 与 tryAcquireShared 功能类似, 但会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞
return fullTryAcquireShared(current);
}
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
// 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞
final boolean writerShouldBlock() {
return false; // writers can always barge
}
// 非公平锁 readerShouldBlock 看 AQS 队列中第一个节点是否是写锁
// true 则该阻塞, false 则不阻塞
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
}
}
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
public final void acquireShared(int arg) {
// tryAcquireShared 返回-1, 表示获取读锁失败
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
private void doAcquireShared(int arg) {
// 将当前线程关联到一个 Node 对象上, 模式为共享模式
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
// 再一次尝试获取读锁
int r = tryAcquireShared(arg);
if (r >= 0) {
// r 表示可用资源数, 在这里总是 1 允许传播
//(唤醒 AQS 中下一个 Share 节点)
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 是否在获取读锁失败时阻塞(前一个阶段 waitStatus == Node.SIGNAL)
if (shouldParkAfterFailedAcquire(p, node) &&
// park 当前线程
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 设置自己为 head
setHead(node);
// propagate 表示有共享资源(例如共享读锁或信号量)
// 原 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
// 现在 head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 如果是最后一个节点或者是等待共享读锁的节点
if (s == null || s.isShared())
doReleaseShared();
}
}
private void doReleaseShared() {
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
}
2. 读写锁解锁原理
(1) 写锁释放成功
- 写锁释放成功,state低16位置为0,exclusiveOwnerThread置为null
(2)写锁释放后并唤醒读锁加锁成功 - 写锁释放成功后,进入唤醒流程 sync.unparkSuccessor,即让老二恢复运行,这时 t2 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行;
- 会再进行一次 for 循环 执行 tryAcquireShared 成功则让读锁计数加一(state高16位加1)
(3)唤醒读锁加锁成功后,重置head节点 - 这时 t2 已经恢复运行,接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点
(4)检查读锁的下一个节点是否还是shared - 在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared,如果是则调用doReleaseShared() 将 head 的状态从 -1 改为 0(改为0为了防止其他线程同时唤醒) 并唤醒老二,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行
(5)下一个节点仍是shared节点,则继续唤醒
- 唤醒的线程再进行一次 for (;😉 执行 tryAcquireShared 成功则让读锁计数加一
(6)唤醒的第二个读锁加锁成功后,重置head节点 - 这时 t3 已经恢复运行,接下来 t3 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点;
- 下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点
(7).释放第一个读锁 - t2 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一(state高16位减1),但由于计数还不为零
(8)释放最后一个读锁,唤醒写锁 - t3 进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让读锁计数减一(state高16位减1),这回计数为零了,进入 doReleaseShared() 将头节点从 -1 改为 0 并唤醒老二
(9)写锁被唤醒后尝试加锁 - 之后 t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;😉 这次自己是老二,并且没有其他竞争,tryAcquire(1) 成功,state低16位加1,exclusiveOwnerThread置为当前线程,最后修改头结点,流程结束
读写锁解锁源码:
// ReentrantReadWriteLock中的WriteLock中的unlock方法
public void unlock() {
sync.release(1);
}
// ReentrantReadWriteLock中的ReadLock中的unlock方法
public void unlock() {
sync.releaseShared(1);
}
// ReentrantReadWriteLock中的Sync中的tryRelease方法
protected final boolean tryRelease(int releases) {
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
// 因为可重入的原因, 写锁计数为 0, 才算释放成功
// state低16位置为0,exclusiveOwnerThread置为null
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
// ReentrantReadWriteLock中的Sync中的tryAcquireShared方法
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// 如果是其它线程持有写锁, 获取读锁失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 读锁的线程计数
int r = sharedCount(c);
// 如果 读锁没阻塞(但是老二是写锁,读锁会阻塞), 并且 小于读锁计数, 并且 尝试增加读锁计数成功(state的高16位加1),则返回加读锁成功
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);
}
// ReentrantReadWriteLock中的Sync中的tryAcquireShared方法
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))
// 读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程
// 计数为 0 才是真正释放
return nextc == 0;
}
}
// AQS中的release方法
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
// 如果写锁释放成功,唤醒head的后继节点
unparkSuccessor(h);
return true;
}
return false;
}
// AQS中的releaseShared方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
// AQS中的doAcquireShared方法
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
// 读锁加锁成功后,将原来的读锁节点置为head
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
// AQS中的setHeadAndPropagate方法
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 读锁加锁成功后,将原来的读锁节点置为head
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 如果是下一个节点也是shared节点,继续唤醒
if (s == null || s.isShared())
doReleaseShared();
}
}
// AQS中的doReleaseShared方法
private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL 更新为0 成功, 则唤醒下一个shared节点
// 如果 head.waitStatus == 0 更新为 Node.PROPAGATE, 为了解决 bug
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// 如果有其它线程也在释放读锁,那么需要将 waitStatus 先改为 0
// 防止 unparkSuccessor 被多次执行
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
// 下一个节点 unpark 如果成功获取读锁
// 并且下下个节点还是 shared, 继续 doReleaseShared
unparkSuccessor(h);
}
// 如果已经是 0 了,改为 -3,用来解决传播性,见后文信号量 bug 分析
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}