AQS之ReentrantLock源码
2024-08-31 13:16:23 0 举报
ReentrantLock是基于AQS(AbstractQueuedSynchronizer)实现的一种可重入锁。它的核心内容包括:通过CAS操作实现公平或非公平的锁获取策略;使用等待队列管理阻塞的线程;利用volatile变量实现锁状态的同步。源码中使用了大量的volatile和atomic包中的方法,保证了多线程环境下的可见性和原子性。此外,ReentrantLock支持中断、超时和条件变量等高级功能。
作者其他创作
大纲/内容
准备阻塞
ReentrantLock.Sync#nonfairTryAcquire
state = 1
node.prev = pred
return false
ws < 0
return true
锁未被持有
唤醒后 CAS 获取锁成功
parkAndCheckInterrupt()
AbstractQueuedSynchronizer#addWaiter
head
CAS 更新tail 节点失败则去该方法中自旋直到入队成功
thread1
thread = thread1nextWaiter = nullwaitStatus = 0
node.thread = null
表示该线程是因为中断而被唤醒的
加锁入口
exclusiveOwnerThread = thread1
return t
java.util.concurrent.locks.AbstractQueuedSynchronizer#release
当 prev 节点成为head 节点且当前线程获取锁成功开始将 head 出队,最后当前线程的Node 成为新的 head
入队操作和 addWaiter 的 pred != null 为 true 的情况下的代码是一样的
释放锁
为什么要在这里设置线程为中断状态?1. 阻塞的线程被唤醒后会在检查中断状态时清除中断标记2. 所以需要还原因中断而被唤醒的线程的状态为中断
nextc = c + acquires
调用 park 对当前线程进行阻塞
锁已经释放的标记
false表示当前线程被别的线程unpark唤醒
CAS 更新 tail 节点
更新state
tail
setExclusiveOwnerThread(current)
head 的waitStatus 小于0时需要 CAS 更新为 0
获取锁失败
解锁入口
尾节点为空说明同步等待队列还没有初始化
prev
尾节点是否为空
node 的 thread属性设为空
尾节点为空初始化同步等待队列CAS 确保同步等待队列只能由一个线程去初始化
Node.EXCLUSIVE 代表独占锁模式
tail 节点更新成功old tail --next--> node
Node h = head
acquire(1)
CAS 将 当前线程 prev Node 的 waitStatus 值更新为 -1
去唤醒 next node 对应的线程
入队
原 head 节点的 next 不指向任何节点这样原 head 节点就完成了出队
入队操作
next
state = 0
LockSupport.unpark(s.thread)
Node t = tail
末尾节点先赋值给临时变量 pred
同步队列为空
unparkSuccessor(h)
尾节点给临时变量
p.next = null
thread = thread2nextWaiter = nullwaitStatus = 0
CAS 加锁失败
park 阻塞 thread1
head 的 next Node
唤醒 thread1 线程
锁的持有者置为空
return node
c == 0
准备阻塞 thread2
AbstractQueuedSynchronizer#shouldParkAfterFailedAcquire
CAS state
prev 节点的waitStatus 属性值为 -1
current == getExclusiveOwnerThread()
java.util.concurrent.locks.ReentrantLock#lock
直接先CAS获取锁
t.next = node
exclusiveOwnerThread = null
tryAcquire(arg)
setExclusiveOwnerThread(null)
free = true
当前线程对应的 Node成功入队到了同步等待队列的末尾
出队
独占锁非重入情况下 c 就是 0 了
return Thread.interrupted()
Thread.currentThread() != getExclusiveOwnerThread()
selfInterrupt()
LockSupport.park(this)
head 节点出队thread1 节点成为新的 head 节点
enq(node)
pred.next = node
1. 如果正常 unpark 唤醒的话,这里一般是能获取到锁的,如果获取失败因为唤醒时 head 的 waitStatus 会置为 0,所以还有一次 CAS 的机会 极端情况下 unpark 唤醒后线程获取锁失败重新进入阻塞,且没有新的线程来竞争锁,那么同步等待队列中似乎没有任何线程能推进队列,或者唤醒已经阻塞的线程,这会导致死锁吗?-> 不会,操作系统的调度或者 JVM 的内置机制会确保线程被唤醒(比如定期的中断检查、线程的优先级调整等)2. 这里获取锁失败基本都是因为阻塞中的线程因中断而被唤醒导致的,需要继续 park 阻塞
当前线程获取锁成功并完成出队跳出循环
解锁
head 不为空且 head 的 waitStatus 值不等于 0
compareAndSetHead(new Node())
node.prev = null
CAS 竞争初始化失败的线程重试进行入队操作
在添加到同步等待队列前还会尝试一次获取锁
tryAcquire(1)
阻塞的线程因中断而被唤醒且恰好获取锁成功需要设置线程为中断状态
同步等待队列可能为空:没有发生过竞争,队列未初始化同步等待队列只有一个节点:head 和 tail 为同一个节点,线程都执行完成了
sync.release(1)
park 阻塞线程
ReentrantLock.NonfairSync#tryAcquire
thread = nullnextWaiter = nullwaitStatus = -1
获取锁成功设置当前线程为锁的持有者用于释放锁校验和锁重入场景
CAS 将 tail 节点更新为当前线程对应的 Node
IllegalMonitorStateException
Node p = node.predecessor()
CAS 加锁成功
return interrupted
return free
node 成为新的 head
开始阻塞
唤醒前的准备(head 的 waitStatus 更新为 0)
node.prev = t
获取锁失败则会添加到同步等待队列中阻塞
不是锁的持有者抛异常
setHead(node)
thread2
head = node
这里传入的是 head 节点
锁已被别的线程持有
setExclusiveOwnerThread(Thread.currentThread())
thread0
thread = thread1nextWaiter = nullwaitStatus = -1
CAS state竞争锁
判断当前线程是否是锁的持有者锁必须由锁的持有者释放
next node 不为空时unpark 唤醒 next node 对应的线程
AbstractQueuedSynchronizer#enq
当前线程的 prev Node 是 head 但是获取锁失败
interrupted = true
true表示当前线程因中断而被唤醒
unpark 唤醒线程
false锁还没有释放(比如重入锁的情况重入几次就要释放几次)
唤醒 next node
入队添加到同步等待队列末尾
ws = node.waitStatus
在 park 前需要确保 prev Node 的 waitStatus 值为 -1
java.util.concurrent.locks.ReentrantLock.Sync#tryRelease
c = getState() - releases
获取锁成功
h != null && h.waitStatus != 0
当前线程的 prev Node 不是 head 则会继续阻塞符合先入先出
head 为空:同步队列为空head 的 waitStatus 值为 0:当head 的 waitStatus 值为 -1 时才能去唤醒 next Node上述情况都不需要去 unpark 唤醒线程
java.util.concurrent.locks.ReentrantLock#unlock
别的线程 unpark 唤醒阻塞的当前线程则返回false阻塞的当前线程因为中断而被唤醒且恰好此时 prev 节点是 head 并且释放了锁,此时当前因中断而被唤醒的线程就能获取到锁,这种情况就会返回 true
node 成为 head 那么 prev 得是空
for (;;) { // 循环直到当前线程的 Node 入队成功}
当前线程对应 Node 的上一个节点
tryRelease(1)
state 为 0说明锁已经被释放了
唤醒 head 的 next Node
获取锁成功设置当前线程为锁的持有者
t == null
state != 0锁还没有释放成功
AbstractQueuedSynchronizer#unparkSuccessor
CAS 更新 tail 节点失败入队重试
当 prev Node 的 waitStatus 值 为-1时返回true,如果不为-1则先 CAS 改为 -1 然后返回false 进行重试
AbstractQueuedSynchronizer#acquireQueued
thread1 的 prev Node(head 节点)的 waitStatus 更新为 -1
独占模式下阻塞已经入队成功的线程其它线程 unpark 唤醒当前线程后重新获取锁并执行出队操作
入队成功跳出循环
setState(c)
exclusiveOwnerThread = thread0
当 prev 节点成为head 节点时当前线程可以去尝试获取锁
Node pred = tail
调用 park 阻塞当前线程
唤醒同步等待队列中阻塞的线程
AQS 加锁解锁演示
state 为 0释放锁成功
先判断下 state 的值是否为 0不为 0 的话说明锁已被持有就不需要去 CAS 了
初始化成功head 节点和 tail 节点为同一个节点
同步等待队列初始化成功
tail = head
为什么这里不需要 CAS 更新 state?重入锁是重入锁是同一个线程在执行,线程安全的
线程被别的线程 unpark 唤醒或者因为线程中断而被唤醒所以要检查线程的中断状态
重入锁增加重入次数
为什么释放锁时不需要 CAS?只有持有锁的线程才能去释放锁,所以释放锁不会出现并发执行的情况,是线程安全的,所以不需要 CAS
for (;;) { // 循环直到当前线程被其它线程唤醒并获取锁成功 // 且 head 节点完成出队}
这里以非公平锁的源码进行分析
c = getState()
false 则去判断是否重入即当前线程是否是锁的持有者
pred != null
s = node.next
准备阻塞 thread1
thread = nullnextWaiter = nullwaitStatus = 0
AbstractQueuedSynchronizer#acquire
同步等待队列已经初始化需要将当前线程对应的 Node 添加到末尾
old tail <--prev-- node
当前线程对应的已入队成功的 Node
该方法会清除中断标记
先为当前线程创建一个 Node 节点
p == head
prev 节点的waitStatus 属性值为 0 或 -3
setState(nextc)
thread2 的prev Node(thread1 节点)的 waitStatus 更新为 -1
收藏
0 条评论
下一页