从ReentrantLock的角度来看AQS原理

2023-10-30

ReentrantLock

ReentrantLock作为java api层面的加锁方式,其性能比synchronized更好(synchronized进行优化后性能差不太多),灵活性更强

ReentrantLock synchronized
可重入 可重入
可尝试加锁 不能尝试加锁
加锁时支持打断 加锁时不支持打断
加锁时可设置超时时间 不能设置超时时间
关联多个条件队列 关联一个条件队列
需要手动释放锁 自动释放锁
公平&非公平 非公平

       // 获取锁
        reentrantLock.lock();
        // 获取锁时可相应中断
        reentrantLock.lockInterruptibly();
        // 尝试获取锁 获取成功返回 true
        boolean b = reentrantLock.tryLock();
        // 尝试获取锁,如果超过指定时间则超时, 获取成功返回 true
        boolean b1 = reentrantLock.tryLock(1, TimeUnit.SECONDS);

ReentrantLock 中有一个 Sync 抽象类继承自 AbstractQueuedSynchronizer
Sync 又有两个子类,分别是 FairSyncNonfairSync 公平锁的实现和非公平锁的实现

在这里插入图片描述
我们再看看 AQS

AQS就是java中的一个类 全名叫做 AbstractQueuedSynchronizer 内部维护了一个双向列表,列表中除头节点以外其他节点都是一个阻塞的线程,每个节点都是用它的一个内部类 Node 进行封装

Node中有下列这些字段

名称 类型 默认值 含义
SHARED Node new Node() 表示共享模式
EXCLUSIVE Node null 表示独占模式
CANCELLED int 1 waitStatus的状态,该状态表示此线程放弃竞争锁
SIGNAL int -1 waitStatus的状态,表示节点在等待队列中,节点线程等待唤醒
CONDITION int -2 waitStatus的状态., 该状态表示此节点在等待队列中,等待被唤醒
PROPAGATE int -3 只有共享模式下才会使用,这里先不解释
waitStatus int 0 去上面那几种状态
prev Node null 该节点的前驱节点
next Node null 该节点的后继节点
thread Thread null 该节点表示的线程

AQS中维护了一个 state 状态,该状态可表示是不是有线程获取了锁,具体的含义由子类进行实现
它还维护了分别指向头部和尾部的节点

AQS中的头节点是个哨兵节点,不代表任何线程

ReentrantLock 本身只实现了 Lock 接口
在这里插入图片描述

ReentrantLock 有两个构造方法,通过默认的构造方法创建的ReentrantLock对象默认使用的是非公平锁


public ReentrantLock() {
        sync = new NonfairSync();
    }
    public ReentrantLock(boolean fair) {
        sync = fair ? new FairSync() : new NonfairSync();
    }


ReentrantLock实现了 Lock 接口,所以要重写 lock() 方法


  public void lock() {
        sync.lock();
    }

它内部调用了 sync.lock() 如果我们使用默认的构造方法创建的 ReentrantLock 那么sync就是 NonfairSync

这里我们以非公平锁 NonfairSync 来讲解

假如我们有下列代码,有一个线程 t1 和 t2 ,我们先让 t1 进行启动并获取锁

public static void main(String[] args) throws InterruptedException {
        ReentrantLock reentrantLock = new ReentrantLock();

        // 获取锁
        reentrantLock.lock();
        
        new Thread(() -> {
            reentrantLock.lock();
            try {
                while (true) {}
            }finally {
                reentrantLock.unlock();
            }
        }, "t1").start();
    
        Thread.sleep(1000);
        new Thread(() -> {

            reentrantLock.lock();
            try {
                while (true) {}
            }finally {
                reentrantLock.unlock();
            }
        }, "t2").start();

    }

加锁流程

此时 t1 先执行 lock 方法


 final void lock() {
 			// 使用CAS的方式尝试将state更改为1
 			// 如果更改成功则代表锁获取成功
 			// t1 进来的时候 t2 并没有启动,那么t1肯定能获取到锁
            if (compareAndSetState(0, 1))
            	// 获取锁成功,将锁的持有者设置为当前线程
                setExclusiveOwnerThread(Thread.currentThread());
            else
            	// 获取锁失败走这个流程
                acquire(1);
        }

t2 启动,并进入 lock() 方法


 final void lock() {
 			// 由于 t1 已经获取到锁了,也就是将 state 修改成 1 了
 			// 那么此时t2通过 CAS的方式再次尝试获取锁将 失败
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
            	// 获取锁失败走这个流程
                acquire(1);
        }

t2 进入该acquire(1) 方法
这个方法位于 AQS中,并且是final修饰的,不让子类进行重写
事实上基于AQS实现的独占锁在获取锁时一般都会调用这个方法

	
	// 这个方法在 Sync中实现
	final boolean nonfairTryAcquire(int acquires) {
            final Thread current = Thread.currentThread();
            // 这时候 t1 是持有锁的,这个state已经被t1修改成 1 了
            // 不过如果此时 t1 恰巧释放了锁那么 t2 state就等于0了,现在不考虑这种情况
            int c = getState();
            // t2 执行到这里的时候很明显该条件不成立
            
            if (c == 0) {
                if (compareAndSetState(0, acquires)) {
                    setExclusiveOwnerThread(current);
                    return true;
                }
            }
            // 判断当前持有锁的线程是不是就是自己,很明显也不是
            else if (current == getExclusiveOwnerThread()) {
                int nextc = c + acquires;
                if (nextc < 0) // overflow
                    throw new Error("Maximum lock count exceeded");
                setState(nextc);
                return true;
            }
            // 获取锁失败
            return false;
    }	

	// 非公平锁中的实现
    protected final boolean tryAcquire(int acquires) {
        return nonfairTryAcquire(acquires);
    }	
	
	// arg 这个 参数是在实现可重入锁时使用的,暂时忽略
   public final void acquire(int arg) {
   		//  首先会执行 tryAcquire(1) 该方法是要子类重写的,默认抛出异常
   		// 其实tryAcquire(1) 就是尝试获取所,如果获取成功了则返回true
   		// 否则会执行  acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
    }

由于 t1 此时持有锁,t2再去获取时就是获取锁失败

那么会执行这句代码 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

先来看看 addWaiter 干了什么事


	// 刚刚调用那里传过来的mode为 : Node.EXCLUSIVE
	// 	表示独占锁模式
   private Node addWaiter(Node mode) {
   		// 将当前线程封装成一个 mode
        Node node = new Node(Thread.currentThread(), mode);
        
        Node pred = tail;
        // 由于t2是第一个获取锁失败的线程,此时队列是空的
        //  该条件不成立
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        // 将t2 封装的 Node返回了出去
        return node;
    }
    
    private Node enq(final Node node) {
    	// 注意这里是个死循环
        for (;;) {
            Node t = tail;
            // 如果此时队列是空的那么将初始化这个队列
            //  经过第一次循环后队列不为空了
            if (t == null) {
                if (compareAndSetHead(new Node()))
                	// 并把头部的节点也指向了尾部
                	// 然后要进行下一次循环了
                    tail = head;
            } else {
            	//  第二次循环会进入到这里
            	// 把 当前的尾部节点 t 作为 我们传过来的那个节点的前驱节点
            	// 我们传过来的节点就是线程 t2 封装的那个
                node.prev = t;
                // 然后将 t2 封装的那个节点设置成尾节点
                if (compareAndSetTail(t, node)) {
                	// 将之前尾节点的下一个节点执行 t2
                    t.next = node;
                    // 返回的是我们当前要插入的节点也就是t2的前驱节点
                    // 也就是上一个尾节点
                    return t;
                }
            }
        }
    }

执行完 addWaiter 之后列表是这样的
现在所有的waitStatus都等于0
在这里插入图片描述

接下来看看 acquireQueued方法的执行


// node 参数是 addWaiter(Node.EXCLUSIVE), arg) 返回的
//  返回的其实就是线程t2封装的那个node
//  arg 还是 1
final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
        	// 这里是标记线程在获取锁阻塞期间是否被打断过
            boolean interrupted = false;
            // 这里死循环
            for (;;) {
            	// node 就是 t2,现在位于队列中的第二个,他的前驱节点是头节点
                final Node p = node.predecessor();
                // 判断t2的前驱节点是不是头节点 也就是判断当前这个node是不是在队列中位于第二个节点的位置
                // tryAcquire(arg) 就是尝试去获取一下锁,如果获取成功了就返回true
                // 但是现在 t1 还未释放锁 所以仍然是获取锁失败的
                if (p == head && tryAcquire(arg)) {
                    setHead(node);
                    p.next = null; 
                    failed = false;
                    return interrupted;
                }
                // shouldParkAfterFailedAcquire() 表示获取锁失败了是否需要阻塞住,如果要阻塞住返回true
                //  parkAndCheckInterrupt() 就是阻塞住当前线程
                //  当第一次执行到这里会返回false
                //  第二次执行时会返回true 然后就会阻塞住
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }


// pred 就是 node 的前驱节点,也就是头节点
// node 就是 t2 封装的节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
		// 拿到头节点的watiStatus
        int ws = pred.waitStatus;
        // 如果等于 -1 就代表 需要阻塞住
        if (ws == Node.SIGNAL)
            return true;
        // 大于 0 则代表放弃竞争锁
        if (ws > 0) {
            do {
            	// 循环向前查找取消节点,把取消节点从队列中剔除
                node.prev = pred = pred.prev;
            } while (pred.waitStatus > 0);
            pred.next = node;
        } else {
        	// 将头节点的 waitStatus设置为 -1
            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
        }
        // 返回false 不阻塞
        return false;
    }

 // 会将当前线程阻塞住,如果是被打断的就会清除打断标记
 // 因为 LockSupport.prk() 不会清除打断标记,需要手动调用 Thread.interrupted() 
 // 如果不清除打断标记的话,下一次LockSupport.park() 会阻塞不了
  private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

    


执行完上列方法会 线程的waitStatus会发生改变,但是队尾的waitStatus没有

在这里插入图片描述

解锁流程

假如此时 t1 要释放锁了


	 public void unlock() {
	        sync.release(1);
	    }


    public final boolean release(int arg) {
    	// 尝试释放锁
        if (tryRelease(arg)) {
            Node h = head;
            // 释放锁成功后需要有没有后继节点
            // 如果有则需要唤醒后继节点
            if (h != null && h.waitStatus != 0)
                unparkSuccessor(h);
            return true;
        }
        return false;
    }


	protected final boolean tryRelease(int releases) {
			// 因为线程一 只加了一次锁即没有重入
			// 所以state等于1 releases 这时也是1  1-1=0
            int c = getState() - releases;
            // 判断当前当前线程是否是锁的持有者
            // 如果你都不持有锁肯定不会让你释放
            if (Thread.currentThread() != getExclusiveOwnerThread())
                throw new IllegalMonitorStateException();
            boolean free = false;
            // 等于0就代表释放成功了
            if (c == 0) {
                free = true;
                setExclusiveOwnerThread(null);
            }
            // 因为只能有一个线程持有锁,所以释放锁的时候只会有一个线程执行到这里所以不需要通过CAS的方法修改state
            setState(c);
            return free;
        }

	private void unparkSuccessor(Node node) {
    
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        Node s = node.next;
        if (s == null || s.waitStatus > 0) {
            s = null;
            // 如果没有 下一个节点或者下一个节点已经被取消了
            // 那么判断是否有尾节点并且尾节点不是当前节点
            //  如果成立则从尾部向前找直到找到waitStatus小于等于0的节点为止
            for (Node t = tail; t != null && t != node; t = t.prev)
                if (t.waitStatus <= 0)
                    s = t;
        }
        if (s != null)
        	// 唤醒后继节点 这里将t2唤醒了
            LockSupport.unpark(s.thread);
    }

再来看看将t2唤醒之后 t2获取锁的流程



final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            for (;;) {
                final Node p = node.predecessor();
                //  这时候 t1已经释放锁了
                // 并且t2 的前驱节点就是头节点
                if (p == head && tryAcquire(arg)) {
                	// 获取锁成功之后会将当前t2的这个节点设置为头节点
                    setHead(node);
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                //  parkAndCheckInterrupt() t2 阻塞在这个方法里
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
    }

其实整个AQS的加锁流程还是相对简单的,只要搞懂了其中涉及到的一些概念

在这里插入图片描述

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

从ReentrantLock的角度来看AQS原理 的相关文章

随机推荐

  • PyQt+moviepy音视频剪辑实战文章目录

    前往老猿Python博文目录 本专栏为moviepy音视频剪辑合成相关内容介绍的免费专栏 对应的收费专栏为 moviepy音视频开发专栏 一 moviepy基础能力系统介绍 本部分主要以类为单位介绍moviepy相关知识 相关内容主要来自m
  • Linux——gcc和g++的区别和应用

    Windows中我们常用vs来编译编写好的C和C 代码 vs把编辑器 编译器和调试器等工具都集成在这一款工具中 在Linux下我们能用什么工具来编译所编写好的代码呢 其实Linux下这样的工具有很多 但我们只介绍两款常用的工具 它们分别是g
  • C#难点语法讲解之委托---从应用需求开始讲解

    一 委托的定义 委托 Delegate 是存有对某个方法的引用的一种引用类型变量 引用可在运行时被改变 简单解释 变量好控制 方法不好控制 委托可以把方法变成变量 二 例子解释定义 如果我们有一个数组 里面有10个数据 数组就是变量的一种
  • 逻辑架构图、系统架构图、技术架构图

    逻辑架构图 系统架构图和技术架构图是软件系统中常见的三种不同类型的架构图 用于描述系统的不同方面和层次 1 逻辑架构图 Logical Architecture Diagram 逻辑架构图侧重于系统的功能和模块之间的关系 描述了软件系统的逻
  • 前后端分离 单点登录SSO 纯前端实现单点登录SSO

    示例代码地址 GitHub 以前涉及到单点登录 都是用CAS解决的 不过体验不是很好 但是也确确实实实现了单点登录 利用了session会话 后来我到了公司的架构部 部门决定重新定位前端技术路线 我大胆地采用了前后端分离的方式 让前端工程化
  • 关于dataframe中的警告A value is trying to be set on a copy of a slice from a DataFrame问题解决

    在pandas处理dataframe时新增一列数据时发现这里给出警告 但不影响程序的正常运行 这个警告意思是pandas在使用de 列名 赋值时会返回一个试图而不是原始的dataframe 这种情况下对视图进行修改可能无法生效 针对这个问题
  • window10下半自动标注

    前言 我看了一眼我们项目的标签很多不行 得重新标注 想借助一下自动标注或者半自动标注救救一万多近两万张照片 方法1 easyDL智能标注 1 借助百度easyDL进行标注 选择EasyDL图像 gt 物体检测 我是做图像识别所以选择Easy
  • springboot JPA Connection is read-only. Queries leading to data modification are not allowed

    环境 springboot jpa 数据库 阿里云mysql数据库 数据库连接字符串 问题描述 在自己部署的mysql数据库可以正常访问 没有问题 但是切换到阿里mysql数据库上出现JPA Connection is read only
  • RenRen-Fast-Vue 安装

    node版本 npm版本 v10 24 1 6 14 12 下载代码 https gitee com renrenio renren fast vue git 设置代理 npm config set registry http regist
  • 在clion打断点,debug的时候没有按照顺序进行的情况怎么办?该文章可以提供几个思路

    在使用CLion进行调试时 如果断点无法按照预设的位置停止 通常是由于以下原因之一导致的 编译器优化 编译器可能对代码进行了优化 使得某些代码没有实际执行 因此断点无法触发 可以尝试关闭编译器优化选项 如 O2 重新编译代码并运行调试 代码
  • Unity 代码命名规范

    1 类 class 结构 struct 枚举 enum 标签 Attribute 名 静态 私有 保护 公有 单词首字母大写 比如 Main CharacterController 2 接口 interface 名 静态 私有 保护 公有
  • 鸟哥的linux私房菜一书

    第0章 计算机概论 计算机的容量单位 速度单位 CPU的指令周期使用MHz或者GHz为单位 Hz就是秒分之一 网络传输使用的bit为单位 Mbps Mbits per second 就是每秒多少Mbit cpu是中央处理器 有控制器和运算器
  • C# FTP 遍历所有文件包括子目录文件下载

    文章修改2011 12 3号 char seperator n 现改为 char seperator n 今天用到下载FTP里所有文件和目录的程序 网上找了很久没找到 没办法只好自己写了 代码写得不太优化希望有兴趣的朋友可以研究优化一下性能
  • 自己搭建和部署禅道测试环境

    1 本人使用的是Windows一键安装 地址如下https www zentao net download 80138 html 中文版 下载完成之后 双击解压到根目录C 或D 进入 2 双击运行start exe 选择启用禅道 如果出现如
  • 常见绕过姿势小结

    一 SQL注入 假设关键词被过滤掉 我们尝试以下绕过方法 1 大小写绕过 id 1 AND 1 1 id 1 anD 1 2 查看是否存在注入 id 1 And 1 1 id 1 aNd
  • vue 点击图标切换图标_Vue的动画SweetAlert图标

    vue 点击图标切换图标 Vue的动画SweetAlert图标 Animated SweetAlert Icons for Vue A clean and simple Vue wrapper for SweetAlert s fantas
  • Ubuntu如何把主文件夹的中文设置成英文

    打开终端 输入命令 export LANG en US 接着输入更新命令 xdg user dirs gtk update 然后输入命令 export LANG zh CN 最后输入重启命令 sudo reboot 重启之后就可以看到主文件
  • Lanbda表达式详解

    lambda 表达式最大的用处就是简写代码 在需要降低代码之间的耦合性和侵入性较多使用匿名内部类来解决这一问题 我们使用lambda表达式可以将匿名内部类最大程度的简写 除此之外lambda表达式的作用就是让你的代码变得更加优雅 文件过滤器
  • log4j.properties log4j.xml 路径问题

    我的博客现在已经搬家到极客导航的博客模块中链接地址是 极客博客 顺便做了个程序员资源导航站www gogeeks cn 有兴趣的朋友不妨看一看有哪些还没了解到的IT方面的东西 比如框架 书籍 教程 开源社区等等吧 自动加载配置文件 1 如果
  • 从ReentrantLock的角度来看AQS原理

    ReentrantLock ReentrantLock作为java api层面的加锁方式 其性能比synchronized更好 synchronized进行优化后性能差不太多 灵活性更强 ReentrantLock synchronized