【java并发】AQS中acquire方法解析

       AQS,全名AbstractQueuedSynchronizer(抽象队列同步器),它是CLH(不明白的可以先了解一下CLH)的变种。它与CLH不同之处在于:
       CLH是一种公平锁,它是通过自旋同步队列中节点的前驱结点状态,判断同步队列中的节点是否能够进入临界区;AQS的同步队列中的节点不会以自旋的方式来进入临界区,而是先以公平或者不公平的方式尝试进入临界区,如果不能,则进行阻塞,等待被唤醒,再去尝试是否能够进入临界区。


       AQS属于队列,那么就是一个一个节点连接而成,在AQS中节点的数据结构如下:

static final class Node {
      
        static final Node SHARED = new Node();
      
        /** 独占锁模式 */
        static final Node EXCLUSIVE = null;
        
        /**
         * AQS中判断节点是否为取消状态,有时候是判断状态值是否大于零
         */
        static final int CANCELLED =  1;
      
        /** 
         * 如果节点是这个状态,
         * 那么该节点就需要唤醒它的后继节点 
         */
        static final int SIGNAL    = -1;
        
        /**
         * 不明白,但是不影响对acquire方法的理解
         */
        static final int CONDITION = -2;
        
         /**
         * 不明白,但是不影响对acquire方法的理解
         */
        static final int PROPAGATE = -3;
		
		/** 
		 * 值为:-3 , -2 , -1 , 0 , 1 
		 */
        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) {
            this.waitStatus = waitStatus;
            this.thread = thread;
        }
    }

可以看出来AQS同步队列是一个双向链表结构。需要说明的是除了头节点是进入到临界区的节点,其他大部分节点(有一部分节点可能被取消了,waitStatus=1)都是希望进入临界区的节点。


acquire方法解析

       AQS中的acquire方法,是获取独占锁方法,它的代码很简单,代码主体只有一个if判断语句:

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

可以看到acquire方法实现,主要是下面这三个方法

  1. tryAcquire(arg)
  2. addWaiter(Node.EXCLUSIVE)
  3. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

这三个方法的作用

  1. tryAcquire(arg)
    顾名思义,它就是尝试获取锁,AQS在这里没有对其进行功能的实现,只有一个抛出异常的语句,用户可以对其重写实现公平锁、不公平锁、可重入锁、不可重入锁
protected boolean tryAcquire(int arg) {
        throw new UnsupportedOperationException();
}
  1. addWaiter(Node.EXCLUSIVE)
    一旦尝试获取锁未成功,就要使用该方法将其加入同步队列尾部
private Node addWaiter(Node mode) {
       Node node = new Node(Thread.currentThread(), mode);
       
       // 快速尝试加入到同步队列队尾
       Node pred = tail;
       if (pred != null) {
           node.prev = pred;
           if (compareAndSetTail(pred, node)) {
               pred.next = node;
               return node;
           }
       }
       
       // 如果快速尝试没有成功,则自旋加入队尾,直到成功加入
       enq(node);
       return node;
}

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;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
        }
 }

由于可能有多个线程并发加入队尾产生竞争,因此,采用compareAndSetTail无锁方法来保证同步

  1. acquireQueued(addWaiter(Node.EXCLUSIVE), arg)
    一旦加入同步队列,就需要使用该方法,自旋阻塞唤醒来不断的尝试获取锁,直到被中断或获取到锁
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; // 帮助GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) /** 尝试获取锁失败是否阻塞线程获取锁 */ &&
                    parkAndCheckInterrupt() /** 阻塞 */)
                    interrupted = true;
            }
        } finally {
            if (failed)
                cancelAcquire(node);
        }
}

前面说过,当一个节点的前驱节点的waitStatus=SIGNAL,当其前驱结点释放锁的时候需要对其后继节点进行唤醒。shouldParkAfterFailedAcquire方法的功能就是判断该节点的前驱结点的waitStatus==SIGNAL,如果相等则该节点可以阻塞,否则将该节点的前驱结点waitStatus状态修改为SIGNAL。由此可以知道,该节点如果没有获取到锁,AQS就会尽最大努力(为什么说最大努力,而不是一定会将节点阻塞呢?可以思考一下,为什么,shouldParkAfterFailedAcquire里面能找到答案)将该节点阻塞,之后等待前驱结点唤醒,再尝试获取锁。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章