JUC(二)- ReentrantLock图解流程

ReentrantLock 的图解运行流程 (加锁、释放锁流程)

其他相关文章

JUC(一)-ReentrantLock源码分析


0、ReentrantLock , 了解大概的 概述、原理、流程:

ReentrantLock 是基于AQS实现的 可重入的互斥锁 , AQS(AbstractQueuedSynchronizer抽象类 - 抽象队列同步器)

AQS中维护了一个双向的链表 , 链表中每个元素都是一个 AQS中的Node节点对象(Node节点) , 然后AQS中维护了一个 int类型且是 volatile 修饰的 变量 state

这个state在 ReentrantLock中(state在不同类型的锁中功能不同,如读写锁)
如果 state == 0 则表示当前ReentrantLock还未被线程持有
如果 state == 1 则说明已经有线程持有了当前锁资源
如果 state > 1 说明持有锁的线程已经进行了锁重入

ReentrantLock的实现中 , 当前锁资源只能够被一个线程持有(这点和synchronized相同-互斥锁) , ReentrantLock就是通过state的值来判断当前锁资源是否已经被持有
如果已经被持有,那么就需要加入到AQS维护的队列中去排队了 , 排队时就会执行一系列逻辑使当前线程挂起 , 直到当前锁资源释放到轮到唤醒自己时线程才会继续工作

AQS刚开始的结构 , 其head和tail都是指向null的 , state为0
请添加图片描述



/**
 *   AQS 内部Node类 核心属性
 */
static final class Node {
    // 共享节点 , 共享的在ReentrantLock中不涉及 , 他是互斥锁
    static final Node SHARED = new Node();
    // 互斥节点 
    static final Node EXCLUSIVE = null;
    // 当前节点waitStatus的状态之一 --> 取消状态
    static final int CANCELLED = 1;
    // 当前节点waitStatus的状态之一 --> 信号状态 , 如果当前节点的waitStatus为-1 , 那么表示后继节点处于挂起状态 
    static final int SIGNAL = -1;
    // 当前涉及不到
    static final int CONDITION = -2;
    // 当前涉及不到
    static final int PROPAGATE = -3;
    // 当前节点的状态 , 新建时默认是 0
    volatile int waitStatus;
    // 当前节点的前驱节点
    volatile Node prev;
    // 当前节点的后继节点
    volatile Node next;
    // 当前节点存储的线程对象
    volatile Thread thread;
}

一、场景:

1.1 场景描述

现在模拟有 四个线程在使用 Lock锁 进行加锁执行业务逻辑 , 然后分析每个线程执行加锁流程图解
线程T1 : 目的是为了一直占用着锁资源
线程T2 : 普通的等待节点
线程T3 : 使用Lock的tryAcquire(time,unit) 方法让其挂起10s后尝试获取锁资源
由于T1线程一直持有着锁资源 , 所以T3线程最终会执行cancelAcquire方法
线程T4 : 普通的等待节点 , 这个线程用来观察T3取消后时 AQS内的状况

1.2 场景源代码
public class ReentrantLockBlog{
    private static Lock testLock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {

        mainCode();

    }

    /**
     * 创建四个模拟线程
     *
     *  T1 : 让第一个线程一直占用着锁资源
     *  T2 : 普通的等待节点 让其一直等待
     *  T3 : 使用tryAcquire(time,unit) 让其挂起10s后尝试获取锁资源 , 由于T1一直占用锁资源 那么T3线程最后会执行 cancelAcquire 取消节点
     *  T4 : 也是一个普通节点 , 为了在Debugger时看到 cancelAcquire的节点 
     *  T5 : 普通节点
     */
        public static void mainCode() throws InterruptedException {
        // T1

        Thread t1 = new Thread(() -> {
            testLock.lock();
            try {
                handleProcess();
                sleepThread(TimeUnit.SECONDS , 20);
            }finally {
                testLock.unlock();
            }
        },"T1");
        t1.start();
        sleepThread(TimeUnit.MILLISECONDS , 10);
        // T2

        Thread t2 = new Thread(() -> {
            testLock.lock();
            try {
                handleProcess();
            }finally {
                testLock.unlock();
            }
        }, "T2");
        t2.start();
        sleepThread(TimeUnit.MILLISECONDS , 10);
        // T3

        Thread t3 = new Thread(() -> {
            boolean tryLock = false;
            try {
                if (testLock.tryLock(10 , TimeUnit.SECONDS)) {
                    tryLock = true;
                    handleProcess();
                }else {
                    System.out.println("tryLock(time,unit) 方法没有拿到锁资源 并且没有剩余休眠的时间 自动唤醒");
                }

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (tryLock){
                    testLock.unlock();
                }
            }
        } , "T3");
        t3.start();

        // 让T3节点执行取消方法 时加入新节点T4 , 保证T3节点不是tail否则将直接移除了
        sleepThread(TimeUnit.SECONDS , 5);

        // T4

        Thread t4 = new Thread(() -> {
            // 在 testLock.lock(); 这行打Debug即可看到此时的AQS队列情况
            testLock.lock();
            try {
                handleProcess();
            }finally {
                testLock.unlock();
            }
        }, "T4");
        t4.start();

        // T5

        Thread t5 = new Thread(() -> {
            testLock.lock();
            try {
                handleProcess();
            }finally {
                testLock.unlock();
            }
        }, "T5");
        t5.start();

    }


    private static void sleepThread(TimeUnit unit , long sleepLong){
        try {
            unit.sleep(sleepLong);
        } catch (InterruptedException e) {
            System.out.println(Thread.currentThread().getName() + "抛出中断异常!");
        }
    }

    /**
     * 处理业务代码
     */
    private static void handleProcess(){
        System.out.println(Thread.currentThread().getName() + "拿到了锁资源!");
        System.out.println("处理业务代码!");
        System.out.println("业务代码处理完成!");
    }
}

/* 
        输出结果: 
        
        T1拿到了锁资源!
        处理业务代码!
        业务代码处理完成!
        tryLock(time,unit) 方法没有拿到锁资源 并且没有剩余休眠的时间 自动唤醒
        
        (T1一直在睡眠所以程序不会结束 要一直等待T1睡醒)
 */

二、AQS中的关键点

2.1 hasQueuedPredecessors 方法含义
/** 
 *  该方法就是判断 当前时刻 AQS队列中 , 是否有线程在排队 , 有在排队则返回true 
 *  这个方法最重要的就是配合 当前线程是否具有抢占锁资源资格 ,
 *  
 *  **从代码角度来看** :
 *      1. h != t 
 *         说明 head和tail已经初始化 , 如果没有初始化则说明当前AQS队列肯定没有排队的 返回 true
 *      2. (s = h.next) == null || s.thread != Thread.currentThread()) 
 *         说明不存在head的下一个节点 或者 下一个节点不是当前线程 那么就返回 true
 *  **配合tryAcquire来看** :
 *      1表示没有线程排队则直接返回false让tryAcquire里可以进行CAS操作尝试获取锁资源
 *      2表示队列已经初始化了 但是 head的next节点(第一个有效节点) 是当前线程 则返回false, 那么也可以尝试获取锁资源
 */
public final boolean hasQueuedPredecessors() {
    Node t = tail;
    Node h = head;
    Node s;
    return h != t &&
        ((s = h.next) == null || s.thread != Thread.currentThread());
}

2.2 cancelAcquire 取消节点方法 (只针对当前场景代码解释)

这里不对cancelAcquire方法做详解 , 详细源码解释看 其他博客

1. 真正将取消节点移除AQS中的情况如下

1.当取消的是tail节点时 : 此时可以认为node直接被清理出AQS中了


2.当取消的是head的next节点时 : 则唤醒取消节点的next节点即 head的next的next节点 , 此时这个唤醒的节点就成为了 head , 使用setHead方法将 取消的节点移除AQS中


3.取消的是AQS的中间节点 : 个人认为是只有当 prev是取消节点 的这个节点拿到锁资源 同样是调用setHead方法后 才会将其prev取消节点真正移除出AQS队列



至于 shouldParkAfterFailedAcquire 方法真正起作用的地方 个人觉得条件会比较苛刻(少见)
因为 如果取消的是中间的节点并且还需要 取消节点的后继节点刚刚加入到AQS队列(addWaiter方法) 并且还未调用shouldParkAfterFailedAcquire时刻 , 等取消的节点操作完毕后 shouldParkAfterFailedAcquire才执行这样才能
真正意义的把 当前取消的节点真正移除出去

2. 总结: 真正移除取消节点的方式

1.shouldParkAfterFailedAcquire 在cancelAcquire方法后才执行 , 并且保证取消的节点不是tail




2.prev是取消节点 的节点拿到锁资源后 setHead 方法会移除取消的节点

真正取消节点是在 某个节点唤醒时 其尝试获取锁资源成功然后调用setHead时 才会真正取消掉节点

private void setHead(Node node) {
    head = node;
    node.thread = null;
    // 就是这里 , 会把当前节点 prev关联的已经处于取消状态的节点真正移除AQS队列中
    node.prev = null;
}
private void cancelAcquire(Node node) {
    if (node == null)
        return;

    node.thread = null;
    
    Node pred = node.prev;
    /**
     * 1.这个while循环 将当前需要进行取消的节点node 节点的prev指针指向前驱节点状态不是取消的节点(离node最近的)
     *   在当前代码的情景下 node的前一个节点不是取消状态的 所以不会执行这个
     */
    while (pred.waitStatus > 0)
        node.prev = pred = pred.prev;
    
    Node predNext = pred.next;
    
    node.waitStatus = Node.CANCELLED;
    
    if (node == tail && compareAndSetTail(node, pred)) {
        // 此时取消的节点已经没有任何引用指向node 就等待GC把node清除 所以认为是直接移除出AQS队列了
        compareAndSetNext(pred, predNext, null);
    }
    else {
        int ws;
        // 由于T3是在AQS的中间位置,而不是两边所以走这个if逻辑
        if (pred != head &&
            ((ws = pred.waitStatus) == Node.SIGNAL ||
             (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
            pred.thread != null) {
            
            Node next = node.next;
            if (next != null && next.waitStatus <= 0)
                // 2. 将node的前驱节点(T2) 的next指针指向当前node的next节点(T4) , 此时 从head往tail方向将不可达当前node(T3)了
                // 但是此时 取消节点node的prev指针还是可以遍历到的 从tail到head方向 
                // 因此执行了cancelAcquire方法并不会立即将node节点清理出AQS队列中
                compareAndSetNext(pred, predNext, next);
        } else {
            unparkSuccessor(node);
        }
        // 3. 取消节点node的next指针指向自己
        node.next = node; // help GC
    }
}

三、加锁

前言: 先看一下场景源代码 知道大概背景和意图 场景源代码

逐步分析:

  • 1. 创建Lock锁对象时: private static Lock testLock = new ReentrantLock();

    • 此时 AQS 内部的状态就是这样的 请添加图片描述
  • 2. 当T1线程开始执行任务时 :

    • 此时 AQS 中 state 属性是0 , 那么T1线程会直接去尝试获取锁资源, 拿到锁资源之后就会一直持有着锁资源直到睡眠时间结束
    • 创建完成Lock锁之后 如果仅有一个线程去获取锁资源 那么AQS队列是不会做任何变化的 不会初始化更不会去排队 一直都是 刚创建Lock锁时的状态图 (公平锁、非公平都是这样的)
  • 3. 当T2线程开始执行任务时 :

    • 此时当前线程使用 testLock.lock() , 发现AQS中state属性不是0 , 并且尝试获取锁资源失败 , 那么就会把当前节点加入到AQS队列中,
      addWaiter方法内有一个enq方法,这个方法是进行初始化AQS队列生成head伪节点和 保证本次CAS替换tail成功的目的 , 所以会先进行初始化AQS队列
      然后就会把当前节点里边的线程挂起 并把前驱节点的 waitStats设置为-1
      -请添加图片描述
  • 4. 当T3线程开始执行任务时 :

    • tryLock(time,unit) 方法作用是将当前线程封装为Node节点放入AQS队列中挂起指定时间后 尝试获取锁资源如果获取不到就取消这个节点
    • 明显的T1一直占用着锁资源 T3线程挂起10s也拿不到锁资源 所以就会取消掉T3这个节点 , 取消在T4和T5线程都加入再说
    • 请添加图片描述
  • 5. T4、T5获取锁资源同样的 加入全部节点后的状态

    • 在这里插入图片描述
  • 6. 在5加入全部节点的基础上分析T3取消节点

    • 由于T1线程一直占用着锁资源 T3启动后加入到AQS队列中等待10s后一定是拿不到所资源的
      所以T3线程在自动唤醒之后会执行cancelAcquire(Node node) 方法

    • 查看 cancelAcquire部分代码 的部分代码 , T3线程所在节点为 AQS队列中间部分(非两边)

    • T3线程经过取消方法后的AQS图

    • 请添加图片描述

    • 通过Debug线程T5里边可以看到

      • 从head往tail 找不到T3节点
        请添加图片描述

      • 从tail往head 可以找到T3节点
        请添加图片描述

      • 当前边所有节点都唤醒、然后释放所资源之后 到T3成为head的next节点时 如果这时T4才加进来那么执行 shouldParkAfterFailedAcquire 才会将T3节点完全移除AQS队列

四、释放锁

前言: 还是按照此场景代码来分析释放锁流程步骤

  • tryRelease 代码
protected final boolean tryRelease(int releases) {
    int c = getState() - releases;
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {
        free = true;
        setExclusiveOwnerThread(null);
    }
    setState(c);
    return free;
}

逐步分析 : 不要忘记此时T3已经是取消的节点了

  • 当T1休眠20min后 调用 testLock.unlock()方法

    • unlock方法在这里不再详解
    • 请添加图片描述
  • 当T2释放锁资源时的流程图

    • 请添加图片描述
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值