ReentrantLock 源码解读

ReentrantLock 源码解读

傻瓜源码-内容简介

傻瓜源码-内容简介
🤪【职场经验】(持续更新)
精编短文:如何成为值钱的Java开发-指南

如何平常学习、如何书写简历、引导面试官、系统准备面试、选择offer、提升绩效、晋升TeamLeader.....
🧐【源码解读】(持续更新) <br/>1. 源码选材:Java架构师必须掌握的全部框架和类库源码<br/>2. 内容大纲:按照“企业应用Demo”讲解执行源码:总纲“阅读指南”、第一章“源码基础”、第二章“相关Java基础”、第三章“白话讲源码”、第四章“代码解读”、第五章“设计模式”、第六章“附录-面试习题、相关JDK方法、中文注释可运行源码项目”
3. 读后问题:粉丝群答疑解惑
已收录:HashMapReentrantLockThreadPoolExecutor《Spring源码解读》《Dubbo源码解读》.....
🤩【面试题集】(持续更新)<br/>1. 面试题选材:Java面试常问的全部面试题和必会知识点<br/>2. 内容大纲:第一部分”注意事项“、第二部分“面试题解读”(包括:”面试题“、”答案“、”答案详解“、“实际开发解说”)
3. 深度/广度:面试题集中的答案和答案详解,都是对齐通常面试要求的深度和广度
4. 读后问题:粉丝群答疑解惑
已收录:Java基础面试题集Java并发面试题集JVM面试题集数据库(Mysql)面试题集缓存(Redis)面试题集 .....
🤤【粉丝群】(持续更新) <br/>收录:阿里、字节跳动、京东、小米、美团、哔哩哔哩等大厂内推
😛 做者介绍:Spring系源码贡献者、世界五百强互联网公司、TeamLeader、Github开源产品做者
😛 做者微信:wowangle03 (企业内推联系我)

  加入个人粉丝社群,阅读更多内容。从学习到面试,从面试到工做,从 coder 到 TeamLeader,天天给你答疑解惑,还能有第二份收入!html

图片

第 1 章 阅读指南

  • 本文基于 open-jdk 1.8 版本。
  • 本文根据“ Demo ”解读源码。
  • 本文建议分为两个学习阶段,掌握了第一阶段,再进行第二阶段;java

    • 第一阶段,理解章节“源码解读”前的全部内容。即掌握 IT 技能:熟悉 ReentrantLock 原理。
    • 第二阶段,理解章节“源码解读”(包括源码解读)以后的内容。即掌握 IT 技能:精读 ReentrantLock 源码。
  • 建议按照本文内容顺序阅读(内容先后顺序存在依赖关系)。
  • 阅读过程当中,若是遇到问题,记下来,后面不远的地方确定有解答。
  • 阅读章节“源码解读”时,建议得到中文注释源码项目配合本文,Debug 进行阅读学习。
  • 源码项目中的注释含义:node

    • ” Demo “在源码中,会标注“ // ReentrantLock Demo ”。

第 2 章 简介

  ReentrantLock 是 java.util.concurrent 并发包中的可重入锁(可重入锁就是指持有锁的线程能够重复进入有该锁的代码块);基于 AQS(AbstractQueuedSynchronized)实现。须要手动释放锁,可是支持更多方法,好比:上公平/非公平锁、可被中断锁、可被中判定时锁等。面试

第 3 章 Demo

3.1 代码示例

@Test
    public void testReentrantLock1() {
        // 多个线程使用同一个 ReentrantLock 对象,上同一把锁,默认上非公平锁
        Lock lockNoFair = new ReentrantLock();
        try {
            // 本线程尝试获取锁;若是锁已经被其它线程持有,则会进入阻塞状态,直到获取到锁
            lockNoFair.lock();
            System.out.println("处理中...");
        } finally {
            // 释放锁
            lockNoFair.unlock();
        }
    }

    @Test
    public void testReentrantLock2() {
        // 多个线程使用同一个 ReentrantLock 对象,上同一把锁,构造函数传 true,上公平锁
        Lock lockFair = new ReentrantLock(true);
        try {
            // 本线程尝试获取锁;若是锁已经被其它线程持有,则会进入阻塞状态,直到获取到锁
            lockFair.lock();
            System.out.println("处理中...");
        } finally {
            // 释放锁
            lockFair.unlock();
        }
    }

3.2 方法介绍

  1. lock()(公平锁)sql

  线程获取锁的顺序彻底基于调用 lock() 方法的前后顺序。数据库

时间线 线程 1 线程 2 线程 3
1 线程 1 调用 lockFair.lock() 方法,获取到锁
2 线程 2 调用 lockFair.lock() 方法后,没有获取到锁,将线程 2 顺序放入到链表里排队,进入阻塞状态
3 线程 1 调用 lockFair.unLock() 方法,释放锁,唤醒线程 2
4 线程 3 调用 lockFair.lock() 方法,发现线程 2 已经在链表里等待得到锁;线程 3 就追加到线程 2 以后,进行排队
5 线程 2 被线程 1 唤醒,从新尝试获取锁,获取锁成功

  2. lock()(非公平锁)设计模式

时间线 线程 1 线程 2 线程 3
1 调用 lockNoFair.lock() 方法,获取到锁
2 调用 lockNoFair.lock() 方法后,没有获取到锁,线程 2 顺序追加到链表后排队,进入阻塞状态
3 调用 lockNoFair.unLock() 方法,释放锁,唤醒线程 2
4 调用 lockNoFair.lock() 方法,成功获取到锁
5 线程 2 被线程 1 唤醒,呆在链表里位置不动,从新尝试获取锁,获取锁失败,已经被线程 3 抢占到了锁,再次进入阻塞状态

  3. lockInterruptibly()(可被中断锁)缓存

  lockInterruptibly() 也分为公平锁和非公平锁,与 lock() 方法的区别就在于:当线程调用 lockInterruptibly() 方法没有获取到锁,进入阻塞后;若是其它线程对该线程标记为中断状态, lockInterruptibly() 方法则会从阻塞中唤醒,抛出中断异常。安全

  若是线程一开始就被标记为中断状态,再调用 lockInterruptibly() 方法,lockInterruptibly() 方法则会直接抛出中断异常。微信

  4. tryLock(long timeout, TimeUnit unit) 方法(可被中判定时锁)

  tryLock(long timeout, TimeUnit unit) 也区分公平锁和非公平锁;与 lockInterruptibly() 方法的区别就在于:线程调用 tryLock(long timeout, TimeUnit unit) 方法,获取不到锁,进入阻塞后;若是在指定的时间里,仍然没有被其它释放锁的线程唤醒,则会自动唤醒,直接返回失败。

第 4 章 相关 Java 基础

4.1 原子性

  知足如下几个特色,咱们就说这个操做支持原子性,线程安全:

  1. 原子操做中的全部子操做,要不全成功、要不全失败;
  2. 线程执行原子操做过程当中,不会受到其它线程的任何影响;
  3. 其它线程只能感知到原子操做开始前和结束后的变化。

解释:

  包含多个操做单元,但仍支持原子性,一般都是由锁实现的。

代码示例:

class Test {
    int x = 0;
    int y = 0;

    public void test() {
        // 原子操做
        x = 10; 

        // 大体分为两步:1)获取 x 的值到缓存里;2)取出缓存里的值,赋值给 y
        // 不支持原子性;获取 x 的值到缓存里以后,其它线程可能修改 x 的值,致使 y 值错误
        y = x; 

        // 大体分为三步:1)获取 x 的值到缓存里;2)取出缓存里的值加一;3)赋值给 x
        // 不支持原子性;原理相似 y = x;
        x++; 

    }

}

4.2 Cas

  Cas 是 Compare-and-swap(比较并替换)的缩写,是支持原子性的操做;在 Java 中,底层是 native 方法实现,经过 CPU 提供的 lock 信号保证的原子性。

  想要将数据 V 的原值 O 替换为新值 N,执行 Cas 操做,会有如下操做:

  1. 预先读取数据 V 的值 O 做为预期值;
  2. 执行 Cas 操做:

    1. 比较当前数据 V 的值是不是 O;

      1. 若是是,则替换为 N,返回执行成功;
      2. 若是不是,则不替换,返回执行失败;

例子:

  以 java.util.concurrent.atomic 包中的 AtomicInteger 为例;

public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(100);
        // 将 AtomicInteger 的值,从 100 替换为 200
        Boolean b = atomicInteger.compareAndSet(100, 200);
        // 返回 true,替换成功
        System.out.println(b);
    }

4.3 volatile

  用于修饰变量,能够保证被修饰变量的操做支持可见性和有序性,但不支持原子性。详见”Java 并发面试题集“

4.4 Thread

  void interrupt():在一个线程中调用另外一个线程的 interrupt() 方法,会将那个线程设置成线程中断状态,而不会真的中断线程。

  boolean isInterrupted(boolean ClearInterrupted):返回当前线程是否处于中断状态,ClearInterrupted 为 true,则返回的同时清除中断状态,反之则保留中断状态。

interrupt() 代码示例

class ThreadTest {
    public static void main(String[] args) throws Exception {

        MyThread thread = new MyThread();
        thread.start();
        
        // 主线程经过调用 thread 对象的 interrupt() 方法,将 thread 线程设置成线程中断状态,但不会真的中断线程。
        thread.interrupt();

        System.out.println("主线程执行结束!");
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("test thread!");
    }
}
// 打印结果:
// 主线程执行结束!
// test thread!

第 5 章 源码基础

5.1 Sync

  在 ReentrantLock 中,定义了一个内部抽象类 Sync;有两个实现类,分别为 FairSync(公平锁) 和 NonfairSync (非公平锁),是 ReentrantLock 里的真正实现锁逻辑的类。

代码示例 1 ReentrantLock 成员变量

public class ReentrantLock implements Lock, java.io.Serializable {
    
    private final Sync sync;
    
    abstract static class Sync extends AbstractQueuedSynchronizer {

    // state 表示锁的状态。int 类型未指定值,默认初始化为0;
    // 为 0 则表明当前没有线程持有锁;为 1 则表明有线程持有锁;若是大于1,则表明锁被当前线程重入的次数
    // 继承自 AbstractQueuedSynchronizer
    private volatile int state;
    
    // exclusiveOwnerThread 表示当前持有锁的线程对象
    // 继承自 AbstractOwnableSynchronizer
    private transient Thread exclusiveOwnerThread;

    // tail 记录尾线程节点对象,默认为 null
    // 继承自 AbstractQueuedSynchronizer
    private transient volatile Node tail;

    // head 记录头线程节点对象,默认为 null
    // 继承自 AbstractQueuedSynchronizer
    private transient volatile Node head;
    
}

5.2 ReentrantLock 中的链

  ReentrantLock 使用 FIFO (先进先出)队列来管理竞争锁的线程关系。

5.2.1 Node

  在 ReentrantLock 中, Sync 经过继承了 AQS 抽象类(AbstractQueuedSynchronizer),进而继承 AbstractQueuedSynchronizer 中的内部类 Node,用来封装线程,同时也是链表的组成元素。

代码示例 Node 重要成员变量

static final class Node {

    // prev 指向前一个节点(表明前一个进入链表的线程)
    volatile Node prev; 

    // next 指向后一个节点(表明后一个进入链表的线程)
    volatile Node next; 

    // thread 当前节点表示的线程    
    volatile Thread thread; 

    // 0:表示[初始默认值]或者[表示已解锁]
    // -1(SIGNAL):表示当前节点表明的线程在释放锁后须要唤醒下一个节点的线程
    // 1(CANCELLED):表示当前节点表明的线程在队列中发生异常(发生中断异常或者其它不可预知的异常),标记为取消状态
    volatile int waitStatus;

5.2.2 队列生命周期

  1. 初始状态:head 和 tail 变量为空,不存在链表。

    1. 第一个线程 0 获取到锁( state 成功由 0 修改成 1),不须要链表。
  2. 初始化不表明线程的头节点,再在头节点后插入线程 1 节点(线程 1 获取锁失败,放入链表,等待获取锁):

    1. 首先会先初始化一个不表明任何线程的 Node 节点 node;
    2. 而后将 tail 变量和 head 变量都指向这个 node,做为头节点和尾节点;
    3. 而后再为线程 1 新建一个 Node 节点对象 node1,追加到头节点后面:

      1. 将 node1 的 prev 属性指向链表的尾节点;
      2. 将链表的尾节点的 next 属性指向为 node1 节点;
      3. 最后,将 tail 变量指向了 node1 节点。
  3. 线程 1 节点替换成头节点(线程 1 在链表中被线程 0 唤醒,成功获取到锁):

    1. 线程 0 释放锁后,唤醒头节点后的线程 1 节点;
    2. 当线程 1 成功获取到锁( state 成功由 0 修改成 1)后,会把 head 变量指向为当前节点;
    3. 由于当前节点表明的线程已经获取到锁,因此当前节点再也不须要表明线程,就会把 thread 属性修改成空,prev 属性修改成空;
    4. 将老头节点的 next 属性置为空(加快 GC)。

<br/>

加入个人粉丝社群,阅读所有内容

  从学习到面试,从面试到工做,从 coder 到 TeamLeader,天天给你答疑解惑,还能有第二份收入,这样的知识星球,难道你还要犹豫!

图片

相关文章
相关标签/搜索