深度解读 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%/文章
  • 个人邮箱:[email protected],添加微信时,备注文字标题
    深度解读 ReentrantLock底层源码
    深度解读 ReentrantLock底层源码

猜你喜欢

转载自blog.51cto.com/14528022/2437550