ReentrantLock原理CAS+AQS队列
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。
CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现
ReentrantLock主要利用CAS+AQS队列来实现。它支持公平锁和非公平锁,两者的实现类似。
CAS:Compare and Swap,比较并交换。CAS有3个操作数:内存值V、预期值A、要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。该操作是一个原子操作,被广泛的应用在Java的底层实现中。在Java中,CAS主要是由sun.misc.Unsafe这个类通过JNI调用CPU底层指令实现
AbstractQueuedSynchronizer简称AQS
【ReentrantLock使用示例】
- private Lock lock = new ReentrantLock();
- public void test(){
- lock.lock();
- try{
- doSomeThing();
- }catch (Exception e){
- // ignored
- }finally {
- lock.unlock();
- }
- }
【AQS】
是一个用于构建锁和同步容器的框架。事实上concurrent包内许多类都是基于AQS构建,例如ReentrantLock,Semaphore,CountDownLatch,ReentrantReadWriteLock,FutureTask等。AQS解决了在实现同步容器时设计的大量细节问题。

AQS使用一个FIFO的队列表示排队等待锁的线程,队列头节点称作“哨兵节点”或者“哑节点”,它不与任何线程关联。其他的节点与等待线程关联,每个节点维护一个等待状态waitStatus
ReentrantLock的基本实现可以概括为:先通过CAS尝试获取锁。如果此时已经有线程占据了锁,那就加入AQS队列并且被挂起。当锁被释放之后,排在CLH队列队首的线程会被唤醒,然后CAS再次尝试获取锁。在这个时候,如果:
非公平锁:如果同时还有另一个线程进来尝试获取,那么有可能会让这个线程抢先获取;
公平锁:如果同时还有另一个线程进来尝试获取,当它发现自己不是在队首的话,就会排到队尾,由队首的线程获取到锁。
【lock()与unlock()实现原理】
可重入锁。可重入锁是指同一个线程可以多次获取同一把锁。ReentrantLock和synchronized都是可重入锁。
可中断锁。可中断锁是指线程尝试获取锁的过程中,是否可以响应中断。synchronized是不可中断锁,而ReentrantLock则提供了中断功能。
公平锁与非公平锁。公平锁是指多个线程同时尝试获取同一把锁时,获取锁的顺序按照线程达到的顺序,而非公平锁则允许线程“插队”。synchronized是非公平锁,而ReentrantLock的默认实现是非公平锁,但是也可以设置为公平锁。
CAS操作(CompareAndSwap)。CAS操作简单的说就是比较并交换。CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在 CAS 指令之前返回该位置的值。CAS 有效地说明了“我认为位置 V 应该包含值 A;如果包含该值,则将 B 放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。” Java并发包(java.util.concurrent)中大量使用了CAS操作,涉及到并发的地方都调用了sun.misc.Unsafe类方法进行CAS操作。
ReentrantLock提供了两个构造器,分别是
- public ReentrantLock() {
- sync = new NonfairSync();
- }
- public ReentrantLock(boolean fair) {
- sync = fair ? new FairSync() : new NonfairSync();
- }
默认构造器初始化为NonfairSync对象,即非公平锁,而带参数的构造器可以指定使用公平锁和非公平锁。由lock()和unlock的源码可以看到,它们只是分别调用了sync对象的lock()和release(1)方法。
NonfairSync
- final void lock() {
- if (compareAndSetState(0, 1))
- setExclusiveOwnerThread(Thread.currentThread());
- else
- acquire(1);
- }
首先用一个CAS操作,判断state是否是0(表示当前锁未被占用),如果是0则把它置为1,并且设置当前线程为该锁的独占线程,表示获取锁成功。当多个线程同时尝试占用同一个锁时,CAS操作只能保证一个线程操作成功,剩下的只能乖乖的去排队啦。
“非公平”即体现在这里,如果占用锁的线程刚释放锁,state置为0,而排队等待锁的线程还未唤醒时,新来的线程就直接抢占了该锁,那么就“插队”了。
若当前有三个线程去竞争锁,假设线程A的CAS操作成功了,拿到了锁开开心心的返回了,那么线程B和C则设置state失败,走到了else里面。我们往下看acquire。
- public final void acquire(int arg) {
- if (!tryAcquire(arg) &&
- acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
- selfInterrupt();
- }
1. 第一步。尝试去获取锁。如果尝试获取锁成功,方法直接返回。
- tryAcquire(arg)
- final boolean nonfairTryAcquire(int acquires) {
- //获取当前线程
- final Thread current = Thread.currentThread();
- //获取state变量值
- int c = getState();
- 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");
- // 更新state值为新的重入次数
- setState(nextc);
- return true;
- }
- //获取锁失败
- return false;
- }
非公平锁tryAcquire的流程是:检查state字段,若为0,表示锁未被占用,那么尝试占用,若不为0,检查当前锁是否被自己占用,若被自己占用,则更新state字段,表示重入锁的次数。如果以上两点都没有成功,则获取锁失败,返回false。
2. 第二步,入队。由于上文中提到线程A已经占用了锁,所以B和C执行tryAcquire失败,并且入等待队列。如果线程A拿着锁死死不放,那么B和C就会被挂起。
先看下入队的过程。先看addWaiter(Node.EXCLUSIVE)
- /**
- * 将新节点和当前线程关联并且入队列
- * @param mode 独占/共享
- * @return 新节点
- */
- private Node addWaiter(Node mode) {
- //初始化节点,设置关联线程和模式(独占 or 共享)
- 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;
- }
- }
- // 尾节点为空,说明队列还未初始化,需要初始化head节点并入队新节点
- enq(node);
- return node;
- }
B、C线程同时尝试入队列,由于队列尚未初始化,tail==null,故至少会有一个线程会走到enq(node)。我们假设同时走到了enq(node)里。
- /**
- * 初始化队列并且入队新节点
- */
- private Node enq(final Node node) {
- //开始自旋
- for (;;) {
- Node t = tail;
- if (t == null) { // Must initialize
- // 如果tail为空,则新建一个head节点,并且tail指向head
- if (compareAndSetHead(new Node()))
- tail = head;
- } else {
- node.prev = t;
- // tail不为空,将新节点入队
- if (compareAndSetTail(t, node)) {
- t.next = node;
- return t;
- }
- }
- }
- }
这里体现了经典的自旋+CAS组合来实现非阻塞的原子操作。由于compareAndSetHead的实现使用了unsafe类提供的CAS操作,所以只有一个线程会创建head节点成功。假设线程B成功,之后B、C开始第二轮循环,此时tail已经不为空,两个线程都走到else里面。假设B线程compareAndSetTail成功,那么B就可以返回了,C由于入队失败还需要第三轮循环。最终所有线程都可以成功入队。
当B、C入等待队列后,此时AQS队列如下:

3. 第三步,挂起。B和C相继执行acquireQueued(final Node node, int arg)。这个方法让已经入队的线程尝试获取锁,若失败则会被挂起。
- /**
- * 已经入队的线程尝试获取锁
- */
- final boolean acquireQueued(final Node node, int arg) {
- boolean failed = true; //标记是否成功获取锁
- try {
- boolean interrupted = false; //标记线程是否被中断过
- for (;;) {
- final Node p = node.predecessor(); //获取前驱节点
- //如果前驱是head,即该结点已成老二,那么便有资格去尝试获取锁
- if (p == head && tryAcquire(arg)) {
- setHead(node); // 获取成功,将当前节点设置为head节点
- p.next = null; // 原head节点出队,在某个时间点被GC回收
- failed = false; //获取成功
- return interrupted; //返回是否被中断过
- }
- // 判断获取失败后是否可以挂起,若可以则挂起
- if (shouldParkAfterFailedAcquire(p, node) &&
- parkAndCheckInterrupt())
- // 线程若被中断,设置interrupted为true
- interrupted = true;
- }
- } finally {
- if (failed)
- cancelAcquire(node);
- }
- }
code里的注释已经很清晰的说明了acquireQueued的执行流程。假设B和C在竞争锁的过程中A一直持有锁,那么它们的tryAcquire操作都会失败,因此会走到第2个if语句中。我们再看下shouldParkAfterFailedAcquire和parkAndCheckInterrupt都做了哪些事吧。
- /**
- * 判断当前线程获取锁失败之后是否需要挂起.
- */
- private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
- //前驱节点的状态
- int ws = pred.waitStatus;
- if (ws == Node.SIGNAL)
- // 前驱节点状态为signal,返回true
- return true;
- // 前驱节点状态为CANCELLED
- if (ws > 0) {
- // 从队尾向前寻找第一个状态不为CANCELLED的节点
- do {
- node.prev = pred = pred.prev;
- } while (pred.waitStatus > 0);
- pred.next = node;
- } else {
- // 将前驱节点的状态设置为SIGNAL
- compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
- }
- return false;
- }
- /**
- * 挂起当前线程,返回线程中断状态并重置
- */
- private final boolean parkAndCheckInterrupt() {
- LockSupport.park(this);
- return Thread.interrupted();
- }
线程入队后能够挂起的前提是,它的前驱节点的状态为SIGNAL,它的含义是“Hi,前面的兄弟,如果你获取锁并且出队后,记得把我唤醒!”。所以shouldParkAfterFailedAcquire会先判断当前节点的前驱是否状态符合要求,若符合则返回true,然后调用parkAndCheckInterrupt,将自己挂起。如果不符合,再看前驱节点是否>0(CANCELLED),若是那么向前遍历直到找到第一个符合要求的前驱,若不是则将前驱节点的状态设置为SIGNAL。
整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心挂起,需要去找个安心的挂起点,同时可以再尝试下看有没有机会去尝试竞争锁。
最终队列可能会如下图所示

unlock()
- 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;
- }
如果理解了加锁的过程,那么解锁看起来就容易多了。流程大致为先尝试释放锁,若释放成功,那么查看头结点的状态是否为SIGNAL,如果是则唤醒头结点的下个节点关联的线程,如果释放失败那么返回false表示解锁失败。这里我们也发现了,每次都只唤起头结点的下一个节点关联的线程。
最后我们再看下tryRelease的执行过程
- /**
- * 释放当前线程占用的锁
- * @param releases
- * @return 是否释放成功
- */
- protected final boolean tryRelease(int releases) {
- // 计算释放后state值
- int c = getState() - releases;
- // 如果不是当前线程占用锁,那么抛出异常
- if (Thread.currentThread() != getExclusiveOwnerThread())
- throw new IllegalMonitorStateException();
- boolean free = false;
- if (c == 0) {
- // 锁被重入次数为0,表示释放成功
- free = true;
- // 清空独占线程
- setExclusiveOwnerThread(null);
- }
- // 更新state值
- setState(c);
- return free;
- }
这里入参为1。tryRelease的过程为:当前释放锁的线程若不持有锁,则抛出异常。若持有锁,计算释放后的state值是否为0,若为0表示锁已经被成功释放,并且则清空独占线程,最后更新state值,返回free。
用一张流程图总结一下非公平锁的获取锁的过程。

FairSync
公平锁和非公平锁不同之处在于,公平锁在获取锁的时候,不会先去检查state状态,而是直接执行aqcuire(1
超时机制
在ReetrantLock的tryLock(long timeout, TimeUnit unit) 提供了超时获取锁的功能。它的语义是在指定的时间内如果获取到锁就返回true,获取不到则返回false。这种机制避免了线程无限期的等待锁释放。那么超时的功能是怎么实现的呢?我们还是用非公平锁为例来一探究竟。
- public boolean tryLock(long timeout, TimeUnit unit)
- throws InterruptedException {
- return sync.tryAcquireNanos(1, unit.toNanos(timeout));
- }
还是调用了内部类里面的方法。我们继续向前探究
- public final boolean tryAcquireNanos(int arg, long nanosTimeout)
- throws InterruptedException {
- if (Thread.interrupted())
- throw new InterruptedException();
- return tryAcquire(arg) ||
- doAcquireNanos(arg, nanosTimeout);
- }
这里的语义是:如果线程被中断了,那么直接抛出InterruptedException。如果未中断,先尝试获取锁,获取成功就直接返回,获取失败则进入doAcquireNanos。tryAcquire我们已经看过,这里重点看一下doAcquireNanos做了什么。
- /**
- * 在有限的时间内去竞争锁
- * @return 是否获取成功
- */
- private boolean doAcquireNanos(int arg, long nanosTimeout)
- throws InterruptedException {
- // 起始时间
- long lastTime = System.nanoTime();
- // 线程入队
- final Node node = addWaiter(Node.EXCLUSIVE);
- boolean failed = true;
- try {
- // 又是自旋!
- for (;;) {
- // 获取前驱节点
- final Node p = node.predecessor();
- // 如果前驱是头节点并且占用锁成功,则将当前节点变成头结点
- if (p == head && tryAcquire(arg)) {
- setHead(node);
- p.next = null; // help GC
- failed = false;
- return true;
- }
- // 如果已经超时,返回false
- if (nanosTimeout <= 0)
- return false;
- // 超时时间未到,且需要挂起
- if (shouldParkAfterFailedAcquire(p, node) &&
- nanosTimeout > spinForTimeoutThreshold)
- // 阻塞当前线程直到超时时间到期
- LockSupport.parkNanos(this, nanosTimeout);
- long now = System.nanoTime();
- // 更新nanosTimeout
- nanosTimeout -= now - lastTime;
- lastTime = now;
- if (Thread.interrupted())
- //相应中断
- throw new InterruptedException();
- }
- } finally {
- if (failed)
- cancelAcquire(node);
- }
- }
doAcquireNanos的流程简述为:线程先入等待队列,然后开始自旋,尝试获取锁,获取成功就返回,失败则在队列里找一个安全点把自己挂起直到超时时间过期。这里为什么还需要循环呢?因为当前线程节点的前驱状态可能不是SIGNAL,那么在当前这一轮循环中线程不会被挂起,然后更新超时时间,开始新一轮的尝试
https://blog.csdn.net/fuyuwei2015/article/details/83719444
ReentrantLock原理CAS+AQS队列的更多相关文章
- 并发之AQS原理(二) CLH队列与Node解析
并发之AQS原理(二) CLH队列与Node解析 1.CLH队列与Node节点 就像通常医院看病排队一样,医生一次能看的病人数量有限,那么超出医生看病速度之外的病人就要排队. 一条队列是队列中每一个人 ...
- Java 重入锁 ReentrantLock 原理分析
1.简介 可重入锁ReentrantLock自 JDK 1.5 被引入,功能上与synchronized关键字类似.所谓的可重入是指,线程可对同一把锁进行重复加锁,而不会被阻塞住,这样可避免死锁的产生 ...
- AQS 原理以及 AQS 同步组件总结
1 AQS 简单介绍 AQS 的全称为(AbstractQueuedSynchronizer),这个类在 java.util.concurrent.locks 包下面. AQS 是一个用来构建锁和同步 ...
- ReentrantLock原理分析
一 UML类图 通过类图ReentrantLock是同步锁,同一时间只能有一个线程获取到锁,其他获取该锁的线程会被阻塞而被放入AQS阻塞队列中.ReentrantLock类继承Lock接口:内部抽象类 ...
- ReentrantLock原理
ReentrantLock主要利用CAS+CLH队列来实现.它支持公平锁和非公平锁,两者的实现类似. CAS:Compare and Swap,比较并交换.CAS有3个操作数:内存值V.预期值A.要修 ...
- ReentrantLock的实现原理及AQS和CAS
AQS,即AbstractQueuedSynchronizer, 队列同步器,它是多线程访问共享资源的同步器框架,Java中的ReentrantLock/Semaphore/CountDownLatc ...
- Java中CAS 基本实现原理 和 AQS 原理
一.前言了解CAS,首先要清楚JUC,那么什么是JUC呢?JUC就是java.util.concurrent包的简称.它有核心就是CAS与AQS.CAS是java.util.concurrent.at ...
- Java 同步锁ReentrantLock与抽象同步队列AQS
AbstractQueuedSynchronizer 抽象同步队列,它是个模板类提供了许多以锁相关的操作,常说的AQS指的就是它.AQS继承了AbstractOwnableSynchronizer类, ...
- ReentrantLock原理学习
上文我们学习了ReentrantLock的基本用法,在最后我们留下了一个问题,ReentrantLock获取的锁是什么锁呢?本文我们就从源码的角度来一探究竟.本文涉及到的源码对应JDK版本为1.8. ...
- 源码级深挖AQS队列同步器
我们知道,在java中提供了两类锁的实现,一种是在jvm层级上实现的synchrinized隐式锁,另一类是jdk在代码层级实现的,juc包下的Lock显示锁,而提到Lock就不得不提一下它的核心队列 ...
随机推荐
- Java开发中PO、VO、DAO、BO、DTO、POJO 含义
PO(persistant object) 持久对象 可以看成是与数据库中的表相映射的java对象.使用 Mybatis 来生成 PO 是不错的选择. VO(value object) 值对象 通常用 ...
- 【深度学习项目二】卷积神经网络LeNet实现minst数字识别
相关文章: [深度学习项目一]全连接神经网络实现mnist数字识别 [深度学习项目二]卷积神经网络LeNet实现minst数字识别 [深度学习项目三]ResNet50多分类任务[十二生肖分类] 『深度 ...
- 关闭表单验证/关闭控制台async-validator警告
找到util.js node_modules -> async-validator -> es -> util.js 将console.warn(type, errors)注释 如果 ...
- Linux中国开源社区停止运营
layout: post title: "Linux 中国开源社区停止运营" tags: - "Linux" 昨天看到老王发的公众号文章,Linux中国开源社区 ...
- HBase-compact介绍(minor和major区别)
一.minor和major的区别: Minor Compaction:指选取一些小的.相邻的HFile将他们合并成一个更大的HFile,但不会清理过期(TTL)和删除(打上Delete标记)的数据. ...
- sshd命令-测试sshd_config配置是否正确
sshd命令来自于英文词组"SSH daemon"的缩写,其功能是用于openssh服务器守护进程.openssh套件能够为两台主机之间建立加密的.可信任的数据通信服务,是rlog ...
- 云计算 - 负载均衡SLB方案全解与实战
云计算 - 负载均衡SLB方案全解与实战,介绍SLB的核心技术.用户最佳实践.阿里云 SLB产品举例.应用场景. 关注[TechLeadCloud],分享互联网架构.云服务技术的全维度知识.作者拥有1 ...
- Sora文生视频模型深度剖析:全网独家指南,洞悉98%关键信息,纯干货
Sora文生视频模型深度剖析:全网独家指南,洞悉98%关键信息,纯干货 Sora是一个以视频生成为核心的多能力模型,具备以下能力: 文/图生成视频 视频生成视频 1分钟超长高质量视频生成 视频裂变多视 ...
- phpBB3在Nginx反向代理中的X-Forwarded-For IP检查
记录一下phpBB3对反向代理中的IP处理机制 处理几个phpBB3项目迁移, 部分运行场景转移到内网, 需要外网通过nginx/openresty之类的网关反向代理进行访问. 在网关处已经正确配置了 ...
- 【Unity3D】空间和变换
1 空间 1.1 左右手坐标系及其法则 1.1.1 左右手坐标系 左手坐标系与右手坐标系 Unity 局部空间.世界空间.裁剪空间.屏幕空间都采用左手坐标系,只有观察空间采用右手坐标系. 左右 ...