JDK源码系列——ReentrantLock源码解析


前言

ReentrantLock是一个可重入锁,其底层是基于AQS来实现的,AQS(AbstractQueuedSynchronizer),它是java几乎所有锁和同步器底层的一个基石框架,AQS是基于FIFO的队列实现,内部维护了一个状态变量state,通过CAS更新这个状态变量来实现枷锁解锁等操作。

1、主要内部类及属性分析

1.1、内部类

这里定义了三个内部类

  1. Sync作为一个抽象类,他继承自AbstractQueuedSynchronizer,实现了部分AQS方法
  2. NonfairSync继承自Sync,主要用于实现非公平锁
  3. FairSync实现了Sync,主要用于实现公平锁

在这里插入图片描述

1.2、属性

这里只有一个属性sync,他在构造方法中初始化,用于确定是NonfairSync还是FairSync,实现公平锁还是非公平锁。

在这里插入图片描述

2、构造方法解析

  1. 默认构造方法使用的是非公平锁
  2. 有参构造方法可以根据用户的传入来确定使用公平锁或非公平锁

在这里插入图片描述

3、常用方法解析

3.1、Lock()加锁方法

这里直接委托给sync,会调用Sync#lock方法,,这种就有两种实现NonfairSync#lock和FairSync#lock

在这里插入图片描述

3.1.1、FairSync#lock(公平锁加锁)

这里会调用AQS的acquire()方法获取锁,这里传的值是1

在这里插入图片描述

3.1.1.1、AbstractQueuedSynchronizer#acquire
  1. 首先调用tryAcquire尝试去获取锁
  2. 获取锁失败,把当先线程包装成一个Node节点,然后去队列排队

在这里插入图片描述

3.1.1.1、FairSync#tryAcquire(这里tryAcquire是抽象方法,因为我们这里是公平锁,所以到FairSync)
  1. 获取到当前线程和state变量
  2. 判断state变量是否为0,如果为0说明现在没有线程持有锁,那么调用hasQueuedPredecessors方法判断有没有其他线程在排队,如果没有那么当前线程尝试更新state的值为1,如果成功了,则说明当前线程获取到锁了,那么把当前线程设置到exclusiveOwnerThread变量中,标识当前占有锁的线程。
  3. 如果state变量不为0,则判断exclusiveOwnerThread变量的线程是否是当前线程,如果是当前线程则说明是可重入锁的情况,则让state+1,这里外部传进来的是1。
  4. 如果上面条件都没命中,则返回尝试加锁失败。

在这里插入图片描述

3.1.1.1.1 、hasQueuedPredecessors

判断当前是否没有排队的线程或者自己是当前队列的第一个,判断有没有抢锁的权利。

在这里插入图片描述

3.1.1.2、addWaiter(Node.EXCLUSIVE), arg)

如果上面尝试加锁失败,则会调用这个方法步骤如下:

  1. 把当前线程包装成一个Node节点
  2. 尝试把新节点加入到队列尾部,这里使用CAS更新防止并发冲突
  3. 如果尝试加入尾节点不成功,那么会调用enq方法(这里会循环直到加入队列)

在这里插入图片描述

3.1.1.2.1、enq(node)

这里会不断自旋,尝试把当前节点插入尾节点。

在这里插入图片描述

3.1.1.3、acquireQueued

上面已经调用了addWaiter()方法把新节点加入队列了,这个方法是尝试让当前节点去获取锁的,流程如下

  1. 这里会阻塞在一个死循环中,循环体中会先判断当前的前一个节点是否为head,如果是head节点可能是head节点释放锁唤醒后续节点的操作,那么进行抢占锁,这里有可能获取不到,因为后续进来的也会直接尝试加锁
  2. 如果获取到锁了,那么会把head设置为自己,将自己的next设置为null方便gc然后返回。
  3. 如果没有获取到锁,那么会调用shouldParkAfterFailedAcquire判断是否应该阻塞自己,如果可以,那么会调用parkAndCheckInterrupt阻塞自己
  4. 这里如果线程被阻塞了,因为已经加入队列了,那么前面的释放锁会唤醒后续节点,那么这个时候从parkAndCheckInterrupt出来,回到循环开头尝试去加锁,如果获取不到锁再进行阻塞。

在这里插入图片描述

3.1.1.3.1、shouldParkAfterFailedAcquire

这里判断节点是否可以阻塞,是根据前一个节点的状态,如果前驱节点的状态是SIGNAL,表示他在释放锁的时候会唤醒后续节点,那么当前节点可以被唤醒,那么可以进行阻塞,反之不应该阻塞。

在这里插入图片描述

3.1.1.3.2、parkAndCheckInterrupt

这里会调用LockSupport.park阻塞当前线程,这里就等待前驱节点释放锁把它唤醒,然后继续去抢占锁。

在这里插入图片描述

3.1.1、NonfairSync#lock(非公平锁加锁)

这里和公平锁的区别就是:

  1. 一开始就尝试使用CAS更新state状态的值,如果修改成功了,那说明加锁成功了,那么修改当前占有锁的线程
  2. 这里就是非公平锁,我不管队列中有没有等待的线程,我不直接去排队,我首先尝试去获取锁,获取不到锁再去队列排队。
  3. 如果获取不到锁那么会调用acquire(1)尝试去队列排队,这个是和上面公平锁加锁的流程一样。

在这里插入图片描述

3.2、tryLock()加锁方法

这里尝试获取一次锁,成功了就返回true,没成功就返回false,不会继续尝试。

在这里插入图片描述

3.2.1、Sync#nonfairTryAcquire

这里和非公平锁的tryAcquire一致,产生加锁。

在这里插入图片描述

3.3、tryLock(long timeout, TimeUnit unit)带超时时间加锁方法

尝试获取锁,并等待一段时间,如果在这段时间内都没有获取到锁,就返回false

在这里插入图片描述

3.3.1、AbstractQueuedSynchronizer#tryAcquireNanos

这里首先会调用tryAcquire方法尝试获取锁,之前我们已经剖析过了,如果尝试没有加到锁,这里会调用doAcquireNanos方法

在这里插入图片描述

3.3.2、AbstractQueuedSynchronizer#doAcquireNanos

这里流程和上面剖析的差不多,只不过在park的时候加了过期时间,过期时间到了并且也没获取到锁,那么返回失败。

在这里插入图片描述

3.3、unlock()释放锁方法

这里会直接委托给sync的release方法,会调用到AbstractQueuedSynchronizer#release方法

在这里插入图片描述

3.3.1、AbstractQueuedSynchronizer#release

  1. 首先会调用tryRelease方法,这里是模版,会调用到具体实现的sync类,因为这里只有一个线程持有锁,所以不涉及到并发
  2. 释放锁成功,那么会唤醒后继节点
    在这里插入图片描述
3.3.1.1、Sync#tryRelease

这里释放锁就是对state状态做–,如果–后state变成0,则说明没有不是可重入锁,那么把记录的线程信息也给清除了。

在这里插入图片描述

3.3.1.2、AbstractQueuedSynchronizer#unparkSuccessor

这里就是找到下一个满足条件可以被唤醒的节点,然后调用unpark方法唤醒后继节点。

在这里插入图片描述

4、总结

为什么ReentrantLock默认采用的是非公平状态状态?
因为非公平模式效率比较高,它一开始就尝试去获取锁,如果当前并发度不高的话,那么直接就可以获取到锁,减少了排队导致线程的阻塞,用户态到内存态切换的开销,非公平锁也有可能会导致一开始排队的线程一直获取不到锁,导致线程饥饿,不过,这种情况不常发生。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值