ReentrantLock
ReentrantLock,是JUC提供的重入锁,底层依赖AQS(AbstractQueuedSynchronizer,一种提供了原子式管理同步状态、阻塞及唤醒、同步队列模型的框架)
重入锁是指,已获取锁的线程每次进入同步代码块不必反复获取锁,获取一次即可
区别
通常,提到ReentrantLock就会想起另一把锁,JVM级别的内置锁synchronized。两者有三点明显区别:
- 锁机制,前者依赖于AQS后者则监视器
- 灵活性,前者支持响应中断、超时退出、尝试获取锁,后者不支持
- 锁类型,前者既可是公平锁又可是非公平锁,后者非公平锁
公平锁与非公平锁
ReentrantLock源码中,公平锁FairSync与非公平锁NonfairSync都是继承了Sync。
而Sync继承了AQS,然后在tryAquire()上区别于,公平锁获取锁之前进行了判断hasQueuedPredecessors(),当前线程是否位于同步队列中的第一个。
lock()及unlock()源码分析
image.png公平锁与非公平锁的实现相似,这里主要分析非公平锁的获取锁、释放锁。lock()主要步骤分为四步:
- tryAcquire()尝试获取锁,如果成功则true,否false。AQS中的tryAcquire()没有实现,用模板设计模式下放到NonfairSync去实现。
- 如果没有获取成功,addWaiter(Node.EXCLUSIVE)以独占模式进入同步队列。如果tail为空,则initializeSyncQueue初始化同步队列(head,tail分别指向一个空node)。如果不为空,则通过CAS将其放到同步队列的尾部。
- acquireQueued()已经入同步队列的等待节点以不可中断的模式一直尝试获取锁或是阻塞线程以等待,直到获取到为止
- 如果获取锁过程中,有中断那么方法内的interrupted变量会被标记为true,最后slefInterrupt()自我中断。
tryAcquire()源码分析
image.png上面说到tryAcquire()用模板设计模式下放了具体实现到ReentrantLock.Sync#nonfairTryAcquire,如上图。主要步骤有三个:
- getState()获取AQS中的同步状态,在ReentrantLock中0代表未锁定,否代表已被获取锁,是指重入的次数
- state为0时,尝试通过CAS将state置为1,如果成功则将锁的所属线程标记为当前线程
- state不为0时,判断是否当前线程获取了锁,如果是,则将acquries=1加到state上表示重入次数
addWaiter(Node.EXCLUSIVE)源码分析
addWaiter()将等待的线程包装成一个Node放进同步队列(FIFO队列,是AQS中内置的双向队列,用于保存获取资源的排队顺序)。主要步骤分为两步:
- 如果tail!=null则将新结点入队到尾部
- 否则进行初始化同步队列
以下是Node类,同步队列中的结点的一些成员变量:
image.pngacquireQueued(final Node node, long args)源码分析
该方法中有个for(;;)循环获取到,直到锁之后才会返回是否中断的标志位。这里步骤主要分为三步:
- 获取node结点的pre结点,如果pre结点是header,那么尝试获取锁,成功后将header设置为当前node
- 获取锁失败后,shouldParkAfterFailedAcquire()判断是否需要阻塞线程,稍后分析这个方法
- 如果需要阻塞该方法,那么调用parkAndCheckInterrupt()阻塞该线程,里面的实现就是LockSupport.park(this),并将该线程是否中断的标志位传递给interrupted变量。
shouldParkAfterFailedAcquire()源码分析
获取锁失败后判断是否需阻塞该线程是根据node中的waitStatus,所以我们先了解下有哪几种状态。
image.png这里根据pre前驱结点的waitStaus进行判断,主要有三种情况:
- 如果处于signal状态,表示前驱结点的线程准备好了等资源,那么阻塞当前线程
- 大于0的只有锁请求已取消状态cancelled,则将pre前驱结点去掉,然后往前进行判断
- 最后如果是未初始化或者propagate,那么则将pred设置signal状态
unlock()源码分析
同样的,unlock()实现与lock()类似,release(int arg)是AbstractQueuedSynchronize抽象类中的公共方法,tryRelease()同样是下放到具体实现类中去实现,这里主要分为三个步骤:
- tryRelease()释放锁,将同步状态state置为0、拥有锁的线程标记置为null
- unparkSuccessor()从同步队列中唤醒下一个等待锁的线程
unparkSuccessor()源码分析
这里主要分为两个步骤:
- 清除node的状态。
-
如果node的next结点为空或者是状态为取消状态,那么从后往前遍历直到找到没有取消或者未初始化的结点,然后唤醒该结点。
image.png
网友评论