JDK1.8 AbstractQueuedSynchronizer的实现分析(学习笔记)

来源:互联网 发布:人工智能第三版答案 编辑:程序博客网 时间:2024/06/08 19:39

lock方法会调用acquire方法,该方法在AQS中实现

 public final void acquire(int arg) {        if (!tryAcquire(arg) &&            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))            selfInterrupt();    }

正常使用一个ReentrantLock 的lock() 方法时,在不能获得所得情况下,该方法是阻塞的,对吧
以上acquire方法拆解一下方法调用
首先tryAcquire(arg),英语不好的我Acquire,百度给出的意思是获得,获取,那么好这里可以给成意思是尝试获得的意思?
如果申请资源失败,则返回false,那么会先调用addWaiter(Node.EXCLUSIVE),这里的Node.EXCLUSIVE代表独占的意思,so这个节点是独占的?

/**     * Creates and enqueues node for current thread and given mode.     *     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared     * @return the new node     */    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) { //如果当前链表有尾节点,就把封装当前线程的节点追加到尾部?这里的尾节点操作也是CAS的            node.prev = pred;             if (compareAndSetTail(pred, node)) {                pred.next = node;                return node;            }        }        // 如果没有等待的线程(节点列表为null),则初始化头尾节点,该方法是一个自旋的方法        enq(node);        return node;    }

这里考虑多线程调用的情况,这里的链表不存在线程安全问题,因为对每个头尾节点的操作都是CAS的。详见代码片段里的
compareAndSetHead和compareAndSetTail方法。

/**     * 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;            //第一次的时候尾节点肯定是一个 null,则初始话一个空内容节点为头节点,第二次循环时尾节点已经不是null了            if (t == null) { // Must initialize                if (compareAndSetHead(new Node()))                    tail = head;            } else {                node.prev = t; //将node节点的前驱节点设置成尾节点,即先尝试追加到链表的尾部,如果尾节点是t,则将尾节点设置成node,退出循环                if (compareAndSetTail(t, node)) {                     t.next = node;                    return t;                }            }        }    }

所以第一个等待锁的线程A在AQS中的数据结构是这样的?

空节点 -> node(name:A)

看到这里,AQS中对内部链表的头尾操作都是CAS的,所以链表中的节点排序也是完全按照申请锁的顺序排列的。addWaiter方法的操作无非是将线程封装为一个node,追加该节点到链表的尾节点,然后addWaiter 方法返回封装好的node方法,继续调用acquireQueued(node) ,
acquireQueued 方法是一个自旋的方法,假设当前只有一个线程A排队申请锁,目前链表形式为。

空节点 -> node(name:A)

/**     * 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();                //获得p的前驱节点,并判断是否为head节点,如果为head节点且成功获取到资源,就将当前线程节点设置成队列的头节点,并返回                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);        }    }

这里判断p==head,如果node的前驱节点是head,说明该节点排成了老二,而老二如果成功获取到资源,则变成老大,那么这里老大的起到的是什么作用?
>

我的程序有一把锁在外边阻塞中(sleep),从代码上看一开始tryAcquire方法就成功申请到资源,就不会有他的node维护在这里,就是说他的锁不需要在这里排队?那么后续的unlock是怎样的一个机制?
因为锁在别的线程中持有(state>0),所以这个acquireQueued方法中的tryAcquire会返回false
然后shouldParkAfterFailedAcquire,字面意思如果申请资源失败则判断是否可以挂起线程

/**     * 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;        //如果是SIGNAL状态,返回true,第一次来这个肯定不会成立,因为是node的前驱节点是一个空节点。        //空节点->node(A)        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) { //Node.CANCLED 如果节点是被放弃的(什么情况下会放弃,中断么??)            /*             * 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.             */            //将前驱节点的状态设置成SIGNAL            compareAndSetWaitStatus(pred, ws, Node.SIGNAL);        }        return false;    }

至此针对我的测试,这个方法会走compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 分支,所以目前node(name:A)节点的前驱(head节点)的waitStatus状态为 SIGNAL,该方法的第一次调用会返回false
目前针对线程A的链表表示

node(waitStatus:SIGNAL)->node(name:A)

因为shouldParkAfterFailedAcquire返回了false,所以不会调用parkAndCheckInterrupt,那么会进入第二次循环。
node的前驱节点是head 这个是毫无疑问的,但是就是因为获取锁的线程还是一个熊孩子,没有释放资源(state>0)
所以依旧会调用shouldParkAfterFailedAcquire方法,但此时方法内部已经不一样了,这里已经开始满足第一个条件分支了,所以此方法此次会返回true,因为因为shouldParkAfterFailedAcquire返回了true,所以会接着调用parkAndCheckInterrupt()方法
该方法实现如下:

 /**     * Convenience method to park and then check if interrupted     *     * @return {@code true} if interrupted     */    private final boolean parkAndCheckInterrupt() {        LockSupport.park(this);//调用park()使线程进入waiting状态        return Thread.interrupted();//如果被唤醒,查看自己是不是被中断的。    }

LockSupport.park(this); 大神说park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。
如果当前线程waiting了,那么就不会有第三次循环了….因为当前线程已经waiting了……
这里阻塞的线程应该是申请锁的线程,LockSupport.park(this);参数的作用是对应的blocker会记录在Thread的一个parkBlocker属性中,通过jstack命令可以非常方便的监控具体的阻塞对象.

如果此时又有一个线程B,按照此逻辑继续申请一把锁,按AQS的处理逻辑会维护成如下形式

node(waitStatus:SIGNAL)->node(name:A)->node(name:B)

假设tryAcquire(arg)依旧失败,state依旧不给释放,熊孩子还不还,那么对于node(name:B) 来说,在acquireQueued中他的前驱肯定不是head节点,所以他只能走shouldParkAfterFailedAcquire方法设置前驱节点的状态,然后调用parkAndCheckInterrupt()方法,去阻塞当前当前申请锁的线程。
同上经历2次自旋后在AQS中链表的维护成如下形式

node(waitStatus:SIGNAL)->node(name:A,waitStatus:SIGNAL)->node(name:B)

这里贴一个大神的总结和图

再来总结下它的流程吧:
调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;
没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;
acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。
由于此函数是重中之重,我再用流程图总结一下:
图

现在AQS的链表里有三个节点。

node(waitStatus:SIGNAL)->node(name:A,waitStatus:SIGNAL)->node(name:B)

假设一开始长时间阻塞不还锁的线程,时间到了玩够了回家吃饭去,线程中调用了reentrantlock.unlock(); 方法,他玩够了我这边两个兄弟还在那waiting呢,是不是会释放他的线程标志
释放锁的代码也是很简单的

    public void unlock() {        sync.release(1);    }

这里sync是实现AQS的一个内部类Sync的实例,所以本质上还是在调用AQS的功能,看看怎么做的。

 /**     * 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) //头结点不为null,且头结点是有状态的?                unparkSuccessor(h);//唤醒等待队列里的下一个线程            return true;        }        return false;    }

由上文得知,调用AQS的acquire(int args)方法,会调用子类的tryAcquire(int args)实现从而实现对线程状态的重置,那么这里有一个tryRelease(arg) 是不是也是子类的一个实现呢?其实就是调用的子类实现,依旧不去管他的实现
暂且把tryRelease当作释放资源,如果释放成功则将头节点做为参数,调用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); //置零当前线程所在的结点状态,允许失败。        /*         * 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; //拿到链表中的第二个节点,本例是node(name:A,waitstatus:SIGNAL)         if (s == null || s.waitStatus > 0) { //如果没有第二个节点或者第二个节点的状态为取消            s = null;            for (Node t = tail; t != null && t != node; t = t.prev)  //从尾节点开始向前找有状态的节点并返回                if (t.waitStatus <= 0)                    s = t;        }        if (s != null)            LockSupport.unpark(s.thread); //唤醒线程    }

依据上面的逻辑本例中的node(name:A,waitstatus:SIGNAL) 节点所持有的线程将得到释放。但是这个节点并没有被移除。AQS中的链表结构变成了这样

node(waitstatus:0)->node(name:A,waitstatus:SIGNAL)->node(name:B)

此时线程A将得到唤醒,然后他会继续做final boolean acquireQueued(final Node node, int arg) 方法中的自旋。为方法继续把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();                //获得p的前驱节点,并判断是否为head节点,如果为head节点且成功获取到资源,就将当前线程节点设置成队列的头节点,并返回                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);        }    }

此时线程A发现他的前驱节点为head,并且此时再申请资源tryAcquire(arg) ,因为那个熊孩子把资源释放了,所以此时申请资源是成功的,然后会将当前节点设置为链表的头结点,并释放原来的头结点占用的内存。
这样链表结构就成了这个样子:

node(name:A,waitstatus:SIGNAL)->node(name:B)

针对线程A来说acquireQueued 方法已经执行完成,假设我们一直老老实实的等没有通知中断线程,则线程A中的代码行reentrantLock.lock() 方法将返回,一个代码行执行完成,会接着执行下一个代码行,lock() 加锁成功了!那么问题来了(问题和挖掘机没有任何关系)tryAcquire和tryRelease 到底在争抢和释放什么东西?
继续看tryAcquire方法在ReentrantLock类中的实现(公平锁)

   /**           * Fair version of tryAcquire.  Don't grant access unless           * recursive call or no waiters or is first.           */          protected final boolean tryAcquire(int acquires) {              //获得当前线程              final Thread current = Thread.currentThread();              //getState来自AQS,放方法取AQS中的全局变量state 注意是全局              int c = getState();              if (c == 0) { //AQS中默认state值为0,如果为0应该可以证明当前AQS队列里的线程没有修改过state,也就是说没有人持有锁                  if (!hasQueuedPredecessors() &&                      compareAndSetState(0, acquires)) {                      setExclusiveOwnerThread(current);                      return true;                  }              }              else if (current == getExclusiveOwnerThread()) {                  int nextc = c + acquires;                  if (nextc < 0)                      throw new Error("Maximum lock count exceeded");                  setState(nextc);                  return true;              }              return false;          }

可以发现tryAcquire 方法会先获取AQS中的全局变量,也可以说AQS维护的链表中的所有节点都会去检查state这个变量,所以这个state是一个线程状态,也可以看作是他们在抢的一把锁,如果发现state为0,则代表竞争到资源,并通过CAS的方式设置该值为1,然后设置当前线程为exclusiveOwnerThread,另一个条件因为锁是可重入的啊老铁
这里有一个hasQueuedPredecessors() 方法,参照某大神所说

正如hasQueuedPredecessors的注释所说,该方法的作用是为了避免 “线程闯入”,即对于ReentrantLock来说,即便是state为0时,AQS队列中也可能是有节点的(被取消的,打断的节点)等等,这个时候为了保证AQS队列的公平性,不再尝试加锁,而是返回false,到AQS的队列中去排队。所以,这也是该方法被用在公平锁中的原因。

遗留问题:
1.为什么用链表管理线程
2.节点的状态SIGNAL 主要含义
3.在AQS中的线程如何响应中断,中断策略是什么?

   /** waitStatus value to indicate thread has cancelled */    static final int CANCELLED =  1;    /** waitStatus value to indicate successor's thread needs unparking */    static final int SIGNAL    = -1;    /** waitStatus value to indicate thread is waiting on condition */    static final int CONDITION = -2;    /**     * waitStatus value to indicate the next acquireShared should     * unconditionally propagate     */    static final int PROPAGATE = -3;

引用:
https://www.cnblogs.com/waterystone/p/4920797.html

阅读全文
0 0