一、序言
Lock接口是juc包下一个非常好用的锁,其方便和强大的功能让他成为synchronized的一个很好的替代品。
我们常用的一个Lock的实现类(好像也是唯一一个只实现了Lock接口的类)
当你查看源码时你会惊讶的发现ReentrantLock并没有多少代码,另外有一个很明显的特点是:基本上所有的方法的实现实际上都是调用了其静态内存类Sync中的方法,而Sync类继承了AbstractQueuedSynchronizer(AQS)。可以看出要想理解ReentrantLock关键核心在于对队列同步器AbstractQueuedSynchronizer(简称同步器)的理解。
所以,这系列的文章会对同步器AQS进行源码级别的分析。
而这一篇会主要针对独占锁的模板方法进行源码分析,主要就是acquire方法还有release。
二、关于AQS
2.1 大概介绍
同步器是用来构建锁和其他同步组件的基础框架,它的原理总体来说呢就是主要有一个int的状态类变量,还有一个FIFO的等待队列。
AQS在设计上是很典型的模板方法的运用,JDK建议你这样去实现一个锁,或者说是同步组件:
在你的同步组件里面定义一个静态内部类,然后这个内部类要继承AQS类,一般这个类命名为Sync,然后你要在这个子类中,重写几个AQS的protected方法,这个重写方法一般主要是关于那个状态变量的逻辑代码,就比如说怎样才叫已经lock了,是state值为1呢?还是>0呢?怎么通过这个state实现一个可重入的功能呢?也就是锁的逻辑实现吧可以理解成,而在这些重写方法里一般要对状态类变量就行访问和修改,AQS专门提供了getState,setState以及compareAndSetState这三个方法来帮你完成安全、可靠的对state的访问和修改。
而AQS父类中,除了提供对state的访问和修改的方法,还提供了和屏蔽了线程的排队、等待和唤醒等底层操作,大概就是AQS中提供了这些操作的接口,然后你在子类实现你的锁逻辑的时候调用就好了。
2.2 模板设计方法的理解
刚刚我们说到,要重写AQS的一些protected的方法,然后其实,在AQS的那些封装了线程的排队啊,等待啊什么的模板方法中,又调用了你的子类sync重写的protected方法,比如说看AQS的一个模板方法——acquire()方法:
AQS模板中的acquire()模板方法:
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
可以看到,这里调用了tryAcquire,这个是一个protected的需要我们重新的方法。
其实也很容易理解,这个方法是判断线程能不能拿到锁,然后再看要不要线程排队的一个实现,那有没拿到锁,肯定要看你自己的锁的逻辑是怎么实现的啊,所以要调用你重写的tryAcquire,问问emm现在我这个线程的状态是拿到锁了吗?
2.3 可重写的方法和AQS提供的模板方法
AQS可重写的方法如下图(摘自《java并发编程的艺术》一书):
在实现同步组件时AQS提供的模板方法如下图:
AQS提供的模板方法可以分为3类:
- 独占式获取与释放同步状态;
- 共享式获取与释放同步状态;
- 查询同步队列中等待线程情况;
同步组件通过AQS提供的模板方法实现自己的同步语义。
总之,总结下就是
同步组件的实现者呢,就重写AQS中的protected方法,主要就是告诉AQS怎样判断当前同步状态是否成功获取或者是否成功释放。同步组件专注于对当前同步状态的逻辑判断,从而实现自己的同步语义。
AQS和它的模板方法呢(其实可以理解成工具方法吧)只需要同步组件返回的true和false即可,因为AQS会对true和false会有不同的操作,true会认为当前线程获取同步组件成功直接返回,而false的话就AQS也会将当前线程插入同步队列等一系列的方法。
三、AQS的同步队列
上面我们说到,AQS主要是通过维护一个int的状态类变量还有一个同步等待队列来帮助你实现同步器件的,这里就先让我们看看这个同步队列。
3.1 AQS里面有一个静态内部类——Node,这个就是构成AQS中的等待队列的节点,先来看看这个Node的一些类变量吧:
volatile int waitStatus //节点状态volatile Node prev //当前节点/线程的前驱节点volatile Node next; //当前节点/线程的后继节点volatile Thread thread;//加入同步队列的线程引用Node nextWaiter;/*等待队列中的下一个节点
- 可见这个同步队列是个双向链表的结构,而且 每个节点有状态。(注意和AQS中的那个状态区分,那个是确定同步逻辑的全局状态)
- 然后还有个线程,其实这个也好理解,因为这个队列是干嘛的?是没get到锁后,那些线程排队用的嘛,那就肯定一个node代表着一个排队的线程嘛,这个thread显然就应该是那个线程了。
- 然后这个nextWaiter就emmm,好像是Condition条件队列的东西。。。emmm迟点看condition的相关概念再看吧
节点的状态有如下:
int CANCELLED = 1//节点从同步队列中取消int SIGNAL = -1//后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继节点的线程能够运行;int CONDITION = -2//当前节点进入等待队列中,应该是用在Condition上的,在一般的同步队列应该不用int PROPAGATE = -3//表示下一次共享式同步状态获取将会无条件传播下去int INITIAL = 0;//初始状态,这个不是存在的常量,意思就以上的情况都不是
结点的构造方法:
Node() { //used to establish initial head or SHARED marker}Node(Thread thread, Node mode) { //used by addWaiter this.nextWaiter = mode; this.thread = thread;}Node(Thread thread, int waitStatus) { //used by Condition this.waitStatus = waitStatus; this.thread = thread;}
然后既然是一个队列,那肯定有个头结点什么的才能操作和便利这个链表吧,3.2 所以AQS还有两个关于这个等待队列很重要的类变量:
private transient volatile Node head;private transient volatile Node tail;
(我们也看到,基本上变量都是volatile的,毕竟是并发多线程环境下)
也就是说AQS实际上通过头尾指针来管理同步队列,同时实现包括获取锁失败的线程进行入队,释放锁时对同步队列中的线程进行通知等核心方法。其示意图如下:
3.3 通过对源码的理解以及做实验的方式,现在我们可以清楚的知道这样几点:
- 节点的数据结构,即AQS的静态内部类Node,节点的等待状态等信息;
- 同步队列是一个双向队列,AQS通过持有头尾指针管理同步队列;
那么,节点如何进行入队和出队是怎样做的了?实际上这对应着锁的获取和释放两个操作:获取锁失败进行入队操作,获取锁成功进行出队操作。
public final void acquire(int arg) { //先看同步状态是否获取成功,如果成功则方法结束返回 //若失败则先调用addWaiter()方法再调用acquireQueued()方法 if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();}
acquire方法流程分析
首先,这个方法有个int的参数,这个参数是传给tryAcquire的,但其实这个参数没有特别的含义,你可以根据自己的需要自己定义这个参数应该表达什么意思。
1. 这个方法先是在if里面判断tryAcquire(),也就是判断现在这个线程是否能获得锁,如果可以的话这个方法就什么都没做,直接结束了;
2. 如果tryAcquire判断出现在这个线程无法获得锁,那么就先调用addWaiter()然后在调用acquireQueued()方法。这个addWaiter顾名思义就是,为这个线程搞一个同步等待节点,然后这个acquireQueued()呢就等等再分析吧。
3. 如果acquireQueued成功了(这个acquireQueued返回的是node线程是否被interrupt了)就让这个线程interrupt。
private Node addWaiter(Node mode) { // 1. 将当前线程构建成Node类型 Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure // 2. 当前尾节点是否为null? Node pred = tail; if (pred != null) { // 2.2 将当前节点尾插入的方式插入同步队列中 node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } // 2.1. 当前同步队列尾节点为null,说明当前线程是第一个加入同步队列进行等待的线程 enq(node); return node;}
这个方法做了两件事:
1. 为这个无法获得锁的线程创建一个等待结点,并将这个结点尾插入到等待队列中去;
2. 赋予这个结点一个状态。
在刚刚的acquire方法中,是这样调用addWaiter的——addWaiter(Node.EXCLUSIVE)
这个addWaiter方法接受的是一个Node类型的mode,传入的是Node类中的一个常量,这个EXCLUSIVE是一个null,而如果是SHARED就分享锁的话,传入的是一个空构造器构建的Node实例。这其实就通过一个Node实例来传达些信息而已。
addWaiter的流程分析:
1. 创建这个没法获得锁的线程的代表结点实例。
2. 尾结点为空吗?如果为空,则用尾插法在等待队列尾部插入这个node,注意将tail指针换成新的node的时候要用提供的CAS操作方法。
3. 如果尾结点不为空,则说明这个线程是第一个插入的节点;又或者刚刚的CAS操作尾插法没成功,那么调用enq()方法。(好像并发包很多地方的实现都是这种第一个链头的设置都是要和其他不一样的)
4. 返回node,即这个没法获得锁的线程的代表结点实例。
那么来看看enq()方法吧,可以猜测enq应该有两个任务:1. 处理当前同步队列尾节点为null时进行入队操作;2. 如果CAS尾插入节点失败后负责自旋进行尝试。
4.111 enq()方法
看enq方法源码和注解吧:
/** * Inserts node into queue, initializing if necessary. See picture above. * @param node the node to insert * @return node's predecessor */ private Node enq(final Node node) { for (;;) { Node t = tail; if (t == null) { // Must initialize 如果是链表还没初始化的话(如果是初始化的话这里的if也是第一次循环) if (compareAndSetHead(new Node()))//设头结点,是个空的Node tail = head;//头和尾指针都是这个 } else { //链表不为空 node.prev = t; if (compareAndSetTail(t, node)) { //CAS尾插法,这里是个失败自旋的CAS操作 t.next = node; return t; } } } }
也就是在为这个为无法获得锁的线程构成结点,并放进等待队列的流程是这样的:
- 如果队列不为空,直接尝试一次CAS尾插,不行的话进入enq()方法里面会CAS自旋直至把这个线程代表的结点成功尾插入到同步链表尾部位置;
- 如果队列为空,则直接进入enq()方法,先进行一次tail和head指针的初始化,然后就是自旋CAS的尾插操作。
哦哦还有个小注意,这个enq方法返回的是node的前驱结点。
4.12 acquireQueued方法
final boolean acquireQueued(final Node node, int arg) {
接受一个Node,这个很参数很容易理解,就是要跟踪和处理这个线程的node嘛;而这个arg是给tryAcquire()方法准备的,上面说到,其具体的含义是由自己定义的。
然后这个方法是返回一个boolean的,如果这个线程的node在等待锁的时候,被interrupt了,就返回true。所以上面acquire()方法的最后一步是——如果acquireQueued成功了,就自我interrupt。
acquireQueued源码:
/** * Acquires in exclusive uninterruptible mode for thread already in * queue. Used by condition wait methods as well as acquire. * * @param node the node * @param arg the acquire argument * @return { @code true} if interrupted while waiting */ 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)) { //这个if告诉我们,只有是你的node排队排到第二个了,就下一个就队头了,才能再次看能不能获得锁,就用我们重写的tryAcquire来获取锁 setHead(node);//进来就是可以获得锁辽,那么就轮到这个node排队头了 p.next = null; // help GC emmm其实这个node还有pre指针指向之前那个p的吧,噢噢噢没事了,刚刚那个setHead里面把node的pre指针置空了 failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //这个if就显然是还没排队到可以获得锁的位置咯,就安排堵塞呗,然后第二个方法显然就和interrupt有关的,这个方法为真就会吧interrupted置为true interrupted = true; } } finally { if (failed)//应该是运行异常才会failed为true,不然的话应该在排队的最后置为false的,如果排队失败,或者说运行失败吧,就调用下面这个方法 cancelAcquire(node); } }
程序逻辑通过注释已经标出,整体来看这是一个这又是一个自旋的过程(for (;;)),代码首先获取当前节点的先驱节点,如果先驱节点是头结点的并且成功获得同步状态的时候(if (p == head && tryAcquire(arg))),当前节点所指向的线程能够获取锁。反之,获取锁失败进入等待状态。整体示意图为下图:
可以生动地理解成旋转排队去获得锁的过程。
关于替换头节点那里,setHead还有acquireQueued流程代码中涉及一些指针操作,这里一个图理解下:
/** * Checks and updates status for a node that failed to acquire. * Returns true if thread should block. This is the main signal * control in all acquire loops. Requires that pred == node.prev. * * @param pred node's predecessor holding status * @param node the node * @return { @code true} if thread should block */ 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) { //CANCELLED /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ 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. * 因为Condition是在condition队列里面采用的,所以这里就只可能是PROPAGATE和INITIAL了 */ compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } return false; }
这个方法接受两个Node作为参数,第一个node是第二个参数的pre结点,也就是第一个node排队是在第二个前面。
这个方法的看名字,意思就是,看看这个线程在尝试获得锁失败之后,是否要堵塞,如果需要block,则返回true。
然后代码的逻辑主要是针对这个pre做判断,我想大概是因为要看前面那个排队的结点处理到哪里了???
shouldParkAfterAcquire()的流程分析:
首先,这个方法的总目的是——要让pre结点的状态为SIGNAL,这个状态的意思是——pre结点的下一个要被block或者已经是blocked了,pre结点(自己)在他自己放锁的时候必须要做unpark操作来叫醒park的node(后面那个)。
1. 首先获取pre结点的状态;
2. 如果pre节点的状态是SIGNAL,那么已经达到目的了,node可以安详地堵塞去了。介个方法直接返回true
3. pre结点的状态大于0,也就是CANCELLED,那么就要改pre指针,跳过这些cancelled的node,直到找到一个不是cancelled的node作为新的pre结点。
3. pre结点为 0 or PROPAGATE,注意不会是CONDITON,因为这个是在condition队列里面用的,这个时候就需要用CAS操作把pre结点的状态改成SIGNAL。
如果CAS失败了,就会返回false,然后在acquireQueued方法中的if会因为false而跳出,然后进入for无限循环,再回到shouldParkAfterAcquire方法中,也就是其实是个自旋CAS操作。
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
刚刚分析的shouldParkAfterFailedAcquire方法呢,正常执行的话是应该返回true,也就是现在这个没获得锁的线程的node,已经符合可以进入block的状态了,所以显然这个parkAndCheckInterrupt的功能之一就是park这个node;根据名字还有一个功能应该是检查interrupt并做出相应吧。
来看看源码:
parkAndCheckInterrupt的源码:
/** * Convenience method to park and then check if interrupted * * @return { @code true} if interrupted */ private final boolean parkAndCheckInterrupt() { LockSupport.park(this); return Thread.interrupted();//注意哦,这个方法如果刚刚是interrupt的,会返回true,然后清除当前线程interrupt的状态 }
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) //这个if就显然是还没排队到可以获得锁的位置咯,就安排堵塞呗,然后第二个方法显然就和interrupt有关的,这个方法为真就会吧interrupted置为true interrupted = true;
做了以下几件事:
1. node获得锁失败了,要判断现在这个node可以进入park了吗,也就是要把node前面排队的结点的状态改成SIGNAL,这其实是个自旋CAS操作;
2. park(node);
3. parkAndCHeckInterrupt返回的是这个node线程是否被interrupt了,如果是的话,就将局部变量interrupted设为true
然后这个acquireQueued方法返回的其实就是这个interrupted变量,然后再acquire方法中,如果是true,就会进入if中进行
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt();selfInterrupt()方法,其实就是currentThread的一个interrupt操作。(那为何在上面的parkAndCheckInterrupt中还有把interrupt标志清除呢…………)
最后,还是acquireQueued方法(跳来跳去是不是有点乱),如果运行异常的话,进入finally块中failed就会是true,然后会调用
cancelAcquire(node);
这个方法看名字都直到是取消获得锁的资格,大概就应该是把这个node从等待队列中移除吧。来看看源码是不是。
4.123 cancelAcquire方法源码分析
直接看cancelAcquire源码先吧:
/** * Cancels an ongoing attempt to acquire. * * @param node the node */ private void cancelAcquire(Node node) { // Ignore if node doesn't exist if (node == null) return; node.thread = null; // Skip cancelled predecessors //将pred指向node前面的第一个非cancelled的node //大概是想让node前面的一个正常的和node后面正常的相连接吧 Node pred = node.prev; while (pred.waitStatus > 0) node.prev = pred = pred.prev; // predNext is the apparent node to unsplice. CASes below will // fail if not, in which case, we lost race vs another cancel // or signal, so no further action is necessary. Node predNext = pred.next; // Can use unconditional write instead of CAS here. // After this atomic step, other Nodes can skip past us. // Before, we are free of interference from other threads. node.waitStatus = Node.CANCELLED; // If we are the tail, remove ourselves. if (node == tail && compareAndSetTail(node, pred)) { //如果node是尾巴了,就把前面的那个正常的node作为新的tail //node,expect,update,第一个应该是想要改next指针的node吧,第二个是它的next指针的expect值吧,然后因为是tail了,所以next设置成null嘛, //操作完,新的tail也就是本来node前面的那个正常的node后面的cancel应该都不可达了,可以gc了吧 compareAndSetNext(pred, predNext, null); } else { //node后面还有node,说明后面有需要唤醒的node // If successor needs signal, try to set pred's next-link // so it will get one. Otherwise wake it up to propagate. int ws; if (pred != head && ((ws = pred.waitStatus) == Node.SIGNAL || (ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) && pred.thread != null) { //这个if就,把pred搞成SIGNAL,然后node后面那个如果是正常的就把pred和这个node后面的相连接 Node next = node.next; if (next != null && next.waitStatus <= 0) compareAndSetNext(pred, predNext, next); } else { //要么pred是头节点了,那么就可以唤醒node后面那个了(因为后面那个肯定signal);其他情况不是很懂 unparkSuccessor(node); } node.next = node; // help GC } }
emmm这个方法看得有点抓鸡……注解也一般般
4.2 独占锁的释放——release()方法
release方法源码:
/** * Releases in exclusive mode. Implemented by unblocking one or * more threads if { @link #tryRelease} returns true. * This method can be used to implement method { @link Lock#unlock}. * * @param arg the release argument. This value is conveyed to * { @link #tryRelease} but is otherwise uninterpreted and * can represent anything you like. * @return the value returned from { @link #tryRelease} */ public final boolean release(int arg) { if (tryRelease(arg)) { //看你的同步器的逻辑,现在可以释放锁吗(注意是独占所哦) Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; }
这个逻辑就很简答了,如果可以根据你的同步器逻辑可以释放,就特么唤醒后面排队的结点,不可以释放就返回false。
所以关键就看看unparkSuccessor方法
4.21 unparkSuccessor方法源码:
/** * Wakes up node's successor, if one exists. * * @param node the node */ private void unparkSuccessor(Node node) { /* * If status is negative (i.e., possibly needing signal) try * to clear in anticipation of signalling. It is OK if this * fails or if status is changed by waiting thread. */ int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0);//说可以的话,就把状态复原为initial就0,如果失败或者被等待的线程改了状态也没关系喔 /* * Thread to unpark is held in successor, which is normally * just the next node. But if cancelled or apparently null, * traverse backwards from tail to find the actual * non-cancelled successor. */ Node s = node.next; if (s == null || s.waitStatus > 0) { //头节点的下一个为CANCELLED为1大于0;或者根本为null s = null; //从tail往前遍历,找到一个正常的可以唤醒的node赋值给s for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } if (s != null) LockSupport.unpark(s.thread); }
这个也很简单,就首先尝试改变一下这个head结点的状态,不过这个成不成功好像都没关系(想想也是,都完成任务了……);然后唤醒头节点后面第一个(一般就是下一个)非null且没有被CANCELLED的结点。
明白了这个,再结合上面的独占所的acquire,就对AQS独占锁的模板方法的底层清楚多了。
参考文章
https://www.jianshu.com/p/7a65ab32de2a——《初识Lock与AbstractQueuedSynchronizer(AQS)》
https://www.jianshu.com/p/cc308d82cc71——《深入理解AbstractQueuedSynchronizer(AQS)》