学习笔记-理解锁中的同步队列AQS

概念

  队列同步器AbstractQueuedSynchronizer,也就是我们经常说的AQS,他是java并发工具包里面的一个对象,我们在使用lock的实现类的时候,会经常看到他的身影。大多都是使用它来构建锁或者其他同步组件的基础框架,他使用一个volatile修改的int成员变量来表示同步状态,通过内置一个Node对象来完成资源获取线程的排队工作,这个Node其本质也就是一个FIFO(先进先出)队列。AQS的主要使用方式是通过继承,子类继承AQS,然后实现它的抽象方法来管理同步状态,在实现抽象方法的过程中都是避免不了要对同步状态进行修改的,这个时候就需要使用同步器提供的3个方法:getState(),setState(int newState),compareAndSetState(int expect,int update) 来进行操作,因为这个三个方法能够保证状态的修改是安全的。并且我的这个子类通常都是被定义在同步组件的内部静态类的方法方式来使用,而同步器它自己是没有实现任何同步接口的,他仅仅是定义了几个同步状态的获取和释放来提供给同步组件使用。

同步器的接口与实现

  AQS的设计是其实基于模板方法模式来的,使用者需要继承AQS并重写指定的方法,随后将同步器组合在自定义同步组件的实现中,来调用AQS提供的模板方法,而这些方法将会调用使用者重写的方法。重写同步器指定的方法时,需要使用同步器提供的如下3个方法来访问或修改同步状态。1,getState():获取当前同步状态。2,setState(int newState):设置当前同步状态。3,compareAndSetState(int expect,int update):使用CAS设置当前状态,该方法能够保证状态设置的原子性。下面是自己写的一个简单同步器,也是顺便理解一下使用方式。

 1 import java.util.concurrent.TimeUnit;
 2 import java.util.concurrent.locks.AbstractQueuedSynchronizer;
 3 import java.util.concurrent.locks.Condition;
 4 import java.util.concurrent.locks.Lock;
 5 /**
 6  * @author yk
 7  * @version 1.0
 8  * @describe SimpleMutex
 9  * @date 2017-05-22 12:00
10  */
11 public class SimpleMutex implements Lock {
12     //静态内部类,自定义同步器
13     private static class Sync extends AbstractQueuedSynchronizer{
14         //是否处于占用状态
15         protected boolean isHeldExclusively()
16         {
17             return getState()==1;
18         }
19         // 当状态为0的时候获取锁
20         public boolean tryAcquire(int acquires)
21         {
22             if(compareAndSetState(0,1))
23             {
24                 setExclusiveOwnerThread(Thread.currentThread());
25                 return true;
26             }
27             return false;
28         }
29         //释放锁将状态修改为0
30         protected boolean tryRelease(int releases) {
31             if (getState() == 0) {
32                 throw new IllegalMonitorStateException();
33             }
34             setExclusiveOwnerThread(null);
35             setState(0);
36             return true;
37         }
38         // 返回一个Condition,每个condition都包含了一个condition队列
39         Condition newCondition() {
40             return new ConditionObject();
41         }
42     }
43     // 仅需要将操作代理到Sync上
44     private final  Sync sync=new Sync();
45 
46     @Override
47     public void lock() {
48         sync.acquire(1);
49     }
50 
51     @Override
52     public void lockInterruptibly() throws InterruptedException {
53         sync.acquireInterruptibly(1);
54     }
55 
56     @Override
57     public boolean tryLock() {
58         return sync.tryAcquire(1);
59     }
60 
61     @Override
62     public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
63         return sync.tryAcquireSharedNanos(1,unit.toNanos(time));
64     }
65 
66     @Override
67     public void unlock() {
68         sync.tryRelease(0);
69     }
70 
71     @Override
72     public Condition newCondition() {
73         return sync.newCondition();
74     }
75 
76     public boolean isLocked()
77     {
78         return sync.isHeldExclusively();
79     }
80 
81     public boolean hasQueuedThreads()
82     {
83         return sync.hasQueuedThreads();
84     }
85 }
上述示例中,独占锁Mutex是一个自定义同步组件,它在同一时刻只允许一个线程占有锁。Mutex中定义了一个静态内部类,该内部类继承了同步器并实现了独占式获取和释放同步状态。在tryAcquire(int acquires)方法中,如果经过CAS设置成功(同步状态设置为1),则代表获取了同步状态,而在tryRelease(int releases)方法中只是将同步状态重置为0。用户使用Mutex时并不会直接和内部同步器的实现打交道,而是调用Mutex提供的方法,在Mutex的实现中,以获取锁的lock()方法为例,只需要在方法实现中调用同步器的模板方法acquire(int args)即可,当前线程调用该方法获取同步状态失败后会被加入到同步队列中等待,这样就大大降低了实现一个可靠自定义同步组件的门槛。
  刚我们说了,在同步器的内部依赖了一个FIFO同步队列来完成同步状态的管理的,当前线程获取同步状态失败时,同步器会把当前线程以及等待状态构造成一个Node节点加入到队列中,同时阻塞这个线程,当同步状态释放的时候,会把首节点中的线程唤醒,使其再次获取同步状态。首先看下Node数据结构,以及属性和描述,这样可以对Node这个了解的也更清晰一些。
 1 static final class Node {
 2     //表示线程以共享的模式等待锁
 3     static final Node SHARED = new Node();
 4     //表示线程正在以独占的方式等待锁
 5     static final Node EXCLUSIVE = null;
 6 
 7     //当前节点由于超时或中断被取消
 8     static final int CANCELLED =  1;
 9     //表示当前节点的前节点被阻塞
10     static final int SIGNAL    = -1;
11     //当前节点在等待condition
12     static final int CONDITION = -2;
13     //状态需要向后传播
14     static final int PROPAGATE = -3;
15 
16     /**
17      * 当前节点在队列中的状态,他有五个枚举值:
18      * 0    当一个Node被初始化的时候的默认值
19      * CANCELLED    为1,表示线程获取锁的请求已经取消了
20      * CONDITION    为-2,表示节点在等待队列中,节点线程等待唤醒
21      * PROPAGATE    为-3,当前线程处在SHARED情况下,该字段才会使用
22      * SIGNAL    为-1,表示线程已经准备好了,就等资源释放了
23      */
24     volatile int waitStatus;
25     //前驱节点
26     volatile Node prev;
27     //后继节点
28     volatile Node next;
29     //表示处于该节点的线程
30     volatile Thread thread;
31     //指向下一个处于CONDITION状态的节点
32     Node nextWaiter;
33 
34     final boolean isShared() {
35         return nextWaiter == SHARED;
36     }
37     //返回前驱节点,没有的话抛出npe
38     final Node predecessor() throws NullPointerException {
39         Node p = prev;
40         if (p == null)
41             throw new NullPointerException();
42         else
43             return p;
44     }
45 
46     Node() {
47     }
48 
49     Node(Thread thread, Node mode) {
50         this.nextWaiter = mode;
51         this.thread = thread;
52     }
53 
54     Node(Thread thread, int waitStatus) {
55         this.waitStatus = waitStatus;
56         this.thread = thread;
57     }
58 }
节点是构成同步队列的基础,同步器拥有首节点(head)和尾节点(tail),没有成功获取同步状态的线程将会成为节点加入该队列的尾部,同步队列的基本结构如图:
上图是包含了两个节点的引用,一个指向头结点,一个指向尾部节点。试想一下,当一个线程成功的获取到了同步状态,其他线程将无法获取到同步状态,转而被构造成节点接入到同步队列中,而这个加入队列的过程必须是要保证线程安全的。因此同步器也是提供了一个基于CAS的设置尾节点的方法compareAndSetTail(Node expect,Node update),它需要传递当前线程“认为”的尾节点和当前节点,只有设置成功后,当前节点才正式与之前的尾节点建立关联。下面这个图是同步器将节点加入到同步队列的过程:
同步队列是要遵循FIFO的,首节点是成功获取同步状态的节点,首节点在释放同步状态的时候,将会唤醒后续节点,而后续节点将会在获取同步状态成功时将自己设置为首节点。该过程如下图:
设置首节点是获取同步状态成功的线程来完成的。由于是只有一个线程能够获取到同步状态,因此设置首部节点的方法并不需要使用CAS来保证。它只需要将首节点设置成为原首节点的后继节点并断开原首节点的next引用即可。
  我们通过调用同步器的acquire(int arg)方法可以获取同步状态。
1     public final void acquire(int arg) {
2         if (!tryAcquire(arg) &&
3             acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
4             selfInterrupt();
5     }

这个代码主要是为了完成同步状态的获取,节点构造,加入同步队列以及在同步队列中自旋。其中主要逻辑:首先调用自定义同步器实现的tryAcquire方法,这个方法保证了线程安全的获取同步状态,如果同步状态获取失败,则构造一个节点,并且通过addWaiter把这个节点加入到同步队列的尾部。最后调用acquireQueued,使这个节点以死循环的方式获取同步状态。如果获取不到则阻塞节点中的线程,而这些被阻塞的线程的唤醒主要依靠前驱节点的出队或阻塞线程的中断来实现。

  接下来我们看下同步器的addWaiter和enq方法。

  

 1 private Node addWaiter(Node mode) {
 2         Node node = new Node(Thread.currentThread(), mode);
 3         // 快速尝试在尾部添加
 4         Node pred = tail;
 5         if (pred != null) {
 6             node.prev = pred;
 7             if (compareAndSetTail(pred, node)) {
 8                 pred.next = node;
 9                 return node;
10             }
11         }
12         enq(node);
13         return node;
14 }
15 
16 private Node enq(final Node node) {
17         for (;;) {
18             Node t = tail;
19             if (t == null) { // 初始化队列
20                 if (compareAndSetHead(new Node()))
21                     tail = head;
22             } else {
23                 // t是尾节点,把为节点设置为当前节点的前驱节点,然后在下一步用CAS把当前节点设置成尾节点,设置成功之后,再把当之前的尾节点的下一个节点设置成当前节点,这样,当前节点就标称了尾节点
24                 node.prev = t;
25                 if (compareAndSetTail(t, node)) {
26                     t.next = node;
27                     return t;
28                 }
29             }
30         }
31 }
上述代码通过使用compareAndSetTail(Node expect,Node update)方法来确保节点能够被线程安全添加到队列中。试想一下:如果使用一个普通的LinkedList来维护节点之间的关系,那么当一个线程获取了同步状态,而其他多个线程由于调用tryAcquire(int arg)方法获取同步状态失败而并发地被添加到LinkedList时,LinkedList将难以保证Node的正确添加,最终的结果可能是节点的数量有偏差,而且顺序也是混乱的。在enq(final Node node)方法中,同步器通过“死循环”来保证节点的正确添加,在“死循环”中只有通过CAS将节点设置成为尾节点之后,当前线程才能从该方法返回,否则,当前线程不断地尝试设置。可以看出,enq(final Node node)方法将并发添加节点的请求通过CAS变得“串行化”了。
  节点进入同步队列之后,就进入了一个自旋的过程,每个节点(或者说每个线程)都在自省地观察,当条件满足,获取到了同步状态,就可以从这个自旋过程中退出,否则依旧留在这个自旋过程中(并会阻塞节点的线程),如代码所示:
 1     final boolean acquireQueued(final Node node, int arg) {
 2         boolean failed = true;
 3         try {
 4             boolean interrupted = false;
 5             for (;;) {
 6                 final Node p = node.predecessor();
 7                 if (p == head && tryAcquire(arg)) {
 8                     setHead(node);
 9                     p.next = null; // help GC
10                     failed = false;
11                     return interrupted;
12                 }
13                 if (shouldParkAfterFailedAcquire(p, node) &&
14                     parkAndCheckInterrupt())
15                     interrupted = true;
16             }
17         } finally {
18             if (failed)
19                 cancelAcquire(node);
20         }
21     }
 1 private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
 2         int ws = pred.waitStatus;
 3         if (ws == Node.SIGNAL)
 4             return true;
 5         if (ws > 0) {         
 6             do {
 7                 node.prev = pred = pred.prev;
 8             } while (pred.waitStatus > 0);
 9             pred.next = node;
10         } else {
11             compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
12         }
13         return false;
14 }

在acquireQueued(final Node node,int arg)方法中,当前线程在“死循环”中尝试获取同步状态,而只有前驱节点是头节点才能够尝试获取同步状态,这是为什么?原因有两个,第一,头节点是成功获取到同步状态的节点,而头节点的线程释放了同步状态之后,将会

唤醒其后继节点,后继节点的线程被唤醒后需要检查自己的前驱节点是否是头节点。第二,维护同步队列的FIFO原则。该方法中,节点自旋获取同步状态的行为如图
由于非首节点线程前驱节点出队或者被中断而从等待状态返回,随后检查自己的前驱是否是头节点,如果是则尝试获取同步状态。可以看到节点和节点之间在循环检查的过程中基本不相互通信,而是简单地判断自己的前驱是否为头节点,这样就使得节点的释
放规则符合FIFO,并且也便于对过早通知的处理(过早通知是指前驱节点不是头节点的线程由于中断而被唤醒)。 
  所以AQS的acquire(int arg)的方法流程大致为:前驱节点为头节点且能够获取同步状态的判断条件和线程进入等待状态是获取同步状态的自旋过程。当同步状态获取成功之后,当前线程从acquire(int arg)方法返回,如果对于锁这种并发组件而言,代表着当前线程获取了锁。 当前线程获取同步状态并执行了相应逻辑之后,就需要释放同步状态,使得后续节点能够继续获取同步状态。通过调用同步器的release(int arg)方法可以释放同步状态,该方法在释放了同步状态之后,会唤醒其后继节点(进而使后继节点重新尝试获取同步状态)。代码如下
1     public final boolean release(int arg) {
2         if (tryRelease(arg)) {
3             Node h = head;
4             if (h != null && h.waitStatus != 0)
5                 unparkSuccessor(h);
6             return true;
7         }
8         return false;
9     }
该方法执行时,会唤醒头节点的后继节点线程,unparkSuccessor(Node node)方法使用LockSupport(在后面的章节会专门介绍)来唤醒处于等待状态的线程。
总结
  在获取同步状态时,同步器维护一个同步队列,获取状态失败的线程都会被加入到队列中并在队列中进行自旋;移出队列(或停止自旋)的条件是前驱节点为头节点且成功获取了同步状态。在释放同步状态时,同步器调用tryRelease(int arg)方法释放同步状态,然后唤醒头节点的后继节点。
 
posted @ 2017-05-22 15:57  小杨ABC  阅读(1148)  评论(0编辑  收藏  举报