并发知识-- ReentrantLock
本文最后更新于:4 个月前
⚡ReentrantLock 初步设计思想
new 一个 ReentrantLock 之后,调用 lock 方法,改变锁的状态码,从0变为1,主线程获取到锁,然后继续执行
如果此时同一个线程多次调用则进行多次重入,状态码增加
如果是线程交替执行,即并发度不是很高,那么就不存在锁的竞争,可以依次获取锁,然后依次释放,循环往复,保证高性能高可用
但是如果并发度很大,那么就一定存在线程之前对锁的竞争,这时,如果是公平锁,那么先获取锁的线程先处理,后获取锁的排在一个等待队列,然后这个线程就 park
暂停,然后等待锁释放和通知,unpark
后,再去获取锁
(注意:此处的 park 和 unpark 只是一种抽象概念,不一定指 LockSupport 里面的方法,也可以是 interrupt 中断)
但是,这里虽然想的很容易理解,但是要实现却不是那么容易,且看我如下分析
⚡ReentrantLock 类数据结构
⚡ReentrantLock 抽象内部类 Sync 锁
图 ReentrantLockSyncAndExtend:
图 ReentrantLockSyncAndExtend1:
从源码和上图可知, Sync 只有第一个 lock()
方法是抽象的,有一个默认的非公平的 nonFairTryAcquired(int)
方法,这个方法是尝试获取锁的,并且是一种非公平的方式
⚡Sync 的公平锁具体实现
图 AQSAbstractTryAcquire:
一个 lock 方法,一个 tryAcquire 方法
lock 方法是 Sync 的具体实现,而 tryAcquire 则是重写了 Sync 的父类 AQS 的方法
图 ReentrantLockFairSync1
⚡ReentrantLock 加锁过程 (公平锁)
⚡第一次加锁
-
new 是默认非公平锁(这里分析公平锁)
sync = new FairSync();
-
当程序中调用
reentrantLock.lock()
方法时lock() 方法就会调用
sync.lock()
方法(sync 锁具体实现 lock 方法的 非公平的锁和公平锁不同)
由上面两张图可知,在
lock()
方法里面直接调用的是 AQS 的acquire(int)
方法, 并且参数为1 -
那我们再去看 AQS
我们可以看到首先会调用
tryAcquire(int)
方法,这也是 AQS 中的一个方法,我们可以根据名称猜测这个方法是用来尝试获取锁的点进去,发现:
仅仅抛一个异常?!
这当然不可能,因为这样没有任何意义
再按住
ctrl
和alt
,鼠标点击tryAcquire
,出现如下具体实现:我们点击第一个,发现回到了 ReentrantLock 里面的 FairSync 静态内部类的
tryAcquire(int)
方法, 见图 ReentrantLockFairSync1这说明,内部的公平锁自己重写了 AQS 的方法但是没有加
@Override
注释! -
再回到 ReentrantLock 看这个公平锁的
tryAcquired(int)
方法(和图ReentrantLockFairSync1 一样,只是把代码贴出来了)具体分析见注释:
@Override protected final boolean tryAcquire(int acquires) { // 拿到当前线程 final Thread current = Thread.currentThread(); // 拿到 AQS 的内部 volatile 变量 state int c = getState(); // 如果 state =0 说明此时没有线程拿到锁 if (c == 0) { // 第一个 hasQueuedPredecessors() 是 AQS 的内部方法,是用来判断自己需不需要排队 // 返回 false 在表示队列不存在或者队列中没有前驱(即队列中没有已经拿到锁的线程) // 那么这个线程就不需要排队 // 为什么需要排队呢,最前面将思想的时候其实已经说了: // 想想如果存在多线程竞争,而且又是公平锁,那么就需要排队等待,直到前面那个线程处理完了,后面的线程才能拿到锁,再去处理 // todo 这里理解排队要看之后的线程竞争或者其他特殊情况,我们这里假设的是第一个线程来拿锁,暂时不考虑,因为从开始到现在根本没有初始化队列(new FairSync 也没有初始化 Sync 父类的 AQS 队列) if (!hasQueuedPredecessors() && // 下面调用 AQS 的 CAS 方法,原子操作,改变的是 AQS 内部的 state 变量 // CAS 尝试加锁 compareAndSetState(0, acquires)) { // 如果前面的条件成立,说明: 1. 自己不需要排队 2. 自己成功拿到锁了 // 此时就可以将锁设置为当前线程 // 注意,这个方法是 `AQS` 继承 `AOS` 这个抽象类自己内部的方法,表示是一个排他锁 // 关于 AOS 具体看下面相应的地方,这里就不多做解释,现在就认为是一个排他锁 setExclusiveOwnerThread(current); // 返回 true,表示成功加上了排他锁 // 第一个线程到这里返回 return true; } // 又有一个线程来了,如果锁状态不为0,说明已经加锁,并且该线程就是之前加锁的第一个线程,此时会重入,state +1,并返回 true,代表重入锁加锁成功 } else if (current == getExclusiveOwnerThread()) { // 可能在这里会想,c 是最前面 getState() 方法得到的,为什么这里不用 CAS 来保证原子操作呢? // 因为其它的线程如果来了,会首先排队,不会改变第一个线程的状态值 // 但是如果第一个线程此时刚好释放所了,岂不是重入错了? // 这里就要看上面的 else-if 判断了,如果是同一线程重入,然后释放,然后再加锁是不可能的,一个线程怎么可能同时加锁和解锁呢! // 上面是目前我的分析,如果加上锁自动释放(超时释放)的功能 tryLock 可能会有问题? // 解答: tryLock 是在指定时间内获取锁,而不是在指定时间内释放拿到的锁,拿到锁释放必须由这个线程显式调用 unLock 方法,所以根本不会存在自动释放锁这个逻辑 // 超时释放指的是超过时间没有拿到就放弃抢锁,不是超时了就释放锁 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } // 如果锁状态不为0,并且是另一个线程来加锁,会返回 false return false; }
关于
hasQueuedPredecessors()
方法,放到 AQS 章节再讲,这些 AQS 内部方法也有很多需要分析 -
方法
tryAcquire(int)
返回 true,回到前面 AQS 的 acquire此时 if 条件不成立,直接返回了,然后递归返回,就没有什么,结束程序中的
reentrantLock.lock()
方法,加锁成功,主程序继续执行
⚡如果此时第一个线程又来加锁(重入)
此时在 ReentrantLock
中的 FairSync
的 tryAcquire(int)
方法里面,进入 else 判断,请看代码注释 else-if 部分
tryAcquire(int)
返回 true, 然后也是和第一次拿到锁一样,调用递归返回,主程序继续执行
不过需要注意,FairSync
公平锁的父类 AQS 的内部状态 volatile 变量 state 每次都要加一,而每次释放锁的时候,state 才会减一,最后一定要变为0才释放了所有锁
⚡如果是第二个线程来了,也开始加锁,并且此时第一个线程还没有释放锁
-
同上面两种情况,会调用
sync.lock()
方法,然后acquire(1)
,再然后tryAcquire(1)
但是此时 if 和 else 条件都不满足,会返回 false
-
此时AQS 的
acquire(1)
方法中 if 的第一个tryAcquire(1)
取反,值为 true,进入acquiredQueue(addWaiter(Node.Exclusive,1))
两个方法这两个方法就不在这里分析了,因为这是属于 AQS 内部实现,而且很复杂,需要单独说
我们现在就只需要知道,这里第二个线程就会去在 AQS 内部排队等待
再然后,会调用
selfInterrupt()
方法,这个 AQS 方法比较简单,就把代码截出来了把当前线程中断,来让出 CPU 等待 AQS 的唤醒
⚡ReentrantLock 加锁过程 (非公平锁)
如图 ReentrantLockNonFairSync:
⚡第一次加锁
-
有源码和上图知,调用
ReentrantLock.lock()
方法时,会调用上图中非公平锁 NonFairSync 的方法-
首先会 CAS 直接尝试抢占锁,如果抢到了就直接上锁,程序正常返回
就是比公平锁少了一个判断方法
!hasQueuedPredecessors()
来判断需不需要排队非公平锁当然不需要排队啦~
-
如果没有没有直接抢到,那么其实还是要排队,调用 AQS 的
acquire()
方法注意,这个时候就不要认为之后的逻辑和公平锁一模一样,因为锁在继承
AQS
的实现的时候,都重写了AQS
的tryAcquire()
方法对于非公平锁来说,它默认使用的是
ReentrantLock
类中Sync
父类里面的nonFairTryAcquire()
方法-
nonFairTryAcquire()
和公平锁的实现没有太大差别,仅仅只是少了
hasQueuedPredecessor()
这个重要的查看队列的方法,会上来就去尝试获取锁,而不考虑是否存在等待队列
-
-
⚡重入情况则和公平锁一致
⚡第二个线程来加锁了
假设存在线程竞争,那么根据 图,和公平锁一样,还是会进入上图的 nonFairTryAcquire()
,然后返回 false,然后调用 AQS 的 addWaiter()
和 acquireQueued()
加入到等待队列中,selfInterrupt()
方法自我中断
⚡ReentrantLock 解锁过程
⚡线程调用了 unLock
然而这个 release(1)
方法又是 AQS 内部的方法:
从上面可以看到,这里首先调用了一个 tryRelease(1)
方法
-
tryRelease()
方法这个是
ReentrantLock
内部Sync
自己实现的默认方法,并且公平锁和非公平锁调用一致由于锁可以重入,所以这里
getState()
获取到的状态值可能大于1,所以此处要减去参数 release (=1) 拿到预期值然后进行判断,如果不是锁持有的线程拿到锁会抛出异常(一般不会出现)
如果上面预期的状态值等于0,则说明锁可以释放,此时就把共享锁设为空;反之,不进行该操作
(注意:锁为空并不代表锁已经释放,因为锁的状态是由 state 字段标识的,此时并没有改锁的状态)
然后改变锁的状态
setState()
,并且返回释放锁的结果想想这里改变状态为什么不用 CAS 操作?原因其实上面提到一些
因为此时线程又不可能重入(这个线程正在释放,怎么可能会重入),如果是别的线程,在
setState()
操作之前,锁还没有释放又不可能来抢锁,所以这里就可以安安心心的进行锁的状态改变了 -
tryRelease() 方法返回了,之后就是 AQS 的内部实现和状态判断了
老规律还是把这个实现留在讲 AQS 的文章里面集中分析吧
我们只需要知道此时锁已经释放,并且把 AQS 的内部状态 state 重置成 0,并且锁头已经置为空就行了
⚡写在后面
AQS 和 ReentrantLock 代码,首先看子路老师的视频和马士兵的视频,然后对着源码一点点分析,做笔记,再结合博客看
自己再把 AQS 和 ReentrantLock 的类图先理清楚,有哪些内部类,哪些方法,哪些是抽象的,哪些是具体实现,分别做了什么,一层层理清楚
这篇文章仅仅分析了 ReentrantLock, 很多更重要的代码其实在 AQS 里面,具体分析就暂时等待 AQS 的文章吧
要不然,ReentrantLock 用到了 AQS 的一部分 api,我们就去看 AQS, 看了 AQS 又不知道像 tryAcquire()
和 acquire()
到底分别在什么情况下使用,又要回到 ReentrantLock 去看 api 的应用,让人云里雾里,非常头疼
其实也没有分析很多,这么一想其实 ReentrantLock 还挺简单的
本博客所有文章除特别声明外,均采用 知识共享署名-非商业性使用-相同方式共享 4.0 国际许可协议 。转载请注明出处!