先画个大致的假类图
主要的类都在这里,核心就是ReentrantLock的内部类 Sync,
FairSync NonfairSync 是Sync的公平锁 非公平锁的实现
Sync继承于AbstractQueueSynchronizer(AQS) 核心功能也都在这 先来分析AQS
AQS的核心思想就是: 一个队列(实质是一个链表结构) + 一个状态 / a Queue and a state
大致思路 :
state代表当前锁的状态 默认为0,当该锁被某个线程持有则state 为1,如果是重入锁 每重入一次state+1
当线程尝试获取锁失败就将线程自己封装成Node节点加入到队列,然后挂起等待被唤醒,再去竞争锁
完结 !
源码分析:
竞争锁的队列该有的样子
AQS的几个核心属性 : head, tail, state, exclusiveOwnerThread
head 表示队列的头节点 (头节点不属于阻塞队列!头节点不代表实际的线程)
state 表示当前锁的状态 0 代表无人占有 1 代表被线程占用,每+1 代表被重入一次
tail 表示队列的尾节点
AQS继承于AbstractOwnableSynchronizer
继承了重要属性 exclusiveOwnerThread 表示当前占有锁的线程
AQS内部类 Node
几个重要的属性:
next 后继节点,
perv 先驱节点,
thread 该节点代表的线程,
waitStatus 表示后继节点是否需要唤醒 (由后继节点控制,默认是0 不需要唤醒,当后继节点需要被前置节点唤醒则将0设置为-1,如果前置节点主动放弃竞争锁 则将waitStatus > 0)
EXCLUSIVE 标识节点会独占锁 (目前看来用在ReentratLock上抢锁)
SHARED 标识节点会共享锁 (目前看用在CountDownLautch抢锁)
从客户端调用入手分析
加锁过程
public static void main(String[] args) {
Lock lock = new ReentrantLock();
lock.lock();
try {
//do something
}finally {
lock.unlock();
}
}
1.初始化 无参默认是非公平锁
2.lock.lock(); 非公平锁的实现
先试探CAS去改锁的状态,如果成功则加锁成功! 将持有锁的线程设置为自己 over!
如果CAS失败则 执行acquire(1), 参数传1
3. acquire 尝试获取锁失败,并且加入到阻塞队列还被中断了,自己重新标记一下中断位
4. 重点:tryAcquire(1) 尝试去获取锁 传的参数是1
非公平锁的tryAcquire()实现,
先判断state是否为0 (当前锁是否没被持有)
if (state == 0){
if (尝试CAS直接去改变状态 因为此时可能会有多个线程同时发现了state是0 都想去改变状态){
如果CAS成功了就将持有锁的线程设置为自己 over!
}
}else if (当前线程就是当前持有锁的线程,说明这是锁重入 ){
直接state+1
}
尝试获取锁失败了 return false;
5. boolean acquireQueued() 中断当前线程是否成功 , 传入封装好的节点 和参数1
try {
for(;;){
获取当前节点的前直接点 p
if (如果p是head节点 并且 再次尝试获取锁成功了){
就把当前节点设置为head节点
下个节点置空
return 中断当前线程失败
}
if ( 再次抢占锁失败后是否需要挂起 && 检查并挂起线程){
中断成功
}
}
} finally {
//failed == true的情况 再次尝试获取锁抛异常导致
if (如果中断失败了){
解散节点 不玩了
}
}
6. addWaiter() 尝试加入到阻塞队列 传入的mode标识当前节点是想 独占锁 还是想共享锁
将当前线程封装成一个Node
获取到尾节点 pred
if (如果尾节点不为null){
将perd作为当前节点的前置节点
if (CAS将当前节点设置为队列的尾节点){
return 当前节点;
}
}
说明尾节点是null,就说明没有head节点,
for循环{
判断尾节点是否为空,如果为空则CAS 将自己设置为head节点 兼 尾节点,
如果中途判断发现尾节点不为空了(可能是被别人抢先占了头节点了),就默默的加入到尾节点后面
}
解锁过程 unlock()
1.
unlock() 是 Sync的方法
2. release() 传入参数1
if (尝试去释放锁成功) {
获取头节点
if (头节点不为null && 头节点的下一个节点需要被唤醒) {
唤醒
}
}
tryRelease() 传入1,很简单 尝试释放锁
修改state - 1
如果state减为0 自由了 将持有锁的线程置空
如果是state降为0 return释放锁成功,否则重入锁-1 return false
3. 唤醒节点 传入头节点
if (头节点的下一个节点需要被唤醒) {
CAS修改waitstatus 为0 表示不需要再换醒了
}
获取头节点的下一个节点 s
if (下一个节点是空,或者下一个节点已经退出竞争锁了) {
将节点s 置空
for () {
for循环再去找 直到找到下一个节点符合的
}
}
最终 s 不为空 就将s的线程唤醒
最终通过unpark 唤醒线程
公平锁与非公平锁区别
- 非公平锁在调用 lock 后,首先就会调用 CAS 进行一次抢锁,如果这个时候恰巧锁没有被占用,那么直接就获取到锁返回了。
- 非公平锁在 CAS 失败后,和公平锁一样都会进入到 tryAcquire 方法,在 tryAcquire 方法中,如果发现锁这个时候被释放了(state == 0),非公平锁会直接 CAS 抢锁,但是公平锁会判断等待队列是否有线程处于等待状态,如果有则不去抢锁,乖乖排到后面。
公平锁和非公平锁就这两点区别,如果这两次 CAS 都不成功,那么后面非公平锁和公平锁是一样的,都要进入到阻塞队列等待唤醒。
相对来说,非公平锁会有更好的性能,因为它的吞吐量比较大。当然,非公平锁让获取锁的时间变得更加不确定,可能会导致在阻塞队列中的线程长期处于饥饿状态。