抽象同步队列AQS源码学习
抽象同步队列AQS源码学习
1、AQS类结构剖析
AQS类结构如上图所示,AQS是一个FIFO的双向队列,队列元素的类型为Node,如上代码所示。
在AQS中维持了一个单一的状态信息state,可以通过getState、setState、compareAndSetSetState函数修改其值。对于ReentrantLock的实现来说,state可以用来表示当前线程获取锁的可重入次数;对于读写锁ReentrantReadWriteLock来说,state的高16位表示读状态, 也就是获取该读锁的次数, 低16位表示获取到写锁的线程的可重入次数。
AQS还有个内部类ConditionObject,用来结合锁实现线程同步。ConditionObject可以直接访问AQS对象内部的变量,比如state状态值和AQS队列。ConditionObject是条件变量,每个条件变量对应一个条件队列(单向链表队列),其用来存放调用条件变量的await方法后被阻塞的线程,如上代码所示,这个条件队列的头、尾元素分别为firstWaiter和lastWaiter。
在独占方式下,获取与释放资源代码如下:
当一个线程调用acquire方法获取独占资源时,会首先使用tryAcquire方法尝试获取资源,具体是设置状态变量state的值,成功则直接返回,失败则将当前线程封装为类型为Node.EXCLUSIVE的Node节点后插入到AQS阻塞队列的尾部,并调用LockSupport.park(this)方法挂起自己。
-
tryAcquire
方法主要目的是尝试获取锁,tryAcquire
方法是子类实现的; -
addWaiter
如果tryAcquire
尝试获取锁失败,则调用addWaiter
方法将当前线程添加到一个等待队列
中,等待后续处理; -
acquireQueued
方法处理加入到队列中的节点(Node),通过自旋
去尝试获取锁,会根据前驱节点的waitStatus的情况将线程挂起
或者取消
。
当一个线程调用release方法时,会尝试使用tryRelease方法操作释放资源,这里是设置状态变量state的值,然后调用LockSupport.unpark方法激活AQS队列里面被阻塞的一个线程。被激活的线程则使用tryAcquire尝试,看当前状态变量state的值是否能满足自己的需要,满足则该线程被激活,然后继续向下运行,否则还是会被放入AQS队列并被挂起。
但是AQS并没有提供可用的tryAcquire和tryRelease方法,它们需要由具体的子类去实现,子类在实现的时候要根据具体场景使用CAS算法尝试修改state状态值,成功则返回true,否则返回false。
比如ReentrantLock,定义当status为0时表示锁空闲,为1时表示锁已经被占用。在重写tryAcquire时,在内部需要使用CAS算法查看当前state是否为0,如果为0则使用CAS设置为1,并设置当前锁的持有者为当前线程,然后返回true,如果CAS失败则返回false。
AQS的入队操作,当一个线程获取锁失败后,该线程会被转换为Node节点,然后就会使用enq方法将该节点插入到AQS的阻塞队列。
2、AQS条件变量的支持
synchronized内置锁实现线程间同步可以通过notify和wait来实现,条件变量的signal和await方法也是用来配合锁(使用AQS实现的锁)实现线程间同步的基础设施。
它们的不同在于synchronized同时只能与一个共享变量的notify或wait方法实现同步,而AQS的一个锁可以对应多个条件变量。
代码中Lock对象等价于synchronized加上共享变量,调用lock.lock方法就相当于进入了synchronized块,调用lock.unlock方法就相当于退出synchronized快。调用条件变量的await方法就相当于调用共享变量的wait方法,调用条件变量的signal方法就相当于调用共享变量的notify()方法。调用条件变量的signalall就相当于调用共享变量的notifyall()方法。
lock.newCondition的作用其实是new了一个在AQS内部声明的ConditionObject对象,ConditionObject是AQS的内部类,可以访问AQS内部的变量和方法。在每个条件变量内部都维护了一个条件队列,用来存放调用条件变量的await()方法时被阻塞的线程。这个条件队列和AQS队列不是一回事。
上面代码中,当线程调用条件变量的await()方法时,在内部会构造一个类型为Node.CONDITION的node节点,然后将该节点插入条件队列末尾,之后当前线程会释放获取的锁(也就是会操作锁对应的state变量的值),并被阻塞挂起。这时候如果有其他线程调用lock.lock尝试获取锁,就会有一个线程获取到锁,如果获取到锁的线程调用了条件变量的await方法,则该线程也会被放入条件变量的阻塞队列,随后释放获取到的锁,在await方法处阻塞。
在下面代码中,当另外一个线程调用条件变量的signal方法时,在内部会把条件队列里面队头的一个线程节点从条件队列里面移除并放入AQS的阻塞队列里面,然后激活这个线程。
3、总结
当多个线程同时调用lock.lock()方法获取锁时,只有一个线程获取到了锁,其他线程会被转换为Node节点插入到lock锁对应的AQS阻塞队列里面,并做自旋CAS尝试获取锁。
如果获取到锁的线程又调用了对应的条件变量的await()方法,则该线程会释放获取到的锁,并被转换为Node节点插入到条件变量对应的条件队列里面。
这时候因为调用lock.lock()方法被阻塞到AQS队列里面的一个线程会获取到被释放的锁,如果该线程也调用了条件变量的await()方法则该线程也会被放入条件变量的条件队列里面。
当另外一个线程条用条件变量的signal()或者signalall方法时,会把条件队列里面的一个或者全部Node节点移动到AQS的阻塞队列里面,等待时机获取锁。
一个锁对应一个阻塞队列,对应多个条件变量,每个条件变量有自己的一个条件队列。
__EOF__

本文链接:https://www.cnblogs.com/youngerwb/p/16310782.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Manus重磅发布:全球首款通用AI代理技术深度解析与实战指南
· 被坑几百块钱后,我竟然真的恢复了删除的微信聊天记录!
· 没有Manus邀请码?试试免邀请码的MGX或者开源的OpenManus吧
· 园子的第一款AI主题卫衣上架——"HELLO! HOW CAN I ASSIST YOU TODAY
· 【自荐】一款简洁、开源的在线白板工具 Drawnix