深入浅出ReentrantLock(可重入锁)

本文详细分析了ReentrantLock的使用,重点讲解了非公平锁的lock()和unlock()方法,涉及Lock接口、公平锁与非公平锁的区别,以及基于CLH队列的锁竞争过程。
摘要由CSDN通过智能技术生成

lock.lock();

try{

//更新对象状态

//捕获异常,并在必须时恢复不变性条件

}catch (Exception e){

e.printStackTrace();

} finally {

lock.unlock();

}

上述代码中是使用Lock接口的标准使用方式,这种形式比使用内置锁(synchronized )复杂一些,必须要在 finally 块中释放锁,否则,如果在被保护的代码中抛出了异常,那么这个锁永远都无法释放。

四、ReentrantLock 源码分析


在简介中我们知道 ReentrantLock继承自 Lock接口,Lock提供了一些获取锁和释放锁的方法,以及条件判断的获取的方法,通过实现它来进行锁的控制,因为它是显示锁,所以需要显示指定起始位置和终止位置,下面就来介绍一下Lock接口的方法介绍:


| 方法名称 | 方法描述 |

| — | — |

| lock | 用来获取锁,如果锁已被其他线程获取,则进行等待 |

| tryLock | 表示用来尝试获取锁,如果获取成功,则返回true,如果获取失败(即锁已被其他线程获取),则返回false,也就说这个方法无论如何都会立即返回。在拿不到锁时不会一直在那等待 |

| tryLock(long time, TimeUnit unit) | 和tryLock()类似,区别在于它在拿不到锁时会等待一定的时间,在时间期限之内如果还拿不到锁,就返回false。如果如果一开始拿到锁或者在等待期间内拿到了锁,则返回true |

| lockInterruptibly | 获取锁,如果获取锁失败则进行等到,如果等待的线程被中断会相应中断信息 |

| unlock | 释放锁的操作 |

| newCondition | 获取Condition对象,该组件和当前的锁绑定,当前线程只有获得了锁,才能调用该组件wait()方法,而调用后,当前线程释放锁 |

ReentrantLock 也实现了上面接口的内容,同时 ReentrantLock 提供了 公平锁非公平锁两种模式,如果没有特别的去指定使用何种方式,那么 ReentrantLock 会默认为 非公平锁,首先我们来看一下 ReentrantLock 的构造函数:

/**

  • 无参的构造函数

*/

public ReentrantLock() {

sync = new NonfairSync();

}

/**

  • 有参构造函数

  • 参数为布尔类型

*/

public ReentrantLock(boolean fair) {

sync = fair ? new FairSync() : new NonfairSync();

}

从上述源码中我们可以看到:

  • ReentrantLock 优先使用的是无参构造函数,也就是非公平锁,但是当我们调用有参构造函数时,可以指定使用哪种锁来进行操作(公平锁还是非公平锁),参数为布尔类型,如果指定为 false 的话代表 非公平锁 ,如果指定为 true 的话代表的是 公平锁

  • Sync 类 是 ReentrantLock 自定义的同步组件,它是 ReentrantLock 里面的一个内部类,它继承自AQS(AbstractQueuedSynchronizer),Sync 有两个子类:公平锁 FairSync 和 非公平锁 NonfairSync

  • ReentrantLock 的获取与释放锁操作都是委托给该同步组件来实现的。下面我们来看一看非公平锁的 lock() 方法:

4.1 非公平锁 NonfairSync.lock()

1、NonfairSync.lock() 方法流程图

在这里插入图片描述

2、lock方法详解

  1. 在初始化 ReentrantLock 的时候,如果我们不传参,使用默认的构造函数,那么默认使用非公平锁,也就是 NonfairSync

  2. 当我们调用 ReentrantLocklock() 方法的时候,实际上是调用了 NonfairSynclock() 方法,代码如下:

static final class NonfairSync extends Sync {

private static final long serialVersionUID = 7316153563782823691L;

/**

  • Performs lock. Try immediate barge, backing up to normal

  • acquire on failure.

*/

final void lock() {

//这个方法先用CAS操作,去尝试抢占该锁

// 快速尝试将state从0设置成1,如果state=0代表当前没有任何一个线程获得了锁

if (compareAndSetState(0, 1))

//state设置成1代表获得锁成功

//如果成功,就把当前线程设置在这个锁上,表示抢占成功,在重入锁的时候需要

setExclusiveOwnerThread(Thread.currentThread());

else

//如果失败,则调用 AbstractQueuedSynchronizer.acquire() 模板方法,等待抢占。

acquire(1);

}

protected final boolean tryAcquire(int acquires) {

return nonfairTryAcquire(acquires);

}

}

  1. 调用 acquire(1) 实际上使用的是 AbstractQueuedSynchronizeracquire() 方法,它是一套锁抢占的模板,acquire() 代码比较简单:

public final void acquire(int arg) {

//先去尝试获取锁,如果没有获取成功,就在CLH队列中增加一个当前线程的节点,表示等待抢占。

//然后进入CLH队列的抢占模式,进入的时候也会去执行一次获取锁的操作,如果还是获取不到,

//就调用LockSupport.park() 将当前线程挂起。那么当前线程什么时候会被唤醒呢?当

//持有锁的那个线程调用 unlock() 的时候,会将CLH队列的头节点的下一个节点上的线程

//唤醒,调用的是 LockSupport.unpark() 方法。

if (!tryAcquire(arg) &&

acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

selfInterrupt();

}

  • acquire() 会先调用 tryAcquire() 这个钩子方法去尝试获取锁,这个方法就是在 NonfairSync.tryAcquire()下的 nonfairTryAcquire(),源码如下:

//一个尝试插队的过程

final boolean nonfairTryAcquire(int acquires) {

final Thread current = Thread.currentThread();

//获取state值

int c = getState();

//比较锁的状态是否为 0,如果是0,当前没有任何一个线程获取锁

if (c == 0) {

//则尝试去原子抢占这个锁(设置状态为1,然后把当前线程设置成独占线程)

if (compareAndSetState(0, acquires)) {

// 设置成功标识独占锁

setExclusiveOwnerThread(current);

return true;

}

}

//如果当前锁的状态不是0 state!=0,就去比较当前线程和占用锁的线程是不是一个线程

else if (current == getExclusiveOwnerThread()) {

//如果是,增加状态变量的值,从这里看出可重入锁之所以可重入,就是同一个线程可以反复使用它占用的锁

int nextc = c + acquires;

//重入次数太多,大过Integer.MAX

if (nextc < 0) // overflow

throw new Error(“Maximum lock count exceeded”);

setState(nextc);

return true;

}

//如果以上两种情况都不通过,则返回失败false

return false;

}

  • tryAcquire() 一旦返回 false,就会则进入 acquireQueued() 流程,也就是基于CLH队列的抢占模式,在CLH锁队列尾部增加一个等待节点,这个节点保存了当前线程,通过调用 addWaiter() 实现,这里需要考虑初始化的情况,在第一个等待节点进入的时候,需要初始化一个头节点然后把当前节点加入到尾部,后续则直接在尾部加入节点。

代码如下:

//AbstractQueuedSynchronizer.addWaiter()

private Node addWaiter(Node mode) {

// 初始化一个节点,用于保存当前线程

Node node = new Node(Thread.currentThread(), mode);

// 当CLH队列不为空的视乎,直接在队列尾部插入一个节点

Node pred = tail;

if (pred != null) {

node.prev = pred;

//如果pred还是尾部(即没有被其他线程更新),则将尾部更新为node节点(即当前线程快速设置成了队尾)

if (compareAndSetTail(pred, node)) {

pred.next = node;

return node;

}

}

// 当CLH队列为空的时候,调用enq方法初始化队列

enq(node);

return node;

}

private Node enq(final Node node) {

//在一个循环里不停的尝试将node节点插入到队尾里

for (;😉 {

Node t = tail;

if (t == null) { // 初始化节点,头尾都指向一个空节点

if (compareAndSetHead(new Node()))

tail = head;

} else {

node.prev = t;

if (compareAndSetTail(t, node)) {

t.next = node;

return t;

}

}

}

}

  • 将节点增加到CLH队列后,进入 acquireQueued() 方法

final boolean acquireQueued(final Node node, int arg) {

boolean failed = true;

try {

boolean interrupted = false;

//在一个循环里不断等待前驱节点执行完毕

for (;😉 {

final Node p = node.predecessor();

if (p == head && tryAcquire(arg)) {// 通过tryAcquire获得锁,如果获取到锁,说明头节点已经释放了锁

setHead(node);//将当前节点设置成头节点

p.next = null; // help GC//将上一个节点的next变量被设置为null,在下次GC的时候会清理掉

failed = false;//将failed标记设置成false

return interrupted;

}

//中断

if (shouldParkAfterFailedAcquire(p, node) && // 是否需要阻塞

parkAndCheckInterrupt())// 阻塞,返回线程是否被中断

interrupted = true;

}

} finally {

if (failed)

cancelAcquire(node);

}

}

  • 如果尝试获取锁失败,就会进入 shouldParkAfterFailedAcquire() 方法,会判断当前线程是否阻塞

/**

  • 确保当前结点的前驱结点的状态为SIGNAL

  • SIGNAL意味着线程释放锁后会唤醒后面阻塞的线程

  • 只有确保能够被唤醒,当前线程才能放心的阻塞。

*/

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {

int ws = pred.waitStatus;

if (ws == Node.SIGNAL)

//如果前驱节点状态为SIGNAL

//表明当前线程需要阻塞,因为前置节点承诺执行完之后会通知唤醒当前节点

return true;

if (ws > 0) {//ws > 0 代表前驱节点取消了

do {

node.prev = pred = pred.prev;//不断的把前驱取消了的节点移除队列

} while (pred.waitStatus > 0);

pred.next = node;

} else {

//初始化状态,将前驱节点的状态设置成SIGNAL

compareAndSetWaitStatus(pred, ws, Node.SIGNAL);

}

return false;

}

  • 当进入阻塞阶段,会进入 parkAndCheckInterrupt() 方法,则会调用 LockSupport.park(this) 将当前线程挂起。代码如下:

// 从方法名可以看出这个方法做了两件事

private final boolean parkAndCheckInterrupt() {

LockSupport.park(this);//挂起当前的线程

// 如果当前线程已经被中断了,返回true,否则返回false

// 有可能在挂起阶段被中断了

return Thread.interrupted();

}

4.2 非公平锁 NonfairSync.unlock()

2.1 unlock()方法的示意图

在这里插入图片描述

2.1 unlock()方法详解

  1. 调用 unlock() 方法,其实是直接调用 AbstractQueuedSynchronizer.release() 操作。

  2. 进入 release() 方法,内部先尝试 tryRelease() 操作,主要是去除锁的独占线程,然后将状态减一,这里减一主要是考虑到可重入锁可能自身会多次占用锁,只有当状态变成0,才表示完全释放了锁。

  3. 如果 tryRelease 成功,则将CHL队列的头节点的状态设置为0,然后唤醒下一个非取消的节点线程。

  4. 一旦下一个节点的线程被唤醒,被唤醒的线程就会进入 acquireQueued() 代码流程中,去获取锁。

代码如下:

public void unlock() {

sync.release(1);

}

public final boolean release(int arg) {

//尝试在当前锁的锁定计数(state)值上减1,

if (tryRelease(arg)) {

Node h = head;

if (h != null && h.waitStatus != 0)//waitStatus!=0表明或者处于CANCEL状态,或者是SIGNAL表示下一个线程在等待其唤醒。也就是说waitStatus不为零表示它的后继在等待唤醒。

unparkSuccessor(h);

//成功返回true

return true;

}

//否则返回false

return false;

}

private void unparkSuccessor(Node node) {

int ws = node.waitStatus;

//如果waitStatus < 0 则将当前节点清零

if (ws < 0)

compareAndSetWaitStatus(node, ws, 0);

//若后续节点为空或已被cancel,则从尾部开始找到队列中第一个waitStatus<=0,即未被cancel的节点

Node s = node.next;

if (s == null || s.waitStatus > 0) {

s = null;

for (Node t = tail; t != null && t != node; t = t.prev)

if (t.waitStatus <= 0)

s = t;

}

if (s != null)

LockSupport.unpark(s.thread);

}

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

这份面试题几乎包含了他在一年内遇到的所有面试题以及答案,甚至包括面试中的细节对话以及语录,可谓是细节到极致,甚至简历优化和怎么投简历更容易得到面试机会也包括在内!也包括教你怎么去获得一些大厂,比如阿里,腾讯的内推名额!

某位名人说过成功是靠99%的汗水和1%的机遇得到的,而你想获得那1%的机遇你首先就得付出99%的汗水!你只有朝着你的目标一步一步坚持不懈的走下去你才能有机会获得成功!

成功只会留给那些有准备的人!

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
本涵盖了95%以上Java开发知识点,真正体系化!**

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

总结

这份面试题几乎包含了他在一年内遇到的所有面试题以及答案,甚至包括面试中的细节对话以及语录,可谓是细节到极致,甚至简历优化和怎么投简历更容易得到面试机会也包括在内!也包括教你怎么去获得一些大厂,比如阿里,腾讯的内推名额!

某位名人说过成功是靠99%的汗水和1%的机遇得到的,而你想获得那1%的机遇你首先就得付出99%的汗水!你只有朝着你的目标一步一步坚持不懈的走下去你才能有机会获得成功!

成功只会留给那些有准备的人!

[外链图片转存中…(img-leYEjs6t-1713747966466)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 8
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值