private Node addWaiter(Node mode){ Node node = new Node(Thread.currentThread(), mode); // Try the fast path of enq; backup to full enq on failure Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); return node; }
Node(Thread thread, Node mode) { // Used by addWaiter this.nextWaiter = mode; this.thread = thread; }
private Node enq(final Node node){ for (;;) { Node t = tail; if (t == null) { // 判断队列是否为空,空队列应该先初始化头节点 if (compareAndSetHead(new Node())) tail = head;//头尾共同指向头结点 } else {//CAS添加并允许失败, 走for(;;) node.prev = t; if (compareAndSetTail(t, node)) { t.next = node; return t;//返回新插入节点的前置节点 } } } } ```
入队完成后看下`acquireQueued` 逻辑:
`failed` 标记最终acquire是否成功, `interrupted`标记是否曾被挂起过。注意到for(;;) 跳出的唯一条件就是`if (p == head && tryAcquire(arg))` 即当前线程结点是头结点且获取锁成功。从这里我们应该看到,这是一个线程第三次又想着尝试快速获取锁:虽然此时该节点已被加入等待队列,在进行睡眠之前又通过`p == head && tryAcquire(arg)`方法看看能否获取锁。也就是说只有该线程结点的所有 有效的前置结点都拿到过锁了,当前结点才有机会争夺锁,如果失败了那就通过`shouldParkAfterFailedAcquire`方法判断是否应该挂起当前结点,等待响应中断。观察 每次调用`tryAcquire`方法的时机,可以看出作者优化意图:
1. 尽量在没入队的时候拿到锁,避免过多队列操作维护成本 2. 尽量在睡眠前拿到锁,避免过多上下文切换 ```java finalbooleanacquireQueued(final Node node, int arg){ boolean failed = true; try { boolean interrupted = false; //如果第一次循环就获取成功那么返回的interrupted是false,不需要自我中断。 //否则 说明在获取到同步状态之前经历过挂起(返回true)。 for (;;) {// 获取前置结点 final Node p = node.predecessor(); //如果刚入队的尚未被挂起的节点的前置节点是头节点,那么此节点线程有必要尝试一下获取锁,因为head很可能是 刚初始化的 dummy head,或者 会预设head很快释放锁 if (p == head && tryAcquire(arg)) { setHead(node); //设置新的head p.next = null; // help GC failed = false; return interrupted; // fase 因为刚入队还没挂起就拿到锁了 } //走到这里说明前置节点不为head或者抢锁失败了 //判断当前node线程是否可挂起,是,就调用parkAndCheckInterrupt挂起 ,interrupted设置为true,表示曾经被中断过 if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) interrupted = true; } } finally { if (failed) //如果tryAcquire出现异常那么取消当前结点的获取,毕竟tryAcquire是留给子类实现的,谁知道弄出啥幺蛾子 cancelAcquire(node); } } ```
看下`shouldParkAfterFailedAcquire` 方法的逻辑: ```java privatestaticbooleanshouldParkAfterFailedAcquire(Node pred, Node node){ int ws = pred.waitStatus; if (ws == Node.SIGNAL) /* * Predecessor was cancelled. Skip over predecessors and * indicate retry. */ /* 如果前置结点是SIGNAL状态, 那么当前置结点执行完成后是可以唤醒后续结点的, 此时可以安全的挂起当前结点, 不需要进行不必要的for(;;),前置结点自然会通知。 */ returntrue; if (ws > 0) { // 如果ws>0说明前置结点是被自己取消获取同步的结点(只有线程本身可以取消自己)。 //那么do while循环一直往头结点方向找waitStatus < 0的节点; //含义就是去除了FIFO队列中的已经自我取消申请同步状态的线程。 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. */ //如果是其它状态waitStatus要不是0或着PROPAGATE,意味着当前结点需要一个signal但是尚未park, // 所以调用者必须确保在挂起之前不能获取同步状态。 // 并强行设置前置结点为SIGNAL。之所以CAS设置是因为,pred线程也会操作cancelAcquire来取消自己和node线程对pred的操作产生竞争条件。 compareAndSetWaitStatus(pred, ws, Node.SIGNAL); } returnfalse; }
int ws = node.waitStatus; if (ws < 0)//同样是因为node线程和当前线程有竞争,node本身也可以修改自身状态嘛,因此CAS compareAndSetWaitStatus(node, ws, 0); Node s = node.next; if (s == null || s.waitStatus > 0) { s = null; //发现这里竟然从tail结点往前找最靠近head结点的且处于非取消状态的结点?这不增加了遍历复杂度么? 留个疑问下面解释! for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) s = t; } //s是后继结点 因为后继结点很可能是满足条件的, 如果满足即s!=NULL 直接unpark后继结点。 if (s != null) LockSupport.unpark(s.thread); }