关于 ReentrantLock 中锁 lock() 和解锁 unlock() 的底层原理浅析(2)

static Lock lock = new ReentrantLock();

public static void main(String[] args) {

// 使用两个线程模拟多线程执行并发

new Thread(() -> doBusiness(), “Thread-1”).start();

new Thread(() -> doBusiness(), “Thread-2”).start();

}

private static void doBusiness() {

try {

lock.lock();

System.out.println(“需要加锁的业务处理代码,防止并发异常”);

} finally {

lock.unlock();

}

}

带着这样的疑问,我们先后跟进 lock()和unlock() 源码一探究竟

说明:

1、在进行查看 ReentrantLock 进行 lock() 加锁和 unlock() 解锁源码时,需要知道 LockSupport 类、了解自旋锁以及链表相关知识。

2、在分析过程中,假设第一个线程获取到锁的时候执行代码需要很长时间才释放锁,及在第二个第三个线程来获取锁的时候,第一个线程并没有执行完成,没有释放锁资源。

3、在分析过程中,我们假设第一个线程就是最先进来获取锁的线程,那么第二个第三个线程也是依次进入的,不会存在第三个线程先于第二个线程(即第三个线程如果先于第二个线程发生,那么第三个线程就是我们下面描述的第二个线程)

一、 lock() 方法

============================================================================

1、查看lock()方法源码

public void lock() {

sync.lock();

}

从上面可以看出 ReentrantLock 的 lock() 方法调用的是 sync 这个对象的 lock() 方法,而 Sync 就是一个实现了抽象类AQS(AbstractQueuedSynchronizer) 抽象队列同步器的一个子类 ,继续跟进代码(说明:ReentrantLock 分为公平锁和非公平锁,如果无参构造器创建锁默认是非公平锁,我们按照非公平锁的代码来讲解)

1.1 关于Sync子类的源码

abstract static class Sync extends AbstractQueuedSynchronizer {

// 此处省略具体实现AbstractQueuedSynchronizer 类的多个方法

}

这里需要说明的是 AbstractQueuedSynchronizer 抽象队列同步器底层是一个通过Node实现的双向链表,该抽象同步器有三个属性 head 头节点 , tail 尾节点 和 state 状态值。

属性1:head——注释英文翻译:等待队列的头部,懒加载,用于初始化,当调用 setHead() 方法的时候会对 head 进行修改。注:如果 head 节点存在,则 head 节点的 waitStatus 状态值用于保证其不变成 CANCELLED(取消,值为1) 状态

/**

  • Head of the wait queue, lazily initialized. Except for

  • initialization, it is modified only via method setHead. Note:

  • If head exists, its waitStatus is guaranteed not to be

  • CANCELLED.

*/

private transient volatile Node head;

属性2: tail——tail节点是等待队列的尾部,懒加载,在调用 enq() 方法添加一个新的 node 到等待队列的时候会修改 tail 节点。

/**

  • Tail of the wait queue, lazily initialized. Modified only via

  • method enq to add new wait node.

*/

private transient volatile Node tail;

属性3:state——用于同步的状态码。 如果 state 该值为0,则表示没有其他线程获取到锁,如果该值大于1则表示已经被某线程获取到了锁,该值可以是2、3、4,用该值来处理重入锁(递归锁)的逻辑。

/**

  • The synchronization state.

*/

private volatile int state;

1.2 上面 Sync 类使用 Node来作为双向队列的具体保存值和状态的载体,Node 的具体结构如下

static final class Node {

/** Marker to indicate a node is waiting in shared mode */

static final Node SHARED = new Node(); // 共享锁模式(主要用于读写锁中的读锁)

/** Marker to indicate a node is waiting in exclusive mode */

static final Node EXCLUSIVE = null; // 排他锁模式(也叫互斥锁)

/** waitStatus value to indicate thread has cancelled */

static final int CANCELLED = 1; // 线程等待取消,不再参与锁竞争

/** waitStatus value to indicate successor’s thread needs unparking */

static final int SIGNAL = -1; // 表明线程需要被唤醒,可以竞争锁

/** waitStatus value to indicate thread is waiting on condition */

static final int CONDITION = -2; // 表明线程在某种情况下等待

/** waitStatus value to indicate the next acquireShared should unconditionally propagate */

static final int PROPAGATE = -3;

volatile int waitStatus; // 默认等待状态为0

volatile Node prev;

volatile Node next;

/** The thread that enqueued this node. Initialized on construction and nulled out after use.*/

volatile Thread thread; // 当前线程和节点进行绑定,通过构造器初始化Thread,在使用的时候将当前线程替换原有的null值

// 省略部分代码

说明: Sync 通过Node节点构建队列,Node节点使用prev和next节点来行程双向队列,使用prev来关联上一个节点,使用next来关联下一个节点,每一个node节点和一个thread线程进行绑定,用来表示当前线程在阻塞队列中的具体位置和状态 waitStatus 。

2、上面的 sync.lock() 继续跟进源码(非公平锁):

final void lock() {

if (compareAndSetState(0, 1))

setExclusiveOwnerThread(Thread.currentThread());

else

acquire(1);

}

说明:上面代码说明,如果 compareAndSetState(0, 1) 为 true ,则执行 setExclusiveOwnerThread(Thread.currentThread()) ,否则执行 acquire(1);

2.1 compareAndSetState(0, 1) 底层使用unsafe类完成CAS操作 ,意思就是判断当前state状态是否为0,如果为零则将该值修改为1,并返回true;state不为0,则无法将该值修改为1,返回false。

protected final boolean compareAndSetState(int expect, int update) {

// See below for intrinsics setup to support this

return unsafe.compareAndSwapInt(this, stateOffset, expect, update);

}

2.2 假如 第1个线程 进来的时候 compareAndSetState(0, 1) 肯定执行成功,state 状态会从0变成1,同时返回true,执行 setExclusiveOwnerThread(Thread.currentThread()) 方法:

protected final void setExclusiveOwnerThread(Thread thread) {

exclusiveOwnerThread = thread;

}

setExclusiveOwnerThread(Thread.currentThread()) 表示将当前 Sync 对象和当前线程绑定,意思是表明:当前对内同步器执行的线程为 thread,该 thread 获取了锁正在执行。

2.3 假如 进来的线程为第2个 ,并且第一个线程还在执行没有释放锁,那么第2个线程就会执行 acquire(1) 方法:

public final void acquire(int arg) {

if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

selfInterrupt();

}

进入到该方法中发现,需要通过 !tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 两个方法判断是否需要执行 selfInterrupt();

(1)先执行 tryAcquire(arg) 这个方法进行判断

protected final boolean tryAcquire(int acquires) {

return nonfairTryAcquire(acquires);

}

/**

  • Performs non-fair tryLock. tryAcquire is implemented in

  • subclasses, but both need nonfair try for trylock method.

*/

final boolean nonfairTryAcquire(int acquires) {

final Thread current = Thread.currentThread();

// 获取state状态,因为第一个线程进来的时候只要还没有执行完就已经将state设置为1了(即:2.1步)

int c = getState();

// 再次判断之前获取锁的线程是否已经释放锁了

if (c == 0) {

// 如果之前的线程已经释放锁,那么当前线程进来就将状态改为1,并且设置当前占用锁的线程为自身

if (compareAndSetState(0, acquires)) {

setExclusiveOwnerThread(current);

return true;

}

}

// 判断当前占用锁的线程是不是就是我自身,如果是我自身,这将State在原值的基础上进行加1,来处理重入锁逻辑

else if (current == getExclusiveOwnerThread()) {

int nextc = c + acquires;

// 判断重入锁次数是不是超过限制,超过限制则直接报错

if (nextc < 0) // overflow

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

setState(nextc);

return true;

}

return false;

}

从上面的方法看, 如果第二个线程进来,且第一个线程还未释放锁的情况下,该方法 tryAcquire(arg) 直接放回false,那么 !tryAcquire(arg) 就为true,需要判断第二个方法 acquireQueued( addWaiter(Node.EXCLUSIVE) , arg),第二个方法先执行addWaiter(Node.EXCLUSIVE),及添加等待线程进入队列

(2)添加等待线程到同步阻塞队列中

private Node addWaiter(Node mode) {

// 将当前线程和node节点进行绑定,设置模式为排他锁模式

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

// Try the fast path of enq; backup to full enq on failure

Node pred = tail;// 第二个线程也就是第一次进来该方法的线程,tail肯定是null

if (pred != null) { // 如果tail尾节点不为空,表示第3、4、5次进来的线程

node.prev = pred; // 那么就将当前进来的线程节点的 prev 节点指向之前的尾节点

if (compareAndSetTail(pred, node)) { // 通过比较并交换,如果当前尾节点在设置过程中没有被其他线程抢先操作,那么就将当前节点设置为tail尾节点

pred.next = node; // 将以前尾节点的下一个节点指向当前节点(新的尾节点)

return node;

}

}

enq(node); // 如果为第二个线程进来,就是上面的 pred != null 成立没有执行,直接执行enq()方法

return node;

}

private Node enq(final Node node) {

for (;😉 { // 一直循环检查,相当于自旋锁

Node t = tail;

if (t == null) { // Must initialize

// 第二个线程的第一次进来肯定先循环进入该方法,这时设置头结点,该头结点一般被称为哨兵节点,并且头和尾都指向该节点

if (compareAndSetHead(new Node()))

tail = head;

} else {

// 1、第二个线程在第二次循环时将进入else 方法中,将该节点挂在哨兵节点(头结点)后,并且尾节点指向该节点,并且将该节点返回(该节点有prev信息)

node.prev = t;

if (compareAndSetTail(t, node)) {

t.next = node;

return t;

}

}

}

}

如上在执行 enq(final Node node) 结束,并且返回添加了第二个线程node节点的时候, addWaiter(Node mode) 方法会继续向上返回

或者 : 如果是添加第3、4个线程直接走 addWaiter(Node mode) 方法中的 if 流程直接添加返回 都将,到了 2.3 步,执行 acquireQueued(final Node node, int arg) ,再次贴源码

public final void acquire(int arg) {

if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))

selfInterrupt();

}

(3)即下一步就会执行 acquireQueued(addWaiter(Node.EXCLUSIVE), arg) 方法:

注: 上面的流程是将后面的线程加入到了同步阻塞队列中,下面的方法第一个 if (p == head && tryAcquire(arg))则是看同步阻塞队列的第一条阻塞线程是否可以获取到锁,如果能够获取到锁就修改相应链表结构,第二个if ( shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt() 即将发生线程阻塞

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

boolean failed = true;

try {

boolean interrupted = false;

for (;😉 {

// 自旋锁,如果为第二个线程,那么 p 就是 head 哨兵节点

final Node p = node.predecessor();

if (p == head && tryAcquire(arg)) {

// 上面的 if 表明如果当前线程为同步阻塞队列中的第一个线程,那么就再次试图获取锁 tryAcquire(),如果获取成功,则修改同步阻塞队列

setHead(node); // 将head头结点(哨兵节点)设置为已经获取锁的线程node,并将该node的Theread 设置为空

p.next = null; // help GC 取消和之前哨兵节点的关联,便于垃圾回收器对之前数据的回收

failed = false;

return interrupted;

}

// 如果第二个线程没有获取到锁(同步阻塞队列中的第一个线程),那么就需要执行下面两个方法,注标蓝的方法会让当前未获取到锁的线程阻塞

if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())

interrupted = true;

}

} finally {

if (failed)

cancelAcquire(node);

}

}

private void setHead(Node node) {

// 将哨兵节点往后移,并且将 thread 设置为空,取消和以前哨兵节点的关联,并于垃圾回收器回收

head = node;

node.thread = null;

node.prev = null;

}

shouldParkAfterFailedAcquire(p, node)这个方法将哨兵队列的状态设置为待唤醒状态

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

int ws = pred.waitStatus;

// pred为哨兵节点,ws为哨兵节点的状态

if (ws == Node.SIGNAL)

/*

  • This node has already set status asking a release

  • to signal it, so it can safely park.

*/

return true;

if (ws > 0) {

/*

  • Predecessor was cancelled. Skip over predecessors and

  • indicate retry.

*/

do {

node.prev = pred = pred.prev;

} while (pred.waitStatus > 0);

pred.next = node;

} else {

/*

  • waitStatus must be 0 or PROPAGATE. Indicate that we

  • need a signal, but don’t park yet. Caller will need to

  • retry to make sure it cannot acquire before parking.

*/

compareAndSetWaitStatus(pred, ws, Node.SIGNAL); // 将头结点(哨兵节点)设置成待唤醒状态,第一次进来的时候

}

return false;

}

parkAndCheckInterrupt()这个方法会让当前线程阻塞

private final boolean parkAndCheckInterrupt() {

LockSupport.park(this); // LockSupport.park()会导致当前线程阻塞,直到某个线程调用unpark()方法

return Thread.interrupted();

}

那么在lock()方法执行时,只要第一个线程没有unlock()释放锁,其他所有线程都会加入同步阻塞队列中,该队列中记录了阻塞线程的顺序,在加入同步阻塞队列前有多次机会可以抢先执行(非公平锁),如果没有被执行到,那么加入同步阻塞队列后,就只有头部节点(哨兵节点)后的阻塞线程有机会获取到锁进行逻辑处理。再次查看该方法:

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)) {

// if 表明只有头部节点(哨兵节点)后的节点在放入同步阻塞队列前可以获取锁

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

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

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

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

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

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

img

技术学习总结

学习技术一定要制定一个明确的学习路线,这样才能高效的学习,不必要做无效功,既浪费时间又得不到什么效率,大家不妨按照我这份路线来学习。

最后面试分享

大家不妨直接在牛客和力扣上多刷题,同时,我也拿了一些面试题跟大家分享,也是从一些大佬那里获得的,大家不妨多刷刷题,为金九银十冲一波!

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

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

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

img

技术学习总结

学习技术一定要制定一个明确的学习路线,这样才能高效的学习,不必要做无效功,既浪费时间又得不到什么效率,大家不妨按照我这份路线来学习。

[外链图片转存中…(img-9JL0u6nD-1713530667871)]

[外链图片转存中…(img-oqhpXoR6-1713530667871)]

[外链图片转存中…(img-vadaHsyu-1713530667872)]

最后面试分享

大家不妨直接在牛客和力扣上多刷题,同时,我也拿了一些面试题跟大家分享,也是从一些大佬那里获得的,大家不妨多刷刷题,为金九银十冲一波!

[外链图片转存中…(img-5fLfmymo-1713530667872)]

[外链图片转存中…(img-gw2OgtZz-1713530667872)]

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值