AbstractQueuedSynchronizer 个人解析
1、个人总结和看法以及一些问题:
因为我喜欢总结一些,所以每次分析的时候我会先说自己的总结和一些看法,希望大家能够指正,因为是更多的想让自己记录下自己的想法,所以我会以自己的方式来叙述,等有时间了我会重新整理格式。
AQS通常也称为队列同步器,它其实是一个抽象类,简单来说就是规定了一些行为你只是需要去实现自己特定的行为就可以了就像ReentrantLock也是利用了AQS,这个后面再说。
(1)AQS底层数据结构是链表并且它实现了Serializable接口,可以序列化,但是它的底层分为了Sync Queue和Condition Quene两个队列,Sync Queue是一个双向链表,包含了head和tail节点。Condition Queue是一个单向链表,它不一定会被使用,它可能在使用lock锁时作为条件使用,但是Sync Queue一定会使用,两者的关系后面会详细的叙述。
(2)Sync Queue的头节点为什么为空?
关于这个问题,我之前想的原因可能是因为它的头节点只是作为一个标记使用,但是原因并不是这么简单,
Also nulls out unused fields for sake of GC(源码的注释)大意是防止泄漏,我的估计是防止线程对象泄漏和释放头节点的引用
最后和朋友讨论了这个问题,分析师线程也是一个对象,AQS中的锁实现其实就是通过park线程来实现的,说白了就是挂起所有竞争的线程每次只允许一个或者多个,因为是头节点开始唤醒,所以头节点的线程对象就可以释放了,所以这也就解释了为什么构造Node节点时需要线程对象作为属性,因为后面需要挂起它们。
(3)为什么在进行节点唤醒的时候,如果后继节点为cancel状态的时候需要从尾节点开始唤醒?
我的见解是因为在canlcelAcquire方法中取消节点时有一步是node.next=node,相当于就是让jvm来回收自己了,这样如果节点唤醒时是cancel状态的话,就需要从尾部开始唤醒了,因为要找到一个signal节点唤醒。从尾部唤醒还有个好处就是可以重新整理队列,将cancel节点全部移除。
(4)队列节点中的状态是什么时候设置的?是谁设置的?
我们可以从源码中看到,每个节点都有一个状态,但是这并不是在构造节点的时候设置的,而是加入队列以后由后继节点设置的。
2、源码分析部分
AbstractOwnableSynchronizer源代码
1 public abstract class AbstractOwnableSynchronizer 2 implements java.io.Serializable { 3 4 独占的线程 5 private transient Thread exclusiveOwnerThread; 6 7 设置独占的线程 8 protected final void setExclusiveOwnerThread(Thread thread) { 9 exclusiveOwnerThread = thread; 10 } 11 12 获取当前独占的线程 13 protected final Thread getExclusiveOwnerThread() { 14 return exclusiveOwnerThread; 15 } 16 }
AQS继承自AOS里面有些方法都是直接使用的。
因为AQS的两个数据结构之前已经说了Sync Queue和Condition Queue,分别对应的AQS对象的Node内部类和ConditionObject内部类,我主要分析一下Node内部类吧
Node节点的源代码
static final class Node { 节点的模式 static final Node SHARED = new Node(); static final Node EXCLUSIVE = null; 节点的状态 (取消) static final int CANCELLED = 1; 节点的状态 (有效)表示后面有节点需要unpark 因为它的状态就是后面节点标记的 static final int SIGNAL = -1; 这个是为conditon条件控制服务的 static final int CONDITION = -2; 为共享锁服务的 static final int PROPAGATE = -3; 节点的状态 volatile int waitStatus; 前趋节点 volatile Node prev; 后继节点 volatile Node next; 构造节点的线程 volatile Thread thread; 下一个等待者 Node nextWaiter; 是否是共享模式 final boolean isShared() { return nextWaiter == SHARED; } 获取前一节点 final Node predecessor() throws NullPointerException { Node p = prev; if (p == null) throw new NullPointerException(); else return p; } Node() { 用于初始化 } Node(Thread thread, Node mode) { 用于添加队列的构造函数 this.nextWaiter = mode; this.thread = thread; } Node(Thread thread, int waitStatus) { // Used by Condition this.waitStatus = waitStatus; this.thread = thread; } }
重要方法源代码分析 (按照逻辑顺序进行分析)
首先是acqure方法分析
public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); }
我借用了一位大佬的图来体现这个流程
分析:首先是调用tryAcqure() 如果失败了就调用addWaiter(),并且通过acquireQueued来唤醒节点。
tryAcquire()默认是抛出异常 需要子类自己实现,反正可以理解为设置独占线程持有锁就可以了,大致就是这个逻辑,也就是说后面的逻辑都是在设置独占锁的时候失败进行的。我们进行
addWaiter()源码分析
private Node addWaiter(Node mode) { //首先是构造一个节点,因为你竞争失败了只能加入同步队列等待 Node node = new Node(Thread.currentThread(), mode); Node pred = tail; if (pred != null) { // 设置前趋节点 node.prev = pred; // CAS设置尾节点 if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } //如果CAS失败就进行enq强行如队操作 enq(node); return node; }
enq()源代码分析
private Node enq(final Node node) { 死循环 不断的入队直到成功 for (;;) { Node t = tail; if (t == null) { // Must initialize 如果队列还没有初始化就新建头节点 因为前面没有入队成功的原因可能是这个 if (compareAndSetHead(new Node())) tail = head; } else { node.prev = t; 还是CAS 设置尾部 反正新加入的节点就设置为尾节点 if (compareAndSetTail(t, node)) { t.next = node; return t; } } } }
接下来是acquireQueued()源码分析
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)) { 设置当前节点为头节点 并释放头节点 setHead(node); p.next = null; // help GC failed = false; return interrupted; } 如果不是头节点或者竞争锁失败了 就查看是不是应该在竞争失败后挂起 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { //如果失败了 就设置节点在队列中状态为取消状态 不参与竞争了 if (failed) cancelAcquire(node); } }
每一个线程加入队列的时候,都会通过addWaiter返回本身,此时再判断自己是不是头节点,想再争取一次,如果不是那就只能挂起了。
shouldParkAfterFailedAcquire()源码分析
1 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { 2 首先获取前一节点的状态 3 int ws = pred.waitStatus; 4 if (ws == Node.SIGNAL) 5 如果为singal 则表示可以挂起了(其实是它自己帮前一节点设置的 让前一节点变成了singnal相当于第二次进入这个方法) 6 return true; 7 if (ws > 0) { 8 说明前一节点是取消状态 9 do { 10 node.prev = pred = pred.prev; 11 } while (pred.waitStatus > 0); 12 pred.next = node; 13 } else { 14 15 为其他状态就设置为signal状态 16 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 17 } 18 return false; 19 }
这里由于acquireQueued()里面的一个死循环,所有一般节点会进来两次,所以这也就是我们之前说的节点前面的状态是后面节点设置的。
parkAndCheckInterrupt()源码分析
1 private final boolean parkAndCheckInterrupt() { 2 //挂起这个线程 也就呼应了我们之前开头提出的几个问题 3 LockSupport.park(this); 4 return Thread.interrupted(); 5 }
这个方法是用来挂起线程的。
刚刚说的都是获取锁的过程 现在来分析释放锁的过程
release()方法源码分析
1 public final boolean release(int arg) { 2 //释放 成功返回true 3 if (tryRelease(arg)) { 4 Node h = head; 5 if (h != null && h.waitStatus != 0) 6 //如果节点不为null且状态不为0就唤醒后继节点 7 unparkSuccessor(h); 8 return true; 9 } 10 return false; 11 }
unparkSuccessor() 方法源码分析
1 private void unparkSuccessor(Node node) { 2 //获取节点状态 状态值小于0 就设置为0 这是等待状态 3 int ws = node.waitStatus; 4 if (ws < 0) 5 compareAndSetWaitStatus(node, ws, 0); 6 7 //获取后继节点 8 Node s = node.next; 9 //如果后继节点状态大于0 10 if (s == null || s.waitStatus > 0) { 11 s = null; 12 //从尾部开始唤醒 原因我们一开头就已经说明了 13 for (Node t = tail; t != null && t != node; t = t.prev) 14 if (t.waitStatus <= 0) 15 s = t; 16 } 17 //唤醒节点 18 if (s != null) 19 LockSupport.unpark(s.thread); 20 }
核心:为什么后继节点为取消状态需要从尾部开始唤醒,我们已经在开头分析过原因了,因为后继节点如果为取消状态,那么它的后继节点指向自己,不能指向下一节点了。只能从尾部开始 ,这样还能清除状态为取消状态的节点。
总结:我主要分析的是独占模式下的锁获取和释放,中断的分析还没有,后面会陆续的补上。