今天给大家分享一下AQS,AQS 是一个非常精妙的设计当然难度也是有的但是也是人写出来的大家要有信心等大家正在理解了之后就会发现AQS 的精妙了
要了解AQS之前大家要先了解一下 CLH 队列 CHL队列是三位大神(Craig, Landin, and Hagersten)写的 CLH 队列就是取得他们的首字母作为名字,CLH 是一个单向链表结构图如下
CLH队列有三部分组成 头节点、中间节点、尾节点 每个节点(头节点除外)都有一个prev属性用来指向本节点的前一个节点,AQS底层依赖的CLH 是经过优化的把原本的单项链表优化成了双向列表
同时在AQS当中维护了一些重要的属性和内部类
他们分别是
- Node(内部类)
- head
- tail
- state
多线程大家在日常工作或者是学习中多少肯定用到过但是不知道大家有么有想过线程是怎么获得锁的又是怎么获取失败的,接下来我们好好聊一聊聊完之后大家就知道上面的四“位”是怎么回事了
现在有一个线程咱们叫他小明,我们给小明一把锁(加锁)再给之前要判断一下这个锁能不能给小明(可能这个锁没了),如果说小明拿到了这把锁那么这把锁就“名锁有主”了 咱们就把这把锁的state(状态)设置成1,但其他小朋友也想要这把锁的时候过来一看,哎哟喂 state=1了 那不行这把锁已经是别人的了。咱们来看一下代码
这里以ReentrantLock为例 ReentrantLock 默认是非公平锁 并且非公平锁比公平锁复杂咱们学习非公平锁学会之后自然可以向下兼容公平锁
进入lock()方法后会执行compareAndSetState(0, 1)方法返回的结果作为if语句的判断条件,compareAndSetState(0, 1)就是其他小朋友来看看锁是不是被别人拿走了的过程,进入这个方法
进入这个方法之后调用了unsafe这个类中的compareAndSwapInt(this, stateOffset, expect, update),unsafe这个类是java用来调用操作系统功能的一个类 也就是说这个类里面的所有方法调用的都是操作系统所以说不安全并且这个类也不是给普通程序员使用的,并且在jdk23中已经删除了
这个方法是基于 CAS 思想实现的 他的作用就是可以在保证原子性的情况下判断或修改state的值,其他小朋友过来看看这个锁期待他没有被别人拿走(state=0)并且想要拿走这把锁(让state=1),可是这把锁已经是小明的了那就获取失败了只能 “悻悻而归“(返回false)。
因为返回了false 所以要自在else当中的acquire(),那么这个方法是干什么的呢?大家可以想一想如果说其他小朋友没拿到锁但是他还想要那怎么办,那就只能去排队等着小明不要这把锁了然后其他小朋友再去抢这把锁,acquire()就是为了实现这个目的 咱们进入代码看一下
进入代码可以看到他内部其实他是有四个方法的其中有三方法是用来进行if判断的,这三方法才是关键,咱们一个一个看
tryAcquire():
一个小朋友开始马上要去教室里等待 那么他肯定想要去看看这个锁小明还要不要不要的话就直接拿过来 这个过程叫做尝试获取(tryAcquire),咱们看的是非公平锁所以进入的是nonfairTryAcquire()
进来之后你得先知道自己是谁,比如说现在进来的是李华,然后李华想知道这个锁现在还是不是小明的(小明在这里是泛称,也有可能是其他人),如果不是小明的那么 “嘿嘿” 我就不客气啦是我的了 拿到了之后我具成功了(返回 true),如果这把锁还是小明的怎么办那么就要判断一下这次来访问的是不是小明如果是那么就让状态+1(state)加一的目的是为了更好的解锁 因为你加了几次锁就要释放几次锁。很显然这把锁不属于李华那么只能返回 false。
那么我们在跳回 acquire()方法
tryAcquire方法返回的是false 那么! 取反的话就是 true 这样的话就要再看下一个方法了 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) ,咱们先看addWaiter(Node.EXCLUSIVE) 再看之前要先和大家说一下 Node.EXCLUSIVE 这个参数 ,AQS 分为独占模式和排他模式 ReentrantLock 是悲观锁 所以这里是独占模式。
李华去看了一眼还是没有拿到锁那么就只能去教室里等待了,教室里面有课桌 一个人一个 位置要按照顺序坐 并且为您节省成本必须是进来了之后再给桌椅而且必须是坐在前后一个桌椅之后
1李华进去之后老师要给李华加一个身份 一个 等待者 的身份(包装成Node 节点),这个房间里或许不只李华一个人可能还有其他的 “等待者”
2先找到最后一个同学(pred)
3.判断这个同学存不存在(此时李华是第一个进来的 在这里是不存在的)不存在的话说明教室里面一个桌子都没有 没用的话小明怎么坐?这个时候就要去找老师让搬一个过来(初始化CLH队列)
8 进入enq() 进行初始化
参数node 是被包装过的李华
8.1 同样的先拿到尾节点 然后进行判断发现还是null(此时还没有初始化)然后就进入8
8.2 使用compareAndSetHead(new Node())初始化 头节点 ,参数是一个 new Node() ,同样的咱们点进去看一下这个Node
其实他里面什么都没有,所以头节点也就叫做 “虚拟节点“
8.3 把头节点赋给 尾节点 此时头尾节点都是虚拟节点 这个是后就初始化完成了(虚拟节点知识里面没有内容不代表他不存在),我给大家贴张图大家理解一下
此时第一次循环结束开始第二次循环,此时直接跳到8.4当中
8.4 执行8.5 把 t (tail节点)设置成为李华节点的前置节点
8.6 compareAndSetTail(t, node) 进行CAS 比较 将李华节点设置成为尾节点 将李华节点的前置节点设置成为 头节点/最开始的尾节点
到此刻初始化完成了,继续回到 addWaiter()--> acquire(int arg) ,此时有回到了
我们呀开始 进入acquireQueued()方法了
先看 for循环
1. 先通过predecessor() 获取 李华节点的前置节点 定义为 p
2.if语句判断李华节点的前置节点是不是head 节点 很明显李华节点的前置节点是头节点,然后进入tryAcquire() 方法去看看这个锁小明还要不要不要的话我要,如果🔒小明不要了,那么李华就拿到了这把锁然后然后进入if 语句块中,先执行 setHead()方法
想让李华节点成为 head节点 然后把李华节点设置成为虚节点(node.thread=null),把李华节点的前置节点这条指针去掉 那么setHead()执行完成之后就会变成这样
然后把 head 节点的 next 指针去掉(p.next = null),此时 head(旧) 就可以 消失了 代替他的是李华
还记得我们之前没有说的两个属性吗 failed 和 interrupted
从语意上就能看出来他们是干什么的 一个是判断是否失败,一个是判断是否中断。那么 2 处的if 语句正常执行 所以两个属性返回的都是 false,此时 acquireQueued()方法饭后的就是false,此时此次加锁就完成了。
好了这就是一次加锁的过程,但是这次是因为李华是第一个来要锁的 可是如果再来一个人呢,比如说是还有一个小强他也要来横插一脚
小强进来之之后 CHL 队列就会变成
此时小强就要走蓝色区域
进入shouldParkAfterFailedAcquire(p, node) 方法 其中 p 是 小强节点的前置节点也就是李华节点,node节点是 小强节点
先获取当前节点的前置节点的状态(waitStatus)节点在创建完成之后默认是 “0” 所以说会进入 else 当中执行
我给大家列出了 waitStatus的 各个参数大家给可以看一看,如果后期需要的话我会单独出一期 参数的讲解在这里就不赘述了。
// CANCELLED:由于超时或中断,此节点被取消。节点一旦被取消了就不会再改变状态。特别是,取消节点的线程不会再阻塞。
static final int CANCELLED = 1;
// SIGNAL:此节点后面的节点已(或即将)被阻止(通过park),因此当前节点在释放或取消时必须断开后面的节点
// 为了避免竞争,acquire方法时前面的节点必须是SIGNAL状态,然后重试原子acquire,然后在失败时阻塞。
static final int SIGNAL = -1;
// 此节点当前在条件队列中。标记为CONDITION的节点会被移动到一个特殊的条件等待队列(此时状态将设置为0),直到条件时才会被重新移动到同步等待队列 。(此处使用此值与字段的其他用途无关,但简化了机制。)
static final int CONDITION = -2;
//传播:应将releaseShared传播到其他节点。这是在doReleaseShared中设置的(仅适用于头部节点),以确保传播继续,即使此后有其他操作介入。
static final int PROPAGATE = -3;
//0:以上数值均未按数字排列以简化使用。非负值表示节点不需要发出信号。所以,大多数代码不需要检查特定的值,只需要检查符号。
//对于正常同步节点,该字段初始化为0;对于条件节点,该字段初始化为条件。它是使用CAS修改的(或者在可能的情况下,使用无条件的volatile写入)。
进入else语句块中通过 compareAndSetWaitStatus(pred, ws, Node.SIGNAL) 方法来进行设置节点状态 这里设置的是 李华节点也就是小强的前节点
从代码中可以看到 调用过的是 unsafe 类进行了 CAS 操作 ,把小强节点的状态设置成为 SIGNAL(-1) ,执行完之后 shouldParkAfterFailedAcquire(p, node) 方法返回 false 然后在for循环中再次执shouldParkAfterFailedAcquire(p, node) 方法这次 因为 waitStatus 状态 是 SIGNAL 所以就要走下面这if 然后返回 true
然后执行 parkAndCheckInterrupt() 方法
不知道大家对 LockSupport 了不了解,part()的作用就是让这个线程挂起(阻塞) 参数this 在这里是李华线程,之后判断这个线程是否被打断 然后返回 判断的饿结果 这里是false 此时小强这个线程节点就在 CLH 队列中呆着了(挂载起来了)。
还有一种情况那就是如果说小明线程(持有锁的线程)在次加锁怎么办?我们来看一下代码
到这里为止加锁就讲完了,我们接下来开始说一下解锁
再进入关键方法 tryRelease()
然后进入 if 语句块
执行 unpartSuccessor()
在这里h是李华 s 是小强 再看一下 CLH 队列
此时的 CLH 队列如下
这个时候 unparkSuccessor(h) 方法就已经结束了 然后 release()方法返回 true 表示解锁成功。
到这里就已经讲解结束了,本篇文章是借助了 ReentrantLock 进行的演示 至于其他的锁 (Semaphore、ReentrantReadWriteLock 等)都是基于 AQS 的 所以说他们的原理和本文介绍的差不多如果 通过本文学会了思路之后 再去看其他锁的底层原理会有一种 “柳暗花明又一村” 的豁然感。
如果大家看了这篇文章之后发现我讲的透彻的地方可以评论或者给我私信 我会进行完善,希望大家可以通过这篇文章学到东西。
少年易老学难成,一寸光阴不可轻。
ok,大家加油!