专栏首页傻瓜源码ReentrantLock 源码解读

抱歉,你查看的文章已删除

ReentrantLock 源码解读

ReentrantLock 源码解读

第 1 章 阅读指南

  • 本文基于 open-jdk 1.8 版本。
  • 本文根据“ Demo ”解读源码。
  • 本文建议分为两个学习阶段,掌握了第一阶段,再进行第二阶段;
  • 第一阶段,理解章节“源码解读”前的所有内容。即掌握 IT 技能:熟悉 ReentrantLock 原理。
  • 第二阶段,理解章节“源码解读”(包括源码解读)之后的内容。即掌握 IT 技能:精读 ReentrantLock 源码。
  • 建议按照本文内容顺序阅读(内容前后顺序存在依赖关系)。
  • 阅读过程中,如果遇到问题,记下来,后面不远的地方肯定有解答。
  • 阅读章节“源码解读”时,建议获得中文注释源码项目配合本文,Debug 进行阅读学习。
  • 源码项目中的注释含义:
  • ” 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()(公平锁)**

  线程获取锁的顺序完全基于调用 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 操作:
  3. 比较当前数据 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,则代表锁被当前线程重入的次数

    // 继承自 AbstractQueuedSynchronize

    private volatile int state;

    

    // exclusiveOwnerThread 表示当前持有锁的线程对象

    // 继承自 AbstractOwnableSynchronize

    private transient Thread exclusiveOwnerThread;



    // tail 记录尾线程节点对象,默认为 null

    // 继承自 AbstractQueuedSynchronize

    private transient volatile Node tail;



    // head 记录头线程节点对象,默认为 null

    // 继承自 AbstractQueuedSynchronize

    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 变量为空,不存在链表。
  2. 第一个线程 0 获取到锁( state 成功由 0 修改为 1),不需要链表。
  3. **初始化不代表线程的头节点,再在头节点后插入线程 1 节点(线程 1 获取锁失败,放入链表,等待获取锁):**
  4. 首先会先初始化一个不代表任何线程的 Node 节点 node;
  5. 然后将 tail 变量和 head 变量都指向这个 node,作为头节点和尾节点;
  6. 然后再为线程 1 新建一个 Node 节点对象 node1,追加到头节点后面:
  1. 将 node1 的 prev 属性指向链表的尾节点;
  2. 将链表的尾节点的 next 属性指向为 node1 节点;
  3. 最后,将 tail 变量指向了 node1 节点。
  1. **线程 1 节点替换成头节点(线程 1 在链表中被线程 0 唤醒,成功获取到锁):**
  2. 线程 0 释放锁后,唤醒头节点后的线程 1 节点;
  3. 当线程 1 成功获取到锁( state 成功由 0 修改为 1)后,会把 head 变量指向为当前节点;
  4. 因为当前节点代表的线程已经获取到锁,所以当前节点不再需要代表线程,就会把 thread 属性修改为空,prev 属性修改为空;
  5. 将老头节点的 next 属性置为空(加快 GC)。

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

原文链接:https://www.cnblogs.com/javaEE-blog/p/13174137.html

我来说两句

0 条评论
登录 后参与评论

相关文章

  • Android多线程:你必须要了解的多线程基础知识汇总

    版权声明:本文为博主原创文章,未经博主允许不得转载,更多请继续关注Carson_Ho htt...

    Carson.Ho
  • 线程的生命周期

    线程的六种状态: NEW、RUNNABLE、BIOCKED、WAITING、TIME_WAITING、TERMINATED。

    用户7386338
  • 没想到,这么简单的线程池用法,深藏这么多坑!

    生产有个对账系统,每天需要从渠道端下载对账文件,然后开始日终对账。这个系统已经运行了很久,前两天突然收到短信预警,没有获取渠道端对账文件。

    andyxh
  • Java 线程池中的线程复用是如何实现的?

    那么就来和大家探讨下这个问题,在线程池中,线程会从 workQueue 中读取任务来执行,最小的执行单位就是 Worker,Worker 实现了 Runnabl...

    用户1516716
  • Java多线程:线程属性

    喜欢天文的pony站长
  • JUC线程池ThreadPoolExecutor源码分析

    很早之前就打算看一次JUC线程池ThreadPoolExecutor的源码实现,由于近段时间比较忙,一直没有时间整理出源码分析的文章。之前在分析扩展线程池实现可...

    Throwable
  • Java并发编程之线程池

    java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池

    日薪月亿
  • JUC学习笔记(四)—线程池

    线程池 【死磕Java并发】—–J.U.C之线程池:ThreadPoolExecutor

    Monica2333
  • Java并发编程之线程池

    java.util.concurrent.Executors提供了一个 java.util.concurrent.Executor接口的实现用于创建线程池

    日薪月亿
  • 并发编程之线程池ThreadPoolExecutor

    在我们平时自己写线程的测试demo时,一般都是用new Thread的方式来创建线程。但是,我们知道创建线程对象,就会在内存中开辟空间,而线程中的任务执行完毕之...

    烟雨星空

扫码关注云+社区

领取腾讯云代金券