读写锁
读写锁
1、JUC 提供两种实现
(1)ReentrantReadWriteLock
(2)StampedLock
2、当读操作远远高于写操作时,使用读写锁,读-读可以并发,提高性能
3、类似于数据库中的 select ... from ... lock in share mode
4、提供一个数据容器类
(1)内部使用读锁保护数据的 read()
(2)内部使用写锁保护数据的 write()
ReentrantReadWriteLock
1、返回用于阅读的锁
public ReentrantReadWriteLock.ReadLock readLock()
2、返回用于写入的锁
public ReentrantReadWriteLock.WriteLock writeLock()
3、读锁-读锁:可以并发;读锁-写锁、写锁-写锁:相互阻塞
4、事项
(1)读锁不支持条件变量,写锁支持条件变量
(2)重入时不支持升级:不允许持有读锁的情况下,去获取写锁,否则导致获取写锁永久等待
(3)重入时支持降级:允许持有写锁的情况下,去获取读锁
5、写锁状态占 state 的低 16 位,而读锁使用的是 state 的高 16 位
读写锁应用:缓存
1、先清理缓存,后更新数据库
(1)更新速度较慢,期间可能发生多次查询
(2)导致数据库、缓存数据不一致
2、先更新数据库,后清空缓存
(1)可能产生数据库、缓存数据不一致
(2)可以保证后续查询数据的正确性
(3)以下情况的出现几率非常小:查询线程 A 查询数据时,恰好缓存数据由于时间到期失效,或是第一次查询
(4)若需要严格的数据一致,则加锁
ReentrantReadWriteLock.WriteLock 加写锁
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
//获取当前线程
Thread current = Thread.currentThread();
//获取state
int c = getState();
//获取写锁(独占锁)计数
int w = exclusiveCount(c);
//c != 0,已加锁
if (c != 0) {
if (
//w == 0,说明为读锁
w == 0 ||
//w != 0,说明为写锁,判断是否为当前线程的写锁
current != getExclusiveOwnerThread())
//加写锁失败
return false;
//若为当前线程的写锁,其重入数+1,超过65535,则抛出异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//重入写锁
setState(c + acquires);
return true;
}
//c == 0,未加锁
if (
//是否应该阻塞
//公平锁:调用hasQueuedPredecessors(),检查 AQS 队列中是否有非空节点, 有则阻塞
//非公平锁:总返回false
writerShouldBlock() ||
//尝试将写锁state从0改为1
!compareAndSetState(c, c + acquires))
//加写锁失败,返回false
return false;
//加写锁成功,exclusiveOwnerThread设置为当前线程
setExclusiveOwnerThread(current);
return true;
}
ReentrantReadWriteLock.ReadLock 加读锁
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
1、tryAcquireShared 返回值
(1)-1:表示失败
(2)0:表示成功,但后继节点不会继续唤醒
(3)正数表示成功,其值表示有几个后继节点需要唤醒
protected final int tryAcquireShared(int unused) {
//获取当前线程
Thread current = Thread.currentThread();
//获取state
int c = getState();
if (
//是否有写锁
exclusiveCount(c) != 0 &&
//是否为当前线程的写锁
getExclusiveOwnerThread() != current)
//返回-1,表示加读锁失败
return -1;
//获取读锁(共享锁)计数
int r = sharedCount(c);
if (
//读锁是否需要阻塞
!readerShouldBlock() &&
//读锁数量是否超出65535
r < MAX_COUNT &&
//尝试将c+65535,对于高16位,为+1,即读锁计数+1
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++;
}
//读锁+1成功
return 1;
}
//与tryAcquireShared类似,但会不断尝试 for (;;) 获取读锁, 执行过程中无阻塞
return fullTryAcquireShared(current);
}
private void doAcquireShared(int arg) {
//创建共享模式节点
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
//获取node的前驱节点
final Node p = node.predecessor();
//判断p是否为头节点
if (p == head) {
//再次尝试获得锁
int r = tryAcquireShared(arg);
if (r >= 0) {
//成功获取读锁
//唤醒node所关联的线程,设置node为新的头节点,回收原头节点
//r表示可用资源数,允许传播(唤醒AQS中下r个共享节点)
//检查下一个是否为共享节点,如果是,将head的state从-1改为0,并唤醒第二个节点的线程
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);
}
}
private void setHeadAndPropagate(Node node, int propagate) {
//记录原head,以方便在下面检查
Node h = head;
//设置node为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的后继节点
Node s = node.next;
if (
//node已经是最后的节点
s == null ||
//s是共享节点
s.isShared())
//唤醒后续所有共享节点,直到遇见独占节点
doReleaseShared();
}
}
private void doReleaseShared() {
for (;;) {
Node h = head;
//队列还有除head以外的节点
if (h != null && h != tail) {
//获取head的waitStatus
int ws = h.waitStatus;
//若ws == -1
if (ws == Node.SIGNAL) {
//尝试将ws从-1改为0,防止其他线程干扰,即防止多次唤醒第二个节点
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
//修改失败,则继续尝试
continue; // loop to recheck cases
//修改成功,唤醒head的后继节点
unparkSuccessor(h);
}
//尝试将ws从0改为-3,以解决传播性,
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
//修改失败,继续尝试
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
ReentrantReadWriteLock.WriteLock 释放写锁
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
//获取头节点
Node h = head;
if (
//头节点不为null
h != null &&
//头节点state不为0
h.waitStatus != 0)
//唤醒头节点的后继节点中的线程,后继节点为新的头节点,回收原头节点
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//当前线程是否持有写锁
if (!isHeldExclusively())
//没有持有写锁,则抛出异常
throw new IllegalMonitorStateException();
//nextc = state - 1
int nextc = getState() - releases;
//写锁可重入计数-1
boolean free = exclusiveCount(nextc) == 0;
if (free)
//写锁可重入计数为0,才释放exclusiveOwnerThread的线程
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
ReentrantReadWriteLock.ReadLock 释放读锁
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
//读锁计数为0,才进入if块
doReleaseShared();
return true;
}
return false;
}
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 (;;) {
//获取state
int c = getState();
//c-65536,对于高16位,等价于-1,即读锁计数-1
int nextc = c - SHARED_UNIT;
//尝试修改读锁计数
if (compareAndSetState(c, nextc))
//修改成功,则查看读锁计数是否为0
//读锁为0,则返回true
//读锁不为0,返回false,但没有变量接收
return nextc == 0;
}
}
private void doReleaseShared() {
for (;;) {
Node h = head;
//队列还有除head以外的节点
if (h != null && h != tail) {
//获取head的waitStatus
int ws = h.waitStatus;
//若ws == -1
if (ws == Node.SIGNAL) {
//尝试将ws从-1改为0,防止其他线程干扰,即防止多次唤醒第二个节点
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
//修改失败,则继续尝试
continue; // loop to recheck cases
//修改成功,唤醒head的后继节点
unparkSuccessor(h);
}
//尝试将ws从0改为-3,以解决传播性
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
//修改失败,继续尝试
continue; // loop on failed CAS
}
if (h == head) // loop if head changed
break;
}
}
StampedLock
1、从 JDK 8 加入
2、在使用读锁、写锁时,都必须配合 stamp 使用
(1)加、解读锁
long stamp = lock.readLock();
lock.unlockRead(stamp);
(2)加、解写锁
long stamp = lock.writeLock();
lock.unlockWrite(stamp);
3、乐观读
(1)StampedLock 支持 tryOptimisticRead(),不使用 CAS,优化读性能
(2)读取完毕后,需要做一次 stamp 校验,如果校验通过,表示这期间确实没有写操作,数据可以安全使用,如果校验没通过,需要重新获取读锁,保证数据安全
long stamp = lock.tryOptimisticRead();
// 验戳
if(!lock.validate(stamp)){
// 锁升级
}
4、缺点
(1)不支持条件变量
(2)不支持可重入
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】凌霞软件回馈社区,博客园 & 1Panel & Halo 联合会员上线
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】博客园社区专享云产品让利特惠,阿里云新客6.5折上折
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 微软正式发布.NET 10 Preview 1:开启下一代开发框架新篇章
· 没有源码,如何修改代码逻辑?
· PowerShell开发游戏 · 打蜜蜂
· 在鹅厂做java开发是什么体验
· WPF到Web的无缝过渡:英雄联盟客户端的OpenSilver迁移实战