美文网首页
Java 并发 AQS 重入锁

Java 并发 AQS 重入锁

作者: 大树懒呵 | 来源:发表于2020-03-16 23:34 被阅读0次

    实现重入锁ReentrantLock锁使用到的技术

    1. CAS 保证操作原子性
    2. AQS 带有头尾节点的队列 链表实现
    Node {
    //Node代表了等待的线程
      Node prev 前一个Node
      Node next 后面一个Node
    }
    
    AQS{
    Node head 头节点
    Node tail 尾结点
    }
    
    1. 自旋操作

    用到的一些方法的含义(公平锁实现情况)(代码为Open-JDK13中的)

    1. tryAcquire 本线程尝试获取锁,获取成功返回true
    2. hasQueuedPredecessors 判断本线程是否需要排队,需要排队返回true
    3. addWaiter 往队列中添加一个Node 即添加一个排队的线程

    源码具体执行流程
    步骤一:

    上锁.png
    步骤二: 获取锁.png
    步骤三:
    获取锁具体过程.png
    这里开始麻烦起来了,第一步本线程第一次尝试获取锁,调用 tryAcquire, 如果获取成功,则上锁过程结束,如果获取失败就执行另外两步。
    先看下tryAcquire干什么了(为公平锁实现的tryAcquire)
     protected final boolean tryAcquire(int acquires) {
                final Thread current = Thread.currentThread();
                获取锁的状态
                int c = getState();
                如果状态为0,说明此时锁时空闲的
                但是注意可能有多个线程同时检测到锁空闲,那么就会有多个线程都进入下面的IF
                if (c == 0) {
                    if (!hasQueuedPredecessors() &&
                        compareAndSetState(0, acquires)) {
                        setExclusiveOwnerThread(current);
                        return true;
                    }
                }
             ... 省略一部分代码,先不讨论
                return false;
            }
    

    如果是空闲状态,那么就要先看看是不是要排队(因为可能有多个线程同时,非公平锁就不用检测要不要排队)

        public final boolean hasQueuedPredecessors() {
            Node h, s;     h表示头结点,s表示后续节点
            如果h==null 那么就说明此此时队列都没创建 本线程是第一个线程,说明没有其他线程,就**不用排队**
            if ((h = head) != null) {
                s==null说明下一个没有 但也可能因为并发的原因 不能保证后面也是空的
                s.waitStatus>0 说明下一个被取消了,要看后面的是什么情况
                if ((s = h.next) == null || s.waitStatus > 0) {
                    s = null; // traverse in case of concurrent cancellation
                    for (Node p = tail; p != h && p != null; p = p.prev) {   从后往前找
                        找到一个不是null的也不是被取消的
                        也就是找到队列中第一个真正在等待的
                        if (p.waitStatus <= 0)
                            s = p;
                    }
                }
                不用排队情况(人即线程)
                    情况一:根本没有在等待,前面可能有人,也可能没有人
                    情况二:有在等待的,但是等待的人是自己,前面肯定有人
                要排队的情况
                    有在真正等待的,并且第一个真正在等待的不是自己
                  s==null 就说明没有等待的   **不用排队** 队列已经初始化过了
                  s!=null就说明有等待的,
                        等待的人不是自己    **要排队**
                        等待的人是自己   **不用排队** 这里会比较困惑,因为这个情形的出现要结合后面的**acquireQueued方法来看**
                      因为如果你是第一个等待者,那么acquireQueued会再调用一次tryAcquire,而tryAcquire会再调用一次hasQueuedPredecessors,
                      那么这种情形就会出现了即等待的人是自己
                if (s != null && s.thread != Thread.currentThread())
                    return true;
            }
            return false;
        }
    
    

    这个判断排队比较绕。。。
    然后就是回到了之前的tryAcquire了,如果不需要排队,那么直接用CAS操作尝试修改锁的状态,如果修改成功那么获取锁就成功了。
    如果不需要带队,但是获取锁失败了那就说明实际上还是需要排队的,因为这种情况就说明了存在多线程竞争
    如果需要排队就要调用addWaiter往队列中添加等待者,即排队,这个方法内部就不展开了,就是入队,它用CAS操作保证了入队顺序是线程安全的。 注意排好队后,线程还没有阻塞。
    排好队后队列中排队的人看看自己能不能获取锁,如果不能就阻塞线程,直到被唤醒

        final boolean acquireQueued(final Node node, int arg) {
            boolean interrupted = false;
            try {
                死循环,直到获取锁为止,当然获取失败就会阻塞,直到被唤醒,然后再尝试获取 如此循环
                for (;;) {
                    final Node p = node.predecessor();
                    情形一 加入队列后如果自己是第一个等待的那么就在试着获取一次锁,
                            原因是再自己第一次尝试获取锁失败加入队列后的这段时间,拥有锁的那个线程可能释放了锁
                    更后面的等待者是没有机会尝试获取锁的
                    情形二阻塞后被唤醒
                    说明自己是第一个等待的 再尝试获取锁
                    if (p == head && tryAcquire(arg)) {
                        setHead(node);
                        p.next = null; // help GC
                        return interrupted;
                    }
                   决定是否阻塞  根据它前面的人的状态
                    if (shouldParkAfterFailedAcquire(p, node))
                        interrupted |= parkAndCheckInterrupt();  阻塞,并且阻塞醒来也是从这里开始
                }
            } catch (Throwable t) {
                cancelAcquire(node);
                if (interrupted)
                    selfInterrupt();
                throw t;
            }
        }
    

    shouldParkAfterFailedAcquire这个方法也是比较令人困惑的地方,也是我自己理解还不到位的地方

    这个方法其实会执行两遍,第一遍是把前面等待者(如果有的话)的等待状态设置好(设置成阻塞等待)
    第二遍再确定自己是否需要阻塞
    如果你都因为获取锁失败
     private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
          前面一个等待者的等待状态
            int ws = pred.waitStatus;
            说明前面一个已经在阻塞等待了,那么自然我也就要阻塞等待了
            if (ws == Node.SIGNAL)
                return true;
            if (ws > 0) {
                 你前面的等待者(等待线程)可能被取消了 找到一个没有被取消的
                do {
                    node.prev = pred = pred.prev;
                } while (pred.waitStatus > 0);
                pred.next = node;
            } else {
                第一次执行 把前面的等待者状态设置好,因为它肯定已经阻塞等待
                pred.compareAndSetWaitStatus(ws, Node.SIGNAL);
            }
            return false;
        }
    

    这里说的比较乱。。。
    为什么要后面的人来设置前面的人的阻塞状态呢,因为一个线程真正阻塞了,那么它的阻塞状态才能设置,即先有阻塞,然后有阻塞状态,但是阻塞的线程就根本不能动了,自然也不能设置阻塞状态。
    拿睡觉来打个比方,你睡着还是没睡着不能由你说了算,你不可能告诉别人你睡着了,只能由别人来看,只有别人才能看到你是否真正睡着了。
    并且这段有解锁过程相关性挺高的,以后有时间再说了。


    这篇文章主要是为了记录自己在学习AQS时候的一些感受,也方便以后自己复习。所以用到的语言也不够准确,过程不能保证正确。如果有同学看到这篇文章,一定要带着怀疑的心态去看。如果发现错误的地方希望能批评指出。

    相关文章

      网友评论

          本文标题:Java 并发 AQS 重入锁

          本文链接:https://www.haomeiwen.com/subject/qitaehtx.html