这篇 ReentrantLock 看不懂,加我我给你发红包(二)

简介: 在开始本篇文章的内容讲述前,先来回答我一个问题,为什么 JDK 提供一个 synchronized 关键字之后还要提供一个 Lock 锁,这不是多此一举吗?难道 JDK 设计人员都是沙雕吗?

在看第三条路线 acquireQueued

微信图片_20220412185904.jpg

主要会有两个分支判断,首先会进行无限循环中,循环中每次都会判断给定当前节点的先驱节点,如果没有先驱节点会直接抛出空指针异常,直到返回 true。

然后判断给定节点的先驱节点是不是头节点,并且当前节点能否获取独占式锁,如果是头节点并且成功获取独占锁后,队列头指针用指向当前节点,然后释放前驱节点。如果没有获取到独占锁,就会进入 shouldParkAfterFailedAcquireparkAndCheckInterrupt 方法中,我们贴出这两个方法的源码

微信图片_20220412185909.jpg

shouldParkAfterFailedAcquire 方法主要逻辑是使用compareAndSetWaitStatus(pred, ws, Node.SIGNAL)使用CAS将节点状态由 INITIAL 设置成 SIGNAL,表示当前线程阻塞。当 compareAndSetWaitStatus 设置失败则说明 shouldParkAfterFailedAcquire 方法返回 false,然后会在 acquireQueued 方法中死循环中会继续重试,直至compareAndSetWaitStatus 设置节点状态位为 SIGNAL 时 shouldParkAfterFailedAcquire 返回 true 时才会执行方法 parkAndCheckInterrupt 方法。(这块在后面研究 AQS 会细讲)

parkAndCheckInterrupt 该方法的关键是会调用 LookSupport.park 方法(关于LookSupport会在以后的文章进行讨论),该方法是用来阻塞当前线程。

所以 acquireQueued 主要做了两件事情:如果当前节点的前驱节点是头节点,并且能够获取独占锁,那么当前线程能够获得锁该方法执行结束退出

如果获取锁失败的话,先将节点状态设置成 SIGNAL,然后调用 LookSupport.park 方法使得当前线程阻塞。

如果 !tryAcquireacquireQueued 都为 true 的话,则打断当前线程。

那么它们的主要流程如下(注:只是加锁流程,并不是 lock 所有流程)


微信图片_20220412185914.jpg

非公平锁的加锁(lock)流程详解

非公平锁的加锁步骤和公平锁的步骤只有两处不同,一处是非公平锁在加锁前会直接使用 CAS 操作设置同步状态,如果设置成功,就会把当前线程设置为偏向锁的线程;一处是 CAS 操作失败执行 tryAcquire 方法,读取线程同步状态,如果未加锁会使用 CAS 再次进行加锁,不会等待 hasQueuedPredecessors 方法的执行,达到只要线程释放锁就会加锁的目的。下面通过源码和流程图来详细理解

微信图片_20220412185918.jpg

这是非公平锁和公平锁不同的两处地方,下面是非公平锁的加锁流程图

微信图片_20220412185921.jpg

lockInterruptibly 以可中断的方式获取锁

以下是 JavaDoc 官方解释:

lockInterruptibly 的中文意思为如果没有被打断,则获取锁。如果没有其他线程持有该锁,则获取该锁并立即返回,将锁保持计数设置为1。如果当前线程已经持有锁,那么此方法会立刻返回并且持有锁的数量会 + 1。如果锁是由另一个线程持有的,则出于线程调度目的,当前线程将被禁用,并处于休眠状态,直到发生以下两种情况之一

  • 锁被当前线程持有
  • 一些其他线程打断了当前线程

如果当前线程获取了锁,则锁保持计数将设置为1。

如果当前线程发生了如下情况:

  • 在进入此方法时设置了其中断状态
  • 当获取锁的时候发生了中断(Thread.interrupt)

那么当前线程就会抛出InterruptedException 并且当前线程的中断状态会清除。

下面看一下它的源码是怎么写的

微信图片_20220412185925.jpg

首先会调用 acquireInterruptibly 这个方法,判断当前线程是否被中断,如果中断抛出异常,没有中断则判断公平锁/非公平锁 是否已经获取锁,如果没有获取锁(tryAcquire 返回 false)则调用 doAcquireInterruptibly 方法,这个方法和 acquireQueued 方法没什么区别,就是线程在等待状态的过程中,如果线程被中断,线程会抛出异常。

下面是它的流程图

微信图片_20220412185929.jpg

tryLock 尝试加锁

仅仅当其他线程没有获取这把锁的时候获取这把锁,tryLock 的源代码和非公平锁的加锁流程基本一致,它的源代码如下

微信图片_20220412185933.jpg

tryLock 超时获取锁

ReentrantLock除了能以中断的方式去获取锁,还可以以超时等待的方式去获取锁,所谓超时等待就是线程如果在超时时间内没有获取到锁,那么就会返回false,而不是一直死循环获取。可以使用 tryLock 和 tryLock(timeout, unit)) 结合起来实现公平锁,像这样

if (lock.tryLock() || lock.tryLock(timeout, unit)) {...}

如果超过了指定时间,则返回值为 false。如果时间小于或者等于零,则该方法根本不会等待。

它的源码如下

微信图片_20220412185937.jpg

首先需要了解一下 TimeUnit 工具类,TimeUnit 表示给定粒度单位的持续时间,并且提供了一些用于时分秒跨单位转换的方法,通过使用这些方法进行定时和延迟操作。

toNanos 用于把 long 型表示的时间转换成为纳秒,然后判断线程是否被打断,如果没有打断,则以公平锁/非公平锁 的方式获取锁,如果能够获取返回true,获取失败则调用doAcquireNanos方法使用超时等待的方式获取锁。在超时等待获取锁的过程中,如果等待时间大于应等待时间,或者应等待时间设置不合理的话,返回 false。

微信图片_20220412185942.jpg

这里面以超时的方式获取锁也可以画一张流程图如下

微信图片_20220412190218.jpg


相关文章
|
Python
我这样回答多线程并发,面试官非要跟我做朋友!
我这样回答多线程并发,面试官非要跟我做朋友!
122 0
|
存储 安全 算法
Java并发编程73道面试题及答案 —— 这也太棒了(二)
Java并发编程73道面试题及答案 —— 这也太棒了
|
存储 缓存 安全
Java并发编程73道面试题及答案 —— 这也太棒了(一)
Java并发编程73道面试题及答案 —— 这也太棒了
|
安全 算法 Java
Java并发编程73道面试题及答案 —— 这也太棒了(三)
Java并发编程73道面试题及答案 —— 这也太棒了
|
Java
这篇 ReentrantLock 看不懂,加我我给你发红包(三)
在开始本篇文章的内容讲述前,先来回答我一个问题,为什么 JDK 提供一个 synchronized 关键字之后还要提供一个 Lock 锁,这不是多此一举吗?难道 JDK 设计人员都是沙雕吗?
138 1
这篇 ReentrantLock 看不懂,加我我给你发红包(三)
|
Java 调度
这篇 ReentrantLock 看不懂,加我我给你发红包(二)
在开始本篇文章的内容讲述前,先来回答我一个问题,为什么 JDK 提供一个 synchronized 关键字之后还要提供一个 Lock 锁,这不是多此一举吗?难道 JDK 设计人员都是沙雕吗?
100 0
这篇 ReentrantLock 看不懂,加我我给你发红包(二)
|
Java 调度
这篇 ReentrantLock 看不懂,加我我给你发红包(一)
在开始本篇文章的内容讲述前,先来回答我一个问题,为什么 JDK 提供一个 synchronized 关键字之后还要提供一个 Lock 锁,这不是多此一举吗?难道 JDK 设计人员都是沙雕吗?
85 0
这篇 ReentrantLock 看不懂,加我我给你发红包(一)
10分钟手撸Java线程池,yyds!!
10分钟手撸Java线程池,yyds!!
270 0
10分钟手撸Java线程池,yyds!!
|
缓存 Oracle Java
搞定ReentrantReadWriteLock 几道小小数学题就够了
搞定ReentrantReadWriteLock 几道小小数学题就够了
搞定ReentrantReadWriteLock 几道小小数学题就够了
阿粉写了八千多字,就是为了把 ReentrantLock 讲透(一)
啥是可重入锁呢?比如:线程 1 通过调用 lock() 方法获取锁之后,再调用 lock 时,就不会再进行阻塞获取锁,而是直接增加重试次数。 还记得 synchronized 吗?它有 monitorenter 和 monitorexit 两种指令来保证锁,而它们的作用可以理解为每个锁对象拥有一个锁计数器,也就是如果再次调用 lock() 方法,计数器会进行加 1 操作
阿粉写了八千多字,就是为了把 ReentrantLock 讲透(一)