乐趣区

深度解读-ReentrantLock底层源码

目录

  • ReentrantLock 简介
  • 基础知识铺垫
  • state 属性

    • 线程持有者属性
    • ReentrantLock 中的队列使用
  • Demo& 原理解析

    • 公平锁 -lock()方法

      • Demo
      • 白话原理(面试口述)
      • 详尽原理
      • 知识点总结

        • FIFO 链表生命轨迹总结
        • waitStatus 属性生命轨迹总结
    • unLock()方法

      • 白话原理(面试口述)
      • 详尽原理
    • 公平锁 -lock()方法

      • Demo
      • 白话原理(面试口述)
      • 详尽原理
    • lockInterruptibly()方法

      • Demo
      • 白话原理(面试口述)
      • 详尽原理
    • tryLock()方法

      • Demo
      • 白话原理(面试口述)
      • 详尽原理
    • tryLock(long timeout, TimeUnit unit)方法

      • Demo
      • 白话原理(面试口述)
      • 详尽原理
  • 面试题汇总
  • ReentrantLock 全中文注释可运行源码下载

ReentrantLock 简介

​ jdk 并发包中的可重入锁,是基于 AQS(AbstractQueuedSynchronized)实现的,它有公平锁(线程上锁的顺序完全基于调用方法的先后顺序)和不公平锁(线程上锁的顺序不完全基于调用方法的先后顺序)两种实现方式。

​ ReentrantLock 提供多种 API:公平锁 / 非公平锁 -lock()方法、定时释放锁 -tryLock(long timeout, TimeUnit unit)方法、interrupt 中断阻塞线程抛出 InterruptedException 异常、获取锁失败直接返回的 tryLock()

​ 本文基于 open-jdk 1.8 讲解

基础知识铺垫

state 属性

    // 同步状态标识    
    private volatile int state;

state 为 0 时,代表当前没有线程持有锁;为 1 时,代表有线程持有锁;如果大于 1,因为 ReentrantLock 为重入锁,所以代表锁被当前线程重入的次数。

使用 volatile,保证了 state 属性值的可见性(可见性就是在获得属性值时,总能保证是最新值的特性)。

线程持有者属性

    // 当前线程持有者标识
    private transient Thread exclusiveOwnerThread;

该属性是 Thread 类型的,标识了锁的当前持有线程。

ReentrantLock 中的队列使用

​ ReentrantLock 中的队列是 FIFO(先进先出),为了实现公平锁,保证线程的线程的加锁顺序,同时也是存储元素,那么这个队列的数据结构是怎样的呢?

​ 在 ReentrantLock 中,定义了一个 Node 类,用来表示线程,同时也是链表的组成元素,Node 的 prev 属性指向前一个节点(代表前一个进入队列的线程),next 属性指向后一个节点(代表后一个进入队列的线程),这种方式形成了一个链表;AQS 还维护了 Node 类型的 head 属性和 tail 属性,默认为 null,分别表示头结点和尾节点,这两个属性为了让在后续逻辑中,能够很轻易的拿到头和尾节点,做出逻辑处理和判断。

以下是 Node 类的核心属性:

    // 指向前一个节点    
    volatile Node prev; 

    // 指向后一个节点    
    volatile Node next; 

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

    /*
        等待状态, 针对 ReentrantLock 下的方法, 共有 3 种值, 为什么说是针对呢?因为该属性是继承 AQS 而来的,其它并发包也在使用这个属性,所以 ReentrantLock 只有用到部分
        针对 ReentrantLock 都有什么值呢,都是什么含义呢?0: 初始化默认状态或者是无效状态,即在成员变量定义 int 类型默认为 0,或者表示已解锁
      -1(SIGNAL): 标记当前结点表示的线程在释放锁后需要唤醒下一个节点的线程,以当前值来标识是否要进行唤醒操作
      1(CANCELLED): 在同步队列中等待的线程未正常结束(发生中断异常或者其它不可预知的异常), 标记为取消状态
     */
    volatile int waitStatus; 

Demo& 原理解析

非公平锁 -lock()方法

Demo

    public void testReentrantLock() {    
        // 多个线程使用同一个 ReentrantLock 对象,上同一把锁
        Lock lock = new ReentrantLock();      
        lock.lock();  
        System.out.println("处理");
        lock.unlock();}

白话原理(面试口述)

​ 调用 ReentrantLock 无参构造器进行初始化,默认使用不公平锁进行实现;调用 lock 方法:

第一步、抢占锁

  1. 进入 lock 方法就先调用 cas 方法抢占锁(将 state 从 0 修改为 1),不管是否有线程在排队

    1. 如果修改成功,则更新当前线程持有者属性为当前线程
    2. 如果修改不成功,则判断当前的线程持有者是不是当前线程,但是在这之前有可能被其它线程释放锁,state 变为了 0,所以还要再判断一下 state 的值

      1. 如果为 0,则再调用 cas 方法尝试上锁,不管是否有线程在排队

        1. 上锁成功,则修改当前线程持有者属性,返回上锁成功
        2. 如果上锁失败,则进入加入队列流程
      2. 如果 state 不为 0,则判断线程持有者是否是当前线程

        1. 若是当前线程,将 state 加 1,累加重入次数,返回上锁成功
        2. 若不是当前线程,则进入加入队列流程

第二步、抢占锁失败,加入队列

  1. 初始化代表当前线程的 Node 节点 node,通过判断尾节点是否为 null 的方式,判断链表是否被初始化

    1. 如果链表没有被初始化,构建一个不代表任何线程的 Node 类型节点作为头结点,并调用 cas 方法赋值给 head 和 tail 变量,此时的链表只有一个 node 节点,即头节点就是尾结点
    2. 如果链表已经被初始化,将 node 的 prev 属性赋值为之前链表的尾结点,将之前链表的尾结点的 next 属性赋值为 node 节点,再将 tail 变量赋值为 node 节点

第三步、加入队列后,自旋 1 到 2 次尝试获取锁,如果再获取不到锁,则阻塞线程,直到被唤醒,成功获取锁

  1. 判断 node 节点的 prev 属性(前一个节点)是否为 head 变量(头结点)

    1. 如果是头结点,则再尝试获取锁

      1. 如果获取锁成功,将当前 node 节点设置为 head 变量(头节点),并且为了快点 GC 之老的头结点,将老的头结点的 next 属性赋值为 null
      2. 如果获取锁不成功,则尝试阻塞线程
  2. 如果不是头结点或者获取锁失败,则利用 cas 方法将 node 节点的 prev 节点的 waitStatus 从默认值 0 改为 -1,标记当前结点表示的线程在释放锁后需要唤醒下一个节点的线程
  3. 然后当 node 节点的 prev 节点 waitStatus 值为 - 1 时,则调用 LockSupport.park(this)方法将当前线程阻塞(该阻塞是不限时阻塞),阻塞后可以通过上一个节点线程调用解锁操作或者通过对当前线程调用 interrupt 方法进行唤醒(这里要注意,就算被前节点唤醒,也可能因为非公平锁的实现逻辑,在唤醒后,被其它线程获取锁),如果使用 interrupt 方法唤醒后将当前的中断状态暂时清除,再次自旋进行 第三步 逻辑
  4. 在成功上锁后,再将中断状态恢复
  5. 返回加锁成功

interrupt();在一个线程中调用另一个线程的 interrupt()方法,只会将那个线程设置成线程中断状态,不会真正的中断线程,而是在判断是否处于中断状态后,开发者自己定义如何处理处于中断的线程,一般情况会抛出 InterruptedException 异常

isInterrupted(boolean ClearInterrupted);返回是否处于中断状态,ClearInterrupted 为 true,则会清除中断状态,反之则保留中断状态

详尽原理

阅读详尽原理前,为加强理解,请对照源码阅读(本文末尾附带中文注释可运行源码下载链接)

​ 进入 lock 方法后,就先调用 cas 方法抢占锁(将 state 从 0 修改为 1),不管是否有线程在排队;如果修改成功,则更新当前线程持有者属性为当前线程,如果修改不成功,则调用 acquire 方法;

    final void lock() {// 直接使用 cas 原子方法, 如果 state 为 0 则修改为 1, 而不乖乖去 FIFO(先进先出)队列排队    
        if (compareAndSetState(0, 1))        
            // 如果上锁成功, 将锁持有者修改为当前线程对象,上锁成功               
            setExclusiveOwnerThread(Thread.currentThread());   
        else        
            // 如果失败, 则执行以下逻辑(包括判断是否是线程重入、再次尝试上锁、阻塞线程、被唤醒获取锁等逻辑)   
           acquire(1);
    }

​ 进入 acquire 方法后,再尝试获取锁,如果再获取失败则初始化代表当前线程的节点,加入到队列当中去并阻塞,直到被唤醒(这里之所以要尝试 2 次获取动作,是为了充分发挥非公平锁性能优势)

    public final void acquire(int arg) {
        // 1.tryAcquire: 再次尝试上锁,true:标识上锁成功;false:标识上锁失败
        if (!tryAcquire(arg) &&
                /*
                 2.addWaiter:将线程封装成节点, 放入 FIFO 队列中;
                 3.acquireQueued:自旋获取锁,如果再次尝试获取锁失败, 则阻塞线程;
                 等待队列中的前一个线程解锁后, 唤醒本线程,成功获取锁
                 */
                acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            // 将线程的状态置为中断(该代码的作用在 acquireQueued 里讲)selfInterrupt();}

​ 进入 1.tryAcquire 后,如果此时无线程持有锁,则再次尝试获取锁:1. 如果获取失败,进入队列;2. 否则返回获取成功;如果有线程持有锁,则判断是否是当前线程持有锁:1. 如果是,则累加重入数;2. 如果不是,进入队列

    protected final boolean tryAcquire(int acquires) {return nonfairTryAcquire(acquires);
    }

    final boolean nonfairTryAcquire(int acquires) {final Thread current = Thread.currentThread();
        // 如果当前没有线程持有锁, 则再次尝试获取锁, 获取锁成功后, 修改当前锁持有者,返回上锁成功
        int c = getState();
        if (c == 0) {if (compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);
                return true;
            }
        }
        // 当前锁的持有者正是当前线程,则累加重入数,返回上锁成功
        else if (current == getExclusiveOwnerThread()) {
            // 之所以使用不支持原子性的操作进行赋值,是因为只有当前拥有锁的线程才能修改这个 state,所以不会发生其他线程修改
            int nextc = c + acquires;
            if (nextc < 0) // overflow
                throw new Error("Maximum lock count exceeded");
            setState(nextc);
            return true;
        }
        return false;
    }

​ 进入 2.addWaiter 后,创建代表当前线程的节点并追加链表末尾

    /**
     * 描述: 初始化节点并追加链表末尾
     *
     * @param mode 标识当前线程的节点是独占模式还是共享模式, 对代码逻辑没有实际意义
     * @return 代表当前线程的节点
     */
    private Node addWaiter(Node mode) {
        // 初始化链表中的对象 NOde, 代表当前线程
        Node node = new Node(Thread.currentThread(), mode);
    
        Node pred = tail;
        // 如果链表已经初始化(最后一个节点不为空),则直接将当前节点放在尾节点的后面接口
        if (pred != null) {
            // 将 node 的 prev 属性赋值为之前链表的尾结点
            node.prev = pred;
            //  使用原子方法,tail 变量赋值成 node 节点
            //(注:这里只修改了记录了尾节点的变量, 并没有修改链表节点间的关联关系)if (compareAndSetTail(pred, node)) {//#1
                // 将之前链表的尾结点的 next 属性赋值为 node 节点(这里之所以没有使用 cas 原子方法是因为其它线程想要修改 t 的 next 属性都必须成功获取 t,而 t 是只有在 t 所代表节点是尾节点的那个时间点,成功执行 compareAndSetTail(t, node)的线程才能够拥有的)pred.next = node;
                return node;
            }
        }
        // 如果链表还没有初始化或者因为锁竞争激烈,#1 处的 compareAndSetTail 执行失败, 将会对链表进行初始化或者自旋直到 compareAndSetTail 执行成功
        enq(node);
        return node;
    }
    /**
     * 如果链表还没有初始化或者因为锁竞争激烈,#1 处的 compareAndSetTail 执行失败, 将会对链表进行初始化或者 自旋 直到 compareAndSetTail 执行成功
     */
    private Node enq(final Node node) {for (; ;) {
            Node t = tail;
        // 如果链表未初始化(尾节点变量为空),则需要初始化
        if (t == null) { // Must initialize #2
            // 初始化节点(不代表任何线程),作为 head 变量
            if (compareAndSetHead(new Node()))
                // 只有一个节点的链表, 头就是尾
                tail = head;
        } else {
            // 有两种情况会到达这:// 1、链表已经在其它线程初始化, 但是在本线程竞争执行 compareAndSetTail 失败了
            // 2、当前线程在执行 addWaiter 方法时, 链表还未初始化, 但当执行上面的 #2 代码时被其它线程初始化了
            // 将 node 的 prev 属性赋值为之前链表的尾结点
            node.prev = t;
            // 再次尝试修改 tail 变量为当前线程节点, 自旋尝试变更, 直到成功为止
            if (compareAndSetTail(t, node)) {
                // 将之前链表的尾结点的 next 属性赋值为 node 节点
                t.next = node;
                return t;
            }
        }
    }

​ 进入 3.acquireQueued 后,自旋 1 到 2 次尝试获取锁,如果再获取不到锁,则阻塞线程,直到被唤醒成功获取锁


  • 剩余 70% 的内容,添加本人微信,发送 49.9 元红包后可查看,若以上内容已经满足你的需求,赞赏一下,也可以哟!
  • 假如有技术盲点或者描述不清晰等问题,告知我,会及时修改
  • 付费阅读后,若发现源码讲解错误,告知我,情况属实,修改文章后,返现 10%/ 文章
  • 个人邮箱:ningww1@126.com,添加微信时,备注文字标题

退出移动版