AQS是AbstractQueuedSynchronizer的简称,juc包下锁的实现,基本上需要借助于AQS的功能,如下图所示:

通过继承结构,可以看到,常用的可重入锁ReentrantLock,以及同步辅助工具类CountDownLatch、Semaphore,都用到了AQS。

首先要明白:锁分为独占锁和共享锁,独占锁又分为公平锁和非公平锁。

  1. 独占锁:所谓独占锁,就是某一时刻,该锁只能被一个线程占有的
  2. 共享锁:共享锁则是某一时刻,该锁可以被多个线程同时占有
  3. 公平锁:获取锁的顺序,和请求锁的顺序是一致的
  4. 非公平锁:获取锁的顺序,和请求锁的顺序不一致,存在抢占机制。

下面将通过ReentrantLock中的代码,以公平锁为例,聊聊AQS获取锁的过程。先看ReentrantLock类的结构,如下图所示:

可以发现,ReentrantLock中,有3个内部类:Sync、FairSync(公平锁的实现)和NonfairSync(非公平锁的实现),并且FairSync和NonfairSync继承于Sync,而Sync继承了AQS:

以FairSync实现为例,我们加锁一般是通过lock()方法,该方法也是获取锁的顶级入口,看其实现:

        final void lock() {acquire(1);}

可以看到,它调用了acquire()方法:

    /*** Acquires in exclusive mode, ignoring interrupts.  Implemented* by invoking at least once {@link #tryAcquire},* returning on success.  Otherwise the thread is queued, possibly* repeatedly blocking and unblocking, invoking {@link* #tryAcquire} until success.  This method can be used* to implement method {@link Lock#lock}.** @param arg the acquire argument.  This value is conveyed to*        {@link #tryAcquire} but is otherwise uninterpreted and*        can represent anything you like.*/    public final void acquire(int arg) {if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))selfInterrupt();}

根据注释可知:

  1. acquire()方法,获取的是独占锁,并且忽略中断
  2. tryAcquire()方法,尝试获取锁,至少会执行一次
  3. 如果tryAcquire()方法尝试获取锁失败,则通过addWaiter()方法,将当前线程放入同步队列
  4. acquireQueued()方法,可能会重复经历阻塞、唤醒、尝试获取锁的过程,直到成功获取锁

接下来,主要是分析这3个方法:

  1. tryAcquire(int arg)
  2. addWaiter(Node node)
  3. acquireQueued(addWaiter(final Node node, int arg)

1. 尝试获取锁:tryAcquire(int arg)

        protected final boolean tryAcquire(int acquires) {// 当前线程final Thread current = Thread.currentThread();// 同步锁状态int c = getState();// 如果状态为0,则表示没有线程持有该锁if (c == 0) {// 队列的前面没有等待锁的线程,通过CAS设置锁的状态为acquires,也就是1// 并且标记当前线程是占有该锁的线程if (!hasQueuedPredecessors() &&compareAndSetState(0, acquires)) {setExclusiveOwnerThread(current);return true;}}// 如果占有锁的线程是当前线程else if (current == getExclusiveOwnerThread()) {// next是锁将要更新的状态,可以看到,值是累加的,这也就说明了该锁是可重入锁int nextc = c + acquires;if (nextc < 0)throw new Error("Maximum lock count exceeded");// 设置锁的状态setState(nextc);return true;}return false;}

注释在代码中了,其流程大概如下:

  1. 如果同步锁的状态state为0,则并且队列种没有比当前线程等待更久的线程,则尝试获取锁
  2. 如果当前线程是占有该锁的线程,不需要重新获取锁,只需要更新锁的状态(累加1),实现了可重入

2. 将线程添加到同步队列:addWaiter(Node node)

    /*** Creates and enqueues node for current thread and given mode.** @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared* @return the new node*/private Node addWaiter(Node mode) {// 以当前线程和给定的模式(独占式)创建节点Node node = new Node(Thread.currentThread(), mode);Node pred = tail;// 如果尾节点不为空(也就是链表不为空),则将当前节点插入到链表的末尾,成为新的尾节点if (pred != null) {node.prev = pred;if (compareAndSetTail(pred, node)) {pred.next = node;return node;}}// 通过enq()方法,添加到链表的末尾enq(node);return node;}

通过注释以及代码,可以知道:

  1. addWaiter()方法的作用就是将当前线程包装成节点,并添加到链表的末尾
  2. 添加到链表的末尾的两条路径:(1)如果链表不为空,则通过尾节点链接 (2)如果链表为空则通过enq()方法添加

看下enq()方法:

    private Node enq(final Node node) {for (;;) {Node t = tail;// 链表为空,则先初始化if (t == null) { // Must initialize// 从这里可以看出,使用空节点作为头节点,说明这是一个带头节点的链表if (compareAndSetHead(new Node()))tail = head;} else {// 将新节点链接成尾节点node.prev = t;if (compareAndSetTail(t, node)) {t.next = node;return t;}}}}

可以看到,enq()方法通过死循环,也就是自旋的方式来设置的

3. 获取锁:acquireQueued()

    final boolean acquireQueued(final Node node, int arg) {// 获取锁是否失败boolean failed = true;try {// 获取锁的过程种,是否产生了中断boolean interrupted = false;for (;;) {// 当前线程所在节点的前一个节点final Node p = node.predecessor();// 如果前一个节点是头节点(这里也体现了其公平性,按照进入队列的顺序获取锁),就尝试获取锁// 如果获取锁成功,则设置当前线程所在节点为头节点if (p == head && tryAcquire(arg)) {setHead(node);p.next = null; // help GCfailed = false;return interrupted;}// 判断获取锁失败后是否需要阻塞当前线程if (shouldParkAfterFailedAcquire(p, node) &&parkAndCheckInterrupt())interrupted = true;}} finally {// 如果获取锁失败,则取消获取if (failed)cancelAcquire(node);}}

看下它是如何判断当前线程是否需要阻塞的:

    private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {// 前一个节点的等待状态int ws = pred.waitStatus;if (ws == Node.SIGNAL)/** This node has already set status asking a release* to signal it, so it can safely park.*/return true;if (ws > 0) {/** Predecessor was cancelled. Skip over predecessors and* indicate retry.*/// 向前找,直到找到一个waitStatus不是cancelled的节点,作为当前节点的前一个节点do {node.prev = pred = pred.prev;} while (pred.waitStatus > 0);pred.next = node;} else {/** waitStatus must be 0 or PROPAGATE.  Indicate that we* need a signal, but don't park yet.  Caller will need to* retry to make sure it cannot acquire before parking.*/// waitStatus的值为0(初始状态)或者是PROPAGATE// 设置waitStatus的值为SIGNALcompareAndSetWaitStatus(pred, ws, Node.SIGNAL);}return false;}

可以看出,只有当前一个节点的状态为SIGNAL时,才会阻塞当前节点所包装的线程。

总结一下获取锁的过程:

  1. 通过tryAcquire(int arg),尝试获取锁,只尝试了一次,所以有可能没有获取成功
  2. 如果tryAcquire(int arg)没有获取成功,则通过 addWaiter(Node node)将线程封装成Node节点,链接到链表的末尾
  3. 通过acquireQueued(addWaiter(final Node node, int arg)的死循环,不断尝试获取锁,直到成功

多线程系列学习:AQS(一)获取锁相关推荐

  1. 解决多线程安全问题-无非两个方法synchronized和lock 具体原理以及如何 获取锁AQS算法 (百度-美团)

    解决多线程安全问题-无非两个方法synchronized和lock 具体原理以及如何 获取锁AQS算法 (百度-美团) 参考文章: (1)解决多线程安全问题-无非两个方法synchronized和lo ...

  2. 多线程-使用大全 基础使用 / 锁 / 线程池 / 原子类 / 并发包 / CAS / AQS (2022版)

    一.多线程描述 1.什么是cpu CPU的中文名称是中央处理器,是进行逻辑运算用的主要由运算器.控制器.寄存器三部分组成, 运算器:从字面意思看就是运算就是起着运算的作用, 控制器:就是负责发出cpu ...

  3. Java多线程学习十六:读写锁 ReadWriteLock 获取锁有哪些规则

    读写锁 ReadWriteLock 获取锁有哪些规则呢? 在没有读写锁之前,我们假设使用普通的 ReentrantLock,那么虽然我们保证了线程安全,但是也浪费了一定的资源,因为如果多个读操作同时进 ...

  4. 多线程:AQS的一些心得

    AQS作为JUC并发组件实现的核心.全称是AbstructQueuedSynchronizer,也就是同步队列器. 其内部实现主要的是一个state状态标识和基于FIFO一个同步队列. 这个状态标识表 ...

  5. 多线程:AQS源码分析

    AQS 源码分析 概述 Java的内置锁一直都是备受争议的,在JDK 1.6之前,synchronized这个重量级锁其性能一直都是较为低下,虽然在1.6后,进行大量的锁优化策略,但是与Lock相比s ...

  6. 深入剖析基于并发AQS的(独占锁)重入锁(ReetrantLock)及其Condition实现原理

    [版权申明]未经博主同意,谢绝转载!(请尊重原创,博主保留追究权) http://blog.csdn.net/javazejian/article/details/75043422 出自[zejian ...

  7. Java多线程系列--AQS的原理

    原文网址:Java多线程系列--AQS的原理_IT利刃出鞘的博客-CSDN博客 简介 本文介绍Java中的AQS的原理. Java的AQS是JDK自带的锁机制,是JUC(java.util.concu ...

  8. 【C++】多线程与原子操作和无锁编程【五】

    [C++]多线程与原子操作和无锁编程[五] 1.何为原子操作 前面介绍了多线程间是通过互斥锁与条件变量来保证共享数据的同步的,互斥锁主要是针对过程加锁来实现对共享资源的排他性访问.很多时候,对共享资源 ...

  9. Java多线程-线程的同步与锁

    一.同步问题提出 线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏. 例如:两个线程ThreadA.ThreadB都操作同一个对象Foo对象,并修改Foo对象上的数据. package ...

最新文章

  1. HALCON查找圆心
  2. Android 自定义长按响应时间
  3. Android View之间的触摸事件传递图
  4. C指针原理(17)-C指针基础
  5. 15.5 匿名内部类
  6. 再论C++之垃圾回收(GC)
  7. 创建窗口,输入一个无符号整数,输出其对应的二进制数
  8. 树莓派python安装xlwt_利用python包(xlrd和xlwt)处理excel
  9. SQLServer之修改DEFAULT约束
  10. 切图具体需要切什么内容_【切图】UI设计师要懂得切图技巧
  11. c语言课程设计题目 吃豆子,C语言吃豆子游戏
  12. Unity3D中GPS定位信息及经纬度转换方法
  13. 伺服电机常用参数设置_松下伺服几个参数需要熟悉并掌握设置方法
  14. 单机游戏计时器防zuo弊解决方案
  15. win10开机的微软服务器,win10系统开机登录微软账户的操作方法
  16. 5s的app显示无法连接服务器,苹果手机无法连接到app store怎么办
  17. DolphinDB Database丨交易回测系列一:技术信号回测
  18. 记一次微信公众号开发过程
  19. Android 热修复一(热修复流程原理)
  20. 什么是 Skype?

热门文章

  1. 微信小程序开发入门(连载)—— 开发前的准备工作
  2. (原创)CRC计算流程分析(RefIn,Init,RefOut,XorOut)
  3. 常州abb机器人编程_ABB机器人编程程序解析
  4. 《回眸2022·圆满收官||展望2023·砥砺奋发》
  5. 【错误解决】Ubuntu 配置ibus中文输入法后却不能添加
  6. 2021-07-04 【5】
  7. LVGL (1) 介绍
  8. 深度学习基础理论(学习中持续更新)
  9. Prometheus监控神器-Alertmanager篇(1)
  10. ASM磁盘空间假装耗尽,ORA-15041: diskgroup space exhausted