背景:一个线程加了两次锁,可重入释放锁的过程

  1. public void unlock() {
  2. sync.release(1);
  3. }
  4. public final boolean release(int arg) {
  5. //1.尝试释放锁
  6. if (tryRelease(arg)) {
  7. //2.唤醒线程流程
  8. Node h = head;
  9. if (h != null && h.waitStatus != 0)
  10. unparkSuccessor(h);
  11. return true;
  12. }
  13. return false;
  14. }

1.tryRelease(arg),尝试释放锁

  1. //ReentrantLock
  2. protected final boolean tryRelease(int releases) {
  3. //1
  4. int c = getState() - releases;
  5. //2
  6. if (Thread.currentThread() != getExclusiveOwnerThread())
  7. throw new IllegalMonitorStateException();
  8. boolean free = false;
  9. //3
  10. if (c == 0) {
  11. free = true;
  12. setExclusiveOwnerThread(null);
  13. }
  14. //4
  15. setState(c);
  16. return free;
  17. }

第一释放锁

  1. int c = getState() - releases; 这里getState()为2,因为第一个线程加了锁,releases为1
  2. Thread.currentThread() != getExclusiveOwnerThread();当前线程不是加锁线程,说明不是当前线程加锁反而来释放锁,如果能释放就是不对的
  3. 由于现在state减了1还是1,那么这个判断进不去,如果能进去就代表释放锁成功了
  4. 更改state的值

第二次释放锁

  1. int c = getState() - releases; 这里getState()为1,因为第一个线程加了锁,releases为1
  2. 该判断不进入
  3. 进入该if,同时free设置为true并返回。

    2.唤醒线程流程

    1. public final boolean release(int arg) {
    2. if (tryRelease(arg)) {
    3. //唤醒线程流程
    4. Node h = head;
    5. if (h != null && h.waitStatus != 0)
    6. unparkSuccessor(h);
    7. return true;
    8. }
    9. return false;
    10. }
  4. Node h = head; 搞一个中间变量h 指向head节点

image.png

  1. unparkSuccessor(h);

    1. int ws = node.waitStatus;,head节点的status,此时是个signal
    2. compareAndSetWaitStatus(node, ws, 0); 就可以执行这个操作,把head节点的status设置成0
    3. Node s = node.next;拿到第一个等待线程节点,也就是线程2节点
    4. 由于s不是null,就能执行LockSupport.unpark(s.thread);,这个方法就传入了s节点中的Thread进行唤醒

      1. private void unparkSuccessor(Node node) {
      2. /*
      3. * If status is negative (i.e., possibly needing signal) try
      4. * to clear in anticipation of signalling. It is OK if this
      5. * fails or if status is changed by waiting thread.
      6. */
      7. int ws = node.waitStatus;
      8. if (ws < 0)
      9. compareAndSetWaitStatus(node, ws, 0);
      10. /*
      11. * Thread to unpark is held in successor, which is normally
      12. * just the next node. But if cancelled or apparently null,
      13. * traverse backwards from tail to find the actual
      14. * non-cancelled successor.
      15. */
      16. Node s = node.next;
      17. if (s == null || s.waitStatus > 0) {
      18. s = null;
      19. for (Node t = tail; t != null && t != node; t = t.prev)
      20. if (t.waitStatus <= 0)
      21. s = t;
      22. }
      23. if (s != null)
      24. LockSupport.unpark(s.thread);
      25. }
  2. 此时,切回到线程2阻塞的代码中

    1. 唤醒后,线程2就在这个for循环中,此时获取p一定时head,因此就开始执行tryAcquire开始获取锁

image.png

为什么从尾部开始唤醒

既然采用了从尾部遍历的逻辑,那么肯定是为了解决可能会出现的问题。而这个问题就在enq(…)方法中:

  1. private Node enq(final Node node) {
  2. for (;;) {
  3. Node t = tail;
  4. if (t == null) { // Must initialize
  5. //队列为空需要初始化,创建空的头节点
  6. if (compareAndSetHead(new Node()))
  7. tail = head;
  8. } else {
  9. node.prev = t;
  10. //set尾部节点
  11. if (compareAndSetTail(t, node)) {//当前节点置为尾部
  12. t.next = node; //前驱节点的next指针指向当前节点
  13. return t;
  14. }
  15. }
  16. }
  17. }

在该段方法中,将当前节点置于尾部使用了CAS来保证线程安全,但是请注意:在if语句块中的代码并没有使用任何手段来保证线程安全!

也就是说,在高并发情况下,可能会出现这种情况:

线程A通过CAS进入if语句块之后,发生上下文切换,此时线程B同样执行了该方法,并且执行完毕。然后线程C调用了unparkSuccessor方法。

  1. 线程A执行CAS将当前节点置为尾部:

image.png

  1. 原本线程A要执行t.next = node;将node2的next设置为node3,但是,此时发生上下文切换,时间片交由线程B,也就是说,此时node2的next还是null
  2. 线程B执行enq逻辑,最终CLH队列如图所示:

image.png

  1. 此时发生上下文切换,时间片交由线程C,线程C调用了unparkSuccessor方法,假如是从头到尾的遍历形式,在node2就会发现,next指针为null,似乎没有后续节点了。

其最根本的原因在于:
node.prev = t;先于CAS执行,也就是说,你在将当前节点置为尾部之前就已经把前驱节点赋值了,自然不会出现prev=null的情况