AQS原理剖析

来源:互联网 发布:免费开淘宝店 编辑:程序博客网 时间:2024/05/22 07:57

1、引言

在JDK1.5之前,一般是靠synchronized关键字来实现线程对共享变量的互斥访问。synchronized是在字节码上加指令,依赖于底层操作系统的Mutex Lock实现。

而从JDK1.5以后Java界的一位大神—— Doug Lea 开发了AbstractQueuedSynchronizer(AQS)组件,使用原生java代码实现了synchronized语义。换句话说,Doug Lea没有使用更“高级”的机器指令,也不依靠JDK编译时的特殊处理,仅用一个普普通通的类就完成了代码块的并发访问控制,比那些费力不讨好的实现不知高到哪里去了。

java.util.concurrent包有多重要无需多言,一言以蔽之,是Doug Lea大爷对天下所有Java程序员的怜悯。

AQS定义了一套多线程访问共享资源的同步器框架,是整个java.util.concurrent包的基石,Lock、ReadWriteLock、CountDowndLatch、CyclicBarrier、Semaphore、ThreadPoolExecutor等都是在AQS的基础上实现的。


1、实现原理

并发控制的核心是锁的获取与释放,锁的实现方式有很多种,AQS采用的是一种改进的CLH锁。


2.1 CLH锁

CLH(Craig, Landin, andHagersten locks)是一钟自旋锁,能确保无饥饿性,提供先来先服务的公平性。

何谓自旋锁?它是为实现保护共享资源而提出一种锁机制。其实,自旋锁与互斥锁比较类似,它们都是为了解决对某项资源的互斥使用。无论是互斥锁,还是自旋锁,在任何时刻,最多只能有一个保持者,也就是说,在任何时刻最多只能有一个执行单元获得锁。但是两者在调度机制上略有不同。对于互斥锁,如果资源已经被占用,资源申请者只能进入睡眠状态。但是自旋锁不会引起调用者睡眠,如果自旋锁已经被别的执行单元保持,调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,“自旋”一词就是因此而得名

CLH锁是一种基于链表的可扩展、高性能、公平的自旋锁,申请线程只在本地变量上自旋,它不断轮询前驱的状态,如果发现前驱释放了锁就结束自旋。

CLH队列中的结点QNode中含有一个locked字段,该字段若为true表示该线程需要获取锁,且不释放锁,为false表示线程释放了锁。结点之间是通过隐形的链表相连,之所以叫隐形的链表是因为这些结点之间没有明显的next指针,而是通过myPred所指向的结点的变化情况来影响myNode的行为。CLHLock上还有一个尾指针,始终指向队列的最后一个结点。


当一个线程需要获取锁时,会创建一个新的QNode,将其中的locked设置为true表示需要获取锁,然后使自己成为队列的尾部,同时获取一个指向其前趋的引用myPred,然后该线程就在前趋结点的locked字段上旋转,直到前趋结点释放锁。当一个线程需要释放锁时,将当前结点的locked域设置为false,同时回收前趋结点。如上图所示,线程A需要获取锁,其myNode域为true,些时tail指向线程A的结点,然后线程B也加入到线程A后面,tail指向线程B的结点。然后线程A和B都在它的myPred域上旋转,一旦它的myPred结点的locked字段变为false,它就可以获取锁。


2.2 AQS数据模型

AQS维护了一个volatile int state(代表共享资源)和一个FIFO线程等待队列(多线程争用资源被阻塞时会进入此队列)。


AQS的内部队列是CLH同步锁的一种变形。其主要从两方面进行了改造,节点的结构与节点等待机制:

l   在结构上引入了头结点和尾节点,分别指向队列的头和尾,尝试获取锁、入队列、释放锁等实现都与头尾节点相关,

l   为了可以处理timeout和cancel操作,每个node维护一个指向前驱的指针。如果一个node的前驱被cancel,这个node可以前向移动使用前驱的状态字段

l   在每个node里面使用一个状态字段来控制阻塞/唤醒,而不是自旋

l   head结点使用的是傀儡结点

 

FIFO队列中的节点有AQS的静态内部类Node定义:

[java] view plain copy
 print?
  1. static final class Node {  
  2.    
  3.     // 共享模式  
  4.     static final Node SHARED = new Node();  
  5.    
  6.     // 独占模式  
  7.     static final Node EXCLUSIVE = null;  
  8.    
  9.     static final int CANCELLED = 1;  
  10.     static final int SIGNAL = -1;  
  11.     static final int CONDITION = -2;  
  12.     static final int PROPAGATE = -3;  
  13.    
  14.     /** 
  15.      * CANCELLED,值为1,表示当前的线程被取消 
  16.      * SIGNAL,值为-1,表示当前节点的后继节点包含的线程需要运行,也就是unpark; 
  17.      * CONDITION,值为-2,表示当前节点在等待condition,也就是在condition队列中; 
  18.      * PROPAGATE,值为-3,表示当前场景下后续的acquireShared能够得以执行; 
  19.      * 值为0,表示当前节点在sync队列中,等待着获取锁。 
  20.      */  
  21.     volatile int waitStatus;  
  22.    
  23.     // 前驱结点  
  24.     volatile Node prev;  
  25.    
  26.     // 后继结点  
  27.     volatile Node next;  
  28.    
  29.     // 与该结点绑定的线程  
  30.     volatile Thread thread;  
  31.    
  32.     // 存储condition队列中的后继节点  
  33.     Node nextWaiter;  
  34.    
  35.     // 是否为共享模式  
  36.     final boolean isShared() {  
  37.         return nextWaiter == SHARED;  
  38.     }  
  39.    
  40.     // 获取前驱结点  
  41.     final Node predecessor() throwsNullPointerException {  
  42.         Node p = prev;  
  43.         if (p == null)  
  44.             throw new NullPointerException();  
  45.         else  
  46.             return p;  
  47.     }  
  48.    
  49.     Node() { // Used to establish initial heador SHARED marker  
  50.     }  
  51.    
  52.     Node(Thread thread, Node mode) { // Used byaddWaiter  
  53.         this.nextWaiter = mode;  
  54.         this.thread = thread;  
  55.     }  
  56.    
  57.     Node(Thread thread, int waitStatus) { //Used by Condition  
  58.         this.waitStatus = waitStatus;  
  59.         this.thread = thread;  
  60.     }  
  61. }  

Node类中有两个常量SHARE和EXCLUSIVE,顾名思义这两个常量用于表示这个结点支持共享模式还是独占模式,共享模式指的是允许多个线程获取同一个锁而且可能获取成功,独占模式指的是一个锁如果被一个线程持有,其他线程必须等待。多个线程读取一个文件可以采用共享模式,而当有一个线程在写文件时不会允许另一个线程写这个文件,这就是独占模式的应用场景。


2.2 CAS操作

AQS有三个重要的变量:

   

[java] view plain copy
 print?
  1. // 队头结点  
  2. private transient volatile Node head;  
  3.   
  4. // 队尾结点  
  5. private transient volatile Node tail;  
  6.   
  7. // 代表共享资源  
  8. private volatile int state;  
  9.   
  10. protected final int getState() {  
  11.     return state;  
  12. }  
  13.   
  14. protected final void setState(int newState){  
  15.     state = newState;  
  16. }  
  17.   
  18. protected final boolean compareAndSetState(int expect, int update) {  
  19.     return unsafe.compareAndSwapInt(this,stateOffset, expect, update);  
  20. }  

compareAndSetState方法是以乐观锁的方式更新共享资源。

独占锁是一种悲观锁,synchronized就是一种独占锁,会导致其它所有需要锁的线程挂起,等待持有锁的线程释放锁。而另一个更加有效的锁就是乐观锁。所谓乐观锁就是,每次不加锁而是假设没有冲突而去完成某项操作,如果因为冲突失败就重试,直到成功为止。乐观锁用到的机制就是CAS,即Compare And Swap。

CAS 指的是现代 CPU 广泛支持的一种对内存中的共享数据进行操作的一种特殊指令。这个指令会对内存中的共享数据做原子的读写操作。简单介绍一下这个指令的操作过程:

首先,CPU 会将内存中将要被更改的数据与期望的值做比较。然后,当这两个值相等时,CPU 才会将内存中的数值替换为新的值。否则便不做操作。最后,CPU 会将旧的数值返回。

这一系列的操作是原子的。它们虽然看似复杂,但却是 Java 5 并发机制优于原有锁机制的根本。简单来说,CAS 的含义是“我认为原有的值应该是什么,如果是,则将原有的值更新为新值,否则不做修改,并告诉我原来的值是多少”。

CAS通过调用JNIJava Native Interface)调用实现的。JNI允许java调用其他语言,而CAS就是借助C语言来调用CPU底层指令实现的。Unsafe是CAS的核心类,它提供了硬件级别的原子操作

Doug Lea大神在java同步器中大量使用了CAS技术,鬼斧神工的实现了多线程执行的安全性。CAS不仅在AQS的实现中随处可见,也是整个java.util.concurrent包的基石。

 

可以发现,head、tail、state三个变量都是volatile的。

volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。可见性的意思是当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。

volatile变量也存在一些局限:不能用于构建原子的复合操作,因此当一个变量依赖旧值时就不能使用volatile变量。而CAS呢,恰恰可以提供对共享变量的原子的读写操作。

volatile保证共享变量的可见性,CAS保证更新操作的原子性,简直是绝配!把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式:

1.       首先,声明共享变量为volatile;

2.       然后,使用CAS的原子条件更新来实现线程之间的同步;

3.       同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如下:



3、源码解读

前面提到过,AQS定义两种资源共享方式:

l   Exclusive(独占,只有一个线程能执行,如ReentrantLock)

l   Share(共享,多个线程可同时执行,如Semaphore/CountDownLatch)。

不同的自定义同步器争用共享资源的方式也不同。自定义同步器在实现时只需要实现共享资源state的获取与释放方式即可,至于具体线程等待队列的维护(如获取资源失败入队/唤醒出队等),AQS已经在顶层实现好了。自定义同步器实现时主要实现以下几种方法:

l   isHeldExclusively():该线程是否正在独占资源。只有用到condition才需要去实现它。

l   tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。

l   tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。

l   tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。

l   tryReleaseShared(int):共享方式。尝试释放资源,成功则返回true,失败则返回false。

 

以ReentrantLock为例,state初始化为0,表示未锁定状态。A线程lock()时,会调用tryAcquire()独占该锁并将state+1。此后,其他线程再tryAcquire()时就会失败,直到A线程unlock()到state=0(即释放锁)为止,其它线程才有机会获取该锁。当然,释放锁之前,A线程自己是可以重复获取此锁的(state会累加),这就是可重入的概念。但要注意,获取多少次就要释放多么次,这样才能保证state是能回到零态的。

再以CountDownLatch以例,任务分为N个子线程去执行,state也初始化为N(注意N要与线程个数一致)。这N个子线程是并行执行的,每个子线程执行完后countDown()一次,state会CAS减1。等到所有子线程都执行完后(即state=0),会unpark()主调用线程,然后主调用线程就会从await()函数返回,继续后余动作。

一般来说,自定义同步器要么是独占方法,要么是共享方式,他们也只需实现tryAcquire-tryRelease、tryAcquireShared-tryReleaseShared中的一种即可。但AQS也支持自定义同步器同时实现独占和共享两种方式,如ReentrantReadWriteLock。


3.1 acquire(int)

[java] view plain copy
 print?
  1. public final void acquire(int arg) {  
  2.     if (!tryAcquire(arg) &&acquireQueued(addWaiter(Node.EXCLUSIVE), arg))  
  3.         selfInterrupt();  
  4. }  

此方法是独占模式下线程获取共享资源的顶层入口。如果获取到资源,线程直接返回,否则进入等待队列,直到获取到资源为止,且整个过程忽略中断的影响。获取到资源后,线程就可以去执行其临界区代码了。

函数流程如下:

 

1、tryAcquire()尝试直接去获取资源,如果成功则直接返回;

2、addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;

3、acquireQueued()使线程在等待队列中获取资源,一直获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。

4、如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。

下面再来看看每个方法的实现代码。


3.1.1 tryAcquire(int)

[java] view plain copy
 print?
  1. protected boolean tryAcquire(int arg) {  
  2.     throw new UnsupportedOperationException();  
  3. }  

此方法尝试去获取独占资源。如果获取成功,则直接返回true,否则直接返回false。

AQS只是一个框架,在这里定义了一个接口,具体资源的获取交由自定义同步器去实现了(通过state的get/set/CAS),至于能不能重入,能不能加塞,那就看具体的自定义同步器怎么去设计了。当然,自定义同步器在进行资源访问时要考虑线程安全的影响。

这里之所以没有定义成abstract,是因为独占模式下只用实现tryAcquire-tryRelease,而共享模式下只用实现tryAcquireShared-tryReleaseShared。如果都定义成abstract,那么每个模式也要去实现另一模式下的接口。说到底,Doug Lea还是站在咱们开发者的角度,尽量减少不必要的工作量。


3.1.2 addWaiter(Node)

[java] view plain copy
 print?
  1. private Node addWaiter(Node mode) {  
  2.      // 使用当前线程构造结点  
  3.      Node node = new Node(Thread.currentThread(),mode);  
  4.       
  5.      Node pred = tail;  
  6.      if (pred != null) {  //如果队尾结点不为空,将当前节点插入队尾  
  7.          node.prev = pred;  
  8.          if (compareAndSetTail(pred, node)){  
  9.              pred.next = node;  
  10.              return node;  
  11.          }  
  12.      }  
  13.      // 队尾结点为空(队列还没有初始化),则转调enq入队  
  14.      enq(node);  
  15.      return node;  
  16.  }  

其中,compareAndSetTail方法也是调用Unsafe类实现CAS操作,更新队尾。


3.1.3 enq(Node)

[java] view plain copy
 print?
  1. private Node enq(final Node node) {  
  2.     for (;;) {  //CAS自旋,直到插入成功  
  3.         Node t = tail;  
  4.         if (t == null) { // 队尾为空,则先初始化队列,new一个傀儡节点  
  5.             if (compareAndSetHead(newNode()))  
  6.                 tail = head;  //头尾指针都指向傀儡节点  
  7.         } else {  // 插入队尾  
  8.             node.prev = t;  
  9.             if (compareAndSetTail(t, node)){  
  10.                 t.next = node;  
  11.                 return t;  
  12.             }  
  13.         }  
  14.     }  
  15. }  

这段代码的精髓就在于CAS自旋volatile变量,也是AtomicInteger、AtomicBoolean等原子量的灵魂。

3.1.4 acquireQueued(Node, int)

通过tryAcquire()和addWaiter(),如果线程获取资源失败,已经被放入等待队列尾部了。但是,后面还有一项重要的事没干,就是让线程进入阻塞状态,直到其他线程释放资源后唤醒自己。过程跟在银行办理业务时排队拿号有点相似,acquireQueued()就是干这件事:在等待队列中排队拿号(中间没其它事干可以休息),直到拿到号后再返回。

[java] view plain copy
 print?
  1. final boolean acquireQueued(final Nodenode, int arg) {  
  2.     boolean failed = true;  // 是否获取到了资源  
  3.     try {  
  4.         boolean interrupted = false;  //等待过程中有没有被中断  
  5.         for (;;) {  //自旋,直到  
  6.             final Node p = node.predecessor();  
  7.             // 前驱是head,则有资格去尝试获取资源  
  8.             if (p == head && tryAcquire(arg)) {  
  9.                 // 获取资源成功,将自己置为队头,并回收其前驱(旧的队头)  
  10.                 setHead(node);  
  11.                 p.next = null// help GC  
  12.                 failed = false;  
  13.                 return interrupted;  
  14.             }  
  15.             // 获取资源失败,  
  16.             if (shouldParkAfterFailedAcquire(p,node) &&  
  17.                 parkAndCheckInterrupt())  
  18.                 interrupted = true;  
  19.         }  
  20.     } finally {  
  21.         if (failed)  
  22.             cancelAcquire(node);  
  23.     }  
  24. }  

如果获取资源失败后,会调用两个函数,shouldParkAfterFailedAcquire和parkAndCheckInterrupt,下面来看看它俩是干什么的。

3.1.5 shouldParkAfterFailedAcquire(Node,Node)

从名字可以猜出来,该函数的作用是“在获取资源失败后是否需要阻塞”:

[java] view plain copy
 print?
  1. private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {  
  2.     int ws = pred.waitStatus;  // 前驱状态  
  3.     if (ws == Node.SIGNAL)  
  4.         // Node.SIGNAL,代表前驱释放资源后会通知后继结点  
  5.         return true;  
  6.     if (ws > 0) {  // 代表前驱已取消任务,相当于退出了等待队列  
  7.         do {  // 一个个往前找,找到最近一个正常等待的前驱,排在它的后面  
  8.             node.prev = pred = pred.prev;  
  9.         } while (pred.waitStatus > 0);  
  10.         pred.next = node;  
  11.     } else {  
  12.         // 前驱状态正常,则将其状态置为SIGNAL,意为,释放资源后通知后继结点  
  13.         compareAndSetWaitStatus(pred, ws, Node.SIGNAL);  
  14.     }  
  15.     return false;  
  16. }  

整个流程中,如果前驱结点的状态不是SIGNAL,那么自己就不能安心去休息,需要去找个安心的休息点,同时可以再尝试下看有没有机会轮到自己拿号。

3.1.6 parkAndCheckInterrupt()

如果线程找好安全休息点后,那就可以安心去休息了。此方法就是让线程去休息,真正进入等待状态。

[java] view plain copy
 print?
  1. private final boolean parkAndCheckInterrupt() {  
  2.     LockSupport.park(this);  // 使线程进入waiting状态  
  3.     return Thread.interrupted();  
  4. }  

park()会让当前线程进入waiting状态。在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。

3.1.7 小结

总结下acquire的流程:

1、调用自定义同步器的tryAcquire()尝试直接去获取资源,如果成功则直接返回;

2没成功,则addWaiter()将该线程加入等待队列的尾部,并标记为独占模式;

3、acquireQueued()使线程在等待队列中休息,有机会时(轮到自己,会被unpark())会去尝试获取资源。获取到资源后才返回。如果在整个等待过程中被中断过,则返回true,否则返回false。

4、如果线程在等待过程中被中断过,它是不响应的。只是获取资源后才再进行自我中断selfInterrupt(),将中断补上。



3.2 release(int)

release()是acquire()的逆操作,是独占模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。

[java] view plain copy
 print?
  1. public final boolean release(int arg) {  
  2.     if (tryRelease(arg)) {  
  3.         Node h = head;  
  4.         if (h != null &&h.waitStatus != 0)  //状态不为0,证明需要唤醒后继结点  
  5.             unparkSuccessor(h);   
  6.         return true;  
  7.     }  
  8.     return false;  
  9. }  

3.2.1 tryRelease(int)

[java] view plain copy
 print?
  1. protected boolean tryRelease(int arg) {  
  2.     throw new UnsupportedOperationException();  
  3. }  

跟tryAcquire()一样,这个方法是需要自定义同步器去实现的。正常来说,tryRelease()都会成功的,因为这是独占模式,该线程来释放资源,那么它肯定已经拿到独占资源了,直接减掉相应量的资源即可,也不需要考虑线程安全的问题。

3.2.2 unparkSuccessor(Node)

[java] view plain copy
 print?
  1. private void unparkSuccessor(Node node) {  
  2.      
  3.     int ws = node.waitStatus;  
  4.     if (ws < 0)  // 将当前结点状态置零  
  5.         compareAndSetWaitStatus(node, ws,0);  
  6.   
  7.     Node s = node.next;  
  8.     if (s == null || s.waitStatus > 0){  //后继结点为空或者已取消  
  9.         s = null;  
  10.         // 从队尾开始向前寻找,找到第一个正常的后继结点  
  11.         for (Node t = tail; t != null&& t != node; t = t.prev)  
  12.             if (t.waitStatus <= 0)  
  13.                 s = t;  
  14.     }  
  15.     if (s != null)  
  16.         LockSupport.unpark(s.thread);  //唤醒该结点上的线程  
  17. }  

逻辑并不复杂,一句话概括:用unpark()唤醒等待队列中最前边的那个未放弃线程。


3.2    acquireShared(int)

此方法是共享模式下线程获取共享资源的顶层入口。它会获取指定量的资源,获取成功则直接返回,获取失败则进入等待队列,直到获取到资源为止,整个过程忽略中断。

[java] view plain copy
 print?
  1. public final void acquireShared(int arg) {  
  2.     if (tryAcquireShared(arg) < 0)  
  3.         doAcquireShared(arg);  
  4. }  
  5.   
  6. protected int tryAcquireShared(int arg) {  //留给子类实现  
  7.     throw new UnsupportedOperationException();  
  8. }  

这里tryAcquireShared()依然需要自定义同步器去实现。但是AQS已经把其返回值的语义定义好了:负值代表获取失败;0代表获取成功,但没有剩余资源;正数表示获取成功,还有剩余资源,其他线程还可以去获取。

3.2.1 doAcquireShared(int)

[java] view plain copy
 print?
  1. private void doAcquireShared(int arg) {  
  2.     final Node node =addWaiter(Node.SHARED);  //以共享模式加入队尾  
  3.     boolean failed = true;  
  4.     try {  
  5.         boolean interrupted = false;  
  6.         for (;;) {  
  7.             final Node p =node.predecessor();  
  8.             if (p == head) {  // 前驱是队头(队头肯定是已经拿到资源的结点)  
  9.                 int r =tryAcquireShared(arg);  // 尝试获取资源  
  10.                 if (r >= 0) {  //获取资源成功  
  11.                    setHeadAndPropagate(node, r);  //将自己置为队头,若还有剩余资源,向后传播  
  12.                     p.next = null// helpGC  
  13.                     if (interrupted)  
  14.                        selfInterrupt();  //如果等待过程中被打断过,此时将中断补上。  
  15.                     failed = false;  
  16.                     return;  
  17.                 }  
  18.             }  
  19.             //判断状态,寻找合适的前驱,进入waiting状态,等着被unpark()或interrupt()  
  20.             if (shouldParkAfterFailedAcquire(p,node) &&  
  21.                 parkAndCheckInterrupt())  
  22.                 interrupted = true;  
  23.         }  
  24.     } finally {  
  25.         if (failed)  
  26.             cancelAcquire(node);  
  27.     }  
  28. }  

该函数的功能类似于独占模式下的acquireQueued()。

跟独占模式比,有一点需要注意的是,这里只有线程是head.next时(“老二”),才会去尝试获取资源,有剩余的话还会唤醒之后的队友。那么问题就来了,假如老大用完后释放了5个资源,而老二需要6个,老三需要1个,老四需要2个。因为老大先唤醒老二,老二一看资源不够自己用继续park(),也更不会去唤醒老三和老四了。独占模式,同一时刻只有一个线程去执行,这样做未尝不可;但共享模式下,多个线程是可以同时执行的,现在因为老二的资源需求量大,而把后面量小的老三和老四也都卡住了。

3.2.2    setHeadAndPropagate(Node, int)

[java] view plain copy
 print?
  1. private void setHeadAndPropagate(Node node,int propagate) {  
  2.      Node h = head;  
  3.      setHead(node);  // 将自己置为队头  
  4.       
  5.      if (propagate > 0 || h == null ||h.waitStatus < 0) {   
  6.          Node s = node.next;  
  7.          if (s == null || s.isShared())  //后继结点也为共享模式,则触发释放资源函数  
  8.              doReleaseShared();  
  9.      }  
  10.  }  

此方法在setHead()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源),还会去唤醒后继结点,毕竟是共享模式。


3.3 releaseShared(int)

此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果彻底释放了(即state=0),它会唤醒等待队列里的其他线程来获取资源。

[java] view plain copy
 print?
  1. public final boolean releaseShared(int arg){  
  2.     if (tryReleaseShared(arg)) {  //尝试释放资源  
  3.         doReleaseShared();  //释放成功,继续唤醒后继结点  
  4.         return true;  
  5.     }  
  6.     return false;  
  7. }  
  8.   
  9. protected boolean tryReleaseShared(int arg){  //留给子类实现  
  10.     throw new UnsupportedOperationException();  
  11. }  

跟独占模式下的release()相似,但有一点稍微需要注意:独占模式下的tryRelease()在完全释放掉资源(state=0)后,才会返回true去唤醒其他线程,这主要是基于可重入的考量;而共享模式下的releaseShared()则没有这种要求,多线程可并发执行,不适用于可重入。

3.3.1 doReleaseShared()

[java] view plain copy
 print?
  1. private void doReleaseShared() {  
  2.      
  3.     for (;;) {   
  4.         Node h = head;  
  5.         if (h != null && h != tail){  //头结点不为空且有后继结点  
  6.             int ws = h.waitStatus;  
  7.             if (ws == Node.SIGNAL) {   
  8.                 if(!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //头结点状态,SIGNAL——>0  
  9.                     continue;            // 状态更新失败则循环进行,直到成功  
  10.                 unparkSuccessor(h);  // 唤醒后继结点  
  11.             } else if (ws == 0 &&!compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) //头结点状态,0——>PROPAGATE  
  12.                 continue;                // 持续循环,直到状态更新成功  
  13.         }  
  14.         if (h == head)                   // 头结点没变,则结束循环;否则继续  
  15.             break;  
  16.     }  
  17. }  

其余函数已经在上面分析过了。至此,AQS的独占模式与共享模式下的实现原理剖析的差不多了,代码是最好的老师。

除了上面分析的核心方法,AQS还有定义了附带超时功能的tryAcquireNanos()/tryAcquireSharedNanos()方法,以及响应中断的acquireInterruptibly()/acquireSharedInterruptibly()方法,其核心流程与通用方法大同小异,不再赘述。


4、应用实例

我们利用AQS来实现一个不可重入的互斥锁实现。锁资源(AQS里的state)只有两种状态:0表示未锁定,1表示锁定。下边是Mutex的核心源码:

[java] view plain copy
 print?
  1. public class Mutex {  
  2.      
  3.     /** 
  4.      *静态内部类,自定义同步器 
  5.      */  
  6.     private static class Sync extends AbstractQueuedSynchronizer{  
  7.          
  8.         @Override  
  9.         protected boolean isHeldExclusively(){   
  10.             return getState() == 1;  //是否有资源可用  
  11.         }  
  12.          
  13.         @Override  
  14.         public boolean tryAcquire(int acquires){  
  15.             assert acquires == 1;  
  16.             if (compareAndSetState(01)){  //state:0——>1,代表获取锁  
  17.                setExclusiveOwnerThread(Thread.currentThread());  //设置当前占用资源的线程  
  18.                 return true;  
  19.             }  
  20.             return false;  
  21.         }  
  22.          
  23.         @Override  
  24.         protected boolean tryRelease(int releases){  
  25.             assert releases == 1;  
  26.             if (getState() == 0) {  
  27.                 throw newIllegalMonitorStateException();  
  28.             }  
  29.             setExclusiveOwnerThread(null);  
  30.             setState(0);  //state:1——>0,代表释放锁  
  31.             return true;  
  32.         }  
  33.     }  
  34.      
  35.     private final Sync sync = new Sync();  
  36.      
  37.     /** 
  38.      * 获取锁,可能会阻塞 
  39.      */  
  40.     public void lock() {  
  41.         sync.acquire(1);  
  42.     }  
  43.    
  44.     /** 
  45.      * 尝试获取锁,无论成功或失败,立即返回 
  46.      */  
  47.     public boolean tryLock() {  
  48.         return sync.tryAcquire(1);  
  49.     }  
  50.      
  51.     /** 
  52.      * 释放锁 
  53.      */  
  54.     public void unlock() {  
  55.         sync.release(1);  
  56.     }  
  57. }  

同步类在实现时一般都将自定义同步器(sync)定义为内部类,供自己使用;而同步类自己(Mutex)则实现某个接口,对外服务。当然,接口的实现要直接依赖sync,它们在语义上也存在某种对应关系。而sync只用实现资源state的获取-释放方式tryAcquire-tryRelelase,至于线程的排队、等待、唤醒等,上层的AQS都已经实现好了,我们不用关心。

ReentrantLock/CountDownLatch/Semphore这些同步类的实现方式都差不多,不同的地方就在获取-释放资源的方式tryAcquire-tryRelelase。


原创粉丝点击