在并发多线程的情况下,为了保证数据安全性,一般我们会对数据进行加锁,通常使用Synchronized或者ReentrantLock同步锁。Synchronized是基于JVM实现,而ReentrantLock是基于Java代码层面实现的,底层是继承的AQS

AQS全称AbstractQueuedSynchronizer,即抽象队列同步器,是一种用来构建锁和同步器的框架。

我们常见的并发锁ReentrantLockCountDownLatchSemaphoreCyclicBarrier都是基于AQS实现的,所以说不懂AQS实现原理的,就不能说了解Java锁。

当我仔细研究AQS底层加锁原理,发现竟然跟Synchronized加锁原理有惊人的相似。让我突然想到一句名言,记不清怎么说了,意思是框架底层原理很相似,大家多学习底层原理。

Synchronized的加锁流程在前几篇文章已经详细讲过,没看过一块再温习一下。

1. Synchronized加锁流程

我们先想一下Synchronized的加锁需求,如果让你设计Synchronized对象锁存储结构,该怎么设计?

  1. 多个线程执行到Synchronized代码块,只有一个线程获取锁,然后执行同步代码块(需要记录哪个线程获取了对象锁)。
  2. 其他线程被阻塞(被阻塞的线程,是不是可以用链表设计个阻塞队列?)
  3. 持有锁的线程调用wait方法,释放锁,等待被唤醒(等待的线程,是不是可以用链表设计个等待队列?)。
  4. 被阻塞的线程开始竞争锁
  5. 调用notify方法,唤醒等待的线程,被唤醒的线程进入阻塞队列,一块竞争锁。

上面描述了Synchronized的加锁流程,Synchronized对象锁存储结构是不是跟咱们想的一样?实际就是的。

下面是对象锁的存储数据结构(由C++实现):

ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL; // 持有锁的线程
_WaitSet = NULL; // 等待队列,存储处于wait状态的线程
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; // 阻塞队列,存储处于等待锁block状态的线程
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}

上图展示了对象锁的基本工作机制:

  1. 当多个线程同时访问一段同步代码时,首先会进入 _EntryList队列中阻塞。

  2. 当某个线程获取到对象的对象锁后进入临界区域,并把对象锁中的 _owner变量设置为当前线程,即获得对象锁。

  3. 若持有对象锁的线程调用 wait() 方法,将释放当前持有的对象锁,_owner变量恢复为null,同时该线程进入 _WaitSet 集合中等待被唤醒。

  4. 在_WaitSet集合中的线程被唤醒,会被再次放到_EntryList队列中,重新竞争获取锁。

  5. 若当前线程执行完毕也将释放对象锁并复位变量的值,以便其他线程进入获取锁。

Synchronized对象锁存储结构和加锁流程,竟然跟咱们想的一样。

再看一下AQS的存储结构和加锁流程,有没有相似的地方。

2. AQS加锁原理

先分析一下,我们使用AQS的加锁需求:

  1. 多个线程执行到acquire方法的时候,只有一个线程获取锁,然后执行同步代码块(需要记录哪个线程获取了对象锁)。
  2. 其他线程被阻塞(被阻塞的线程,是不是可以用链表设计个阻塞队列?名叫”同步队列“?)
  3. 持有锁的线程调用await方法,释放锁,等待被唤醒(等待的线程,是不是可以用链表设计个等待队列?名叫”条件队列“?)。
  4. 被阻塞的线程开始竞争锁
  5. 调用signal方法,唤醒等待的线程,被唤醒的线程进入阻塞队列,一块竞争锁。

AQS的需求跟Synchronized一模一样。

我们再看一下AQS实际的加锁机制是怎么设计的?是不是跟Synchronized相似?

AQS的加锁流程并不复杂,只要理解了同步队列条件队列,以及它们之间的数据流转,就算彻底理解了AQS

  1. 当多个线程竞争AQS锁时,如果有个线程获取到锁,就把ower线程设置为自己
  2. 没有竞争到锁的线程,在同步队列中阻塞(同步队列采用双向链表,尾插法)。
  3. 持有锁的线程调用await方法,释放锁,追加到条件队列的末尾(条件队列采用单链表,尾插法)。
  4. 持有锁的线程调用signal方法,唤醒条件队列的头节点,并转移到同步队列的末尾。
  5. 同步队列的头节点优先获取到锁

可以看到AQSSynchronized的加锁流程几乎是一模一样的,AQS中同步队列就是SynchronizedEntryListAQS中条件队列就是Synchronized中的waitSet,两个队列之间的数据转移流程也是一样的。

3. 总结

AQSSynchronized的加锁流程是一样的,都是通过同步队列和条件队列实现的,阻塞状态的线程被放到同步队列中,等待状态的线程被放到条件队列中,从条件队列唤醒的线程又被转移到同步队列末尾,一块竞争锁。

看完AQS加锁流程,还没有人不懂AQS的?

下篇文章再讲一下AQS加锁具体的源码实现。里面有很多精巧的设计,值得我们学习。

比如:

为什么同步队列要设计成双向链表?而条件队列要设计成单链表?

为什么AQS加锁性能这么好(乐观锁CAS使用)?

同步队列和条件队列中节点怎么用一个对象实现?

释放锁后,怎么唤醒同步队列中线程?

我是「一灯架构」,如果本文对你有帮助,欢迎各位小伙伴点赞、评论和关注,感谢各位老铁,我们下期见

重大发现,AQS加锁机制竟然跟Synchronized有惊人的相似的更多相关文章

  1. java多线程(五)-访问共享资源以及加锁机制(synchronized,lock,voliate)

    对于单线程的顺序编程而言,每次只做一件事情,其享有的资源不会产生什么冲突,但是对于多线程编程,这就是一个重要问题了,比如打印机的打印工作,如果两个线程都同时进行打印工作,那这就会产生混乱了.再比如说, ...

  2. 深入了解ReentrantLock中的公平锁和非公平锁的加锁机制

    ReentrantLock和synchronized一样都是实现线程同步,但是像比synchronized它更加灵活.强大.增加了轮询.超时.中断等高级功能,可以更加精细化的控制线程同步,它是基于AQ ...

  3. java两种同步机制的实现 synchronized和reentrantlock

    java两种同步机制的实现 synchronized和reentrantlock 双11加保障过去一周,趁现在有空,写一点硬货,因为在进入阿里之后工作域的原因之前很多java知识点很少用,所以记录一下 ...

  4. 【Java并发基础】加锁机制解决原子性问题

    前言 原子性指一个或多个操作在CPU执行的过程不被中断的特性.前面提到原子性问题产生的源头是线程切换,而线程切换依赖于CPU中断.于是得出,禁用CPU中断就可以禁止线程切换从而解决原子性问题.但是这种 ...

  5. MySQL各类SQL语句的加锁机制

    官网参考:https://dev.mysql.com/doc/refman/5.6/en/innodb-locks-set.html MySQL把读操作分为两大类:锁定读和非锁定读(即locking ...

  6. ElasticStack系列之二十 & 数据均衡、迁移、冷热分离以及节点自动发现原理与机制

    1. 数据均衡 某个shard分配到哪个节点上,一般来说,是由 ELasticSearch 自行决定的.以下几种情况会触发分配动作: 新索引的建立 索引的删除 新增副本分片 节点增减引发的数据均衡 在 ...

  7. OSSpinLockLock加锁机制,保证线程安全并且性能高

    在aspect_add.aspect_remove方法里面用了aspect_performLocked, 而aspect_performLocked方法用了OSSpinLockLock加锁机制,保证线 ...

  8. 双重检查加锁机制(并发insert情况下数据重复插入问题的解决方案)

    双重检查加锁机制(并发insert情况下数据重复插入问题的解决方案) c#中单例模式和双重检查锁 转:https://blog.csdn.net/zhongliangtang/article/deta ...

  9. Java并发框架——AQS超时机制

    AQS框架提供的另外一个优秀机制是锁获取超时的支持,当大量线程对某一锁竞争时可能导致某些线程在很长一段时间都获取不了锁,在某些场景下可能希望如果线程在一段时间内不能成功获取锁就取消对该锁的等待以提高性 ...

随机推荐

  1. 1.7_CSS基础

    层叠样式表 (Cascading Style Sheets) CSS产生缘由 HTML 标签原本被设计为用于定义文档内容.通过使用 <h1>.<p>.<table> ...

  2. Html飞机大战(二):面向对象绘制背景

    好家伙, 我们为了后续工作的顺利进行,我试着把每一个模块封装为对象 但冻手之前还是要构思一下 我们把天空封装成一个类: 1.来搞一手简单的对象分析:  属性方面的都好理解 来说明一下方法: (1) p ...

  3. Python入门系列(八)日期时间、数学、json

    日期时间 Python中的日期本身不是数据类型,但我们可以导入一个名为datetime的模块,将日期作为日期对象使用. import datetime x = datetime.datetime.no ...

  4. KingbaseES 工具sys_dump,sys_restore使用介绍

    说明: KingbaseES V8R6版本中自带数据库备份导出sys_dump,和备份恢复sys_restore工具. sys_dump:把KingbaseES数据库抽取为一个脚本文件或其他归档文件. ...

  5. Docker安装Redis并使用Another Redis Desktop Manager连接

    Redis简单介绍 Redis全称是Remote DIctionary Service,即远程字典服务.Redis 是一个使用C语言编写的.开源的(遵守 BSD 协议).高性能的.支持网络.可基于内存 ...

  6. go 中解析JSON的三种姿势

    背景 这是一篇写给0-1年新人的文章,短平快的教会你如何解析json字符串. 示例Json 假设有如下json字符串: { "userName":"admin" ...

  7. LFS(Linux From Scratch)构建过程全记录(一):准备工作

    写在前面 本人修学了一门课,名曰<操作系统课程设计>,其任务为基于LFS以编译源代码的方式制作一个基本的Linux操作系统,并且编写在linux下的GUI软件. 本操作系统构建的全过程将分 ...

  8. Ubuntu22.04 安装配置流水账

    前两天为了测一个CH340的bug, 装了三遍20.04. bug解决完, 心想反正也要重新装各种软件, 不如直接装22.04吧. 把涉及的安装记录一下方便将来参考. 制作启动U盘 在Ubuntu网站 ...

  9. 大家都在用MySQL count(*)统计总数,到底有什么问题?

    在日常开发工作中,我经常会遇到需要统计总数的场景,比如:统计订单总数.统计用户总数等.一般我们会使用MySQL 的count函数进行统计,但是随着数据量逐渐增大,统计耗时也越来越长,最后竟然出现慢查询 ...

  10. 【gRPC】C++异步服务端优化版,多服务接口样例

    官方的C++异步服务端API样例可读性并不好,理解起来非常的费劲,各种状态机也并不明了,整个运行过程也容易读不懂,因此此处参考网上的博客进行了重写,以求顺利读懂. C++异步服务端实例,详细注释版 g ...