zoukankan      html  css  js  c++  java
  • java并发包分析之———AQS框架

    一、什么是同步器

     

    多线程并发的执行,之间通过某种 共享 状态来同步,只有当状态满足 xxxx 条件,才能触发线程执行 xxxx 。

    这个共同的语义可以称之为同步器。可以认为以上所有的锁机制都可以基于同步器定制来实现的。

     

    而juc(java.util.concurrent)里的思想是 将这些场景抽象出来的语义通过统一的同步框架来支持。

    juc 里所有的这些锁机制都是基于 AQS ( AbstractQueuedSynchronizer )框架上构建的。下面简单介绍下 AQS( AbstractQueuedSynchronizer )。 可以参考Doug Lea的论文The java.util.concurrent Synchronizer Framework

    我们来看下java.util.concurrent.locks大致结构

    上图中,LOCK的实现类其实都是构建在AbstractQueuedSynchronizer上,为何图中没有用UML线表示呢,这是每个Lock实现类都持有自己内部类Sync的实例,而这个Sync就是继承AbstractQueuedSynchronizer(AQS)。为何要实现不同的Sync呢?这和每种Lock用途相关。另外还有AQS的State机制。下文会举例说明不同同步器内的Sync与state实现。

     

    二、AQS框架如何构建同步器

     

    0、同步器的基本功能

     

    一个同步器至少需要包含两个功能:

    1.       获取同步状态

    如果允许,则获取锁,如果不允许就阻塞线程,直到同步状态允许获取。

    2.       释放同步状态

    修改同步状态,并且唤醒等待线程。

    根据作者论文, aqs 同步机制同时考虑了如下需求:

    1.       独占锁和共享锁两种机制。

    2.       线程阻塞后,如果需要取消,需要支持中断。

    3.       线程阻塞后,如果有超时要求,应该支持超时后中断的机制。

     

    1、同步状态的获取与释放

     

    AQS实现了一个同步器的基本结构,下面以独占锁与共享锁分开讨论,来说明AQS怎样实现获取、释放同步状态。

     

    1.1、独占模式

     

    独占获取: tryAcquire 本身不会阻塞线程,如果返回 true 成功就继续,如果返回 false 那么就阻塞线程并加入阻塞队列。

     

    [java] view plaincopy
     
     
    1. public final void acquire(int arg) {    
    2.     
    3.         if (!tryAcquire(arg) &&    
    4.     
    5.             acquireQueued(addWaiter(Node.EXCLUSIVE), arg))//获取失败,则加入等待队列    
    6.     
    7.             selfInterrupt();    
    8.     
    9. }    

     

    独占且可中断模式获取:支持中断取消

    [java] view plaincopy
     
     
    1. public final void acquireInterruptibly(int arg) throws InterruptedException {    
    2.     
    3.         if (Thread.interrupted())    
    4.             throw new InterruptedException();    
    5.         if (!tryAcquire(arg))    
    6.     
    7.             doAcquireInterruptibly(arg);    
    8.     
    9.     }     



    独占且支持超时模式获取: 带有超时时间,如果经过超时时间则会退出。

    [java] view plaincopy
     
     
    1. public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws InterruptedException {    
    2.     
    3.      if (Thread.interrupted())    
    4.     
    5.          throw new InterruptedException();    
    6.     
    7.      return tryAcquire(arg) ||    
    8.     
    9.          doAcquireNanos(arg, nanosTimeout);    

    独占模式释放:释放成功会唤醒后续节点

    [java] view plaincopy
     
     
    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. }    

     

    1.2、共享模式

     

    共享模式获取

     

    [java] view plaincopy
     
     
    1. public final void acquireShared(int arg) {    
    2.     
    3.     if (tryAcquireShared(arg) < 0)    
    4.     
    5.         doAcquireShared(arg);    

     

    可中断模式共享获取

      

    [java] view plaincopy
     
     
    1. public final void acquireSharedInterruptibly(int arg) throws InterruptedException {    
    2.         if (Thread.interrupted())    
    3.             throw new InterruptedException();    
    4.         if (tryAcquireShared(arg) < 0)    
    5.             doAcquireSharedInterruptibly(arg);    
    6.     }     

      

    共享模式带定时获取

     

    [java] view plaincopy
     
     
    1. public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws InterruptedException {    
    2.      if (Thread.interrupted())    
    3.          throw new InterruptedException();    
    4.      return tryAcquireShared(arg) >= 0 ||    
    5.          doAcquireSharedNanos(arg, nanosTimeout);    
    6. }     

    共享锁释放

     

    [java] view plaincopy
     
     
    1. public final boolean releaseShared(int arg) {    
    2.         if (tryReleaseShared(arg)) {    
    3.             doReleaseShared();    
    4.             return true;    
    5.         }    
    6.         return false;    
    7.     }     



    注意以上框架只定义了一个同步器的基本结构框架,的基本方法里依赖的 tryAcquire 、 tryRelease 、tryAcquireShared 、 tryReleaseShared 四个方法在 AQS 里没有实现,这四个方法不会涉及线程阻塞,而是由各自不同的使用场景根据情况来定制:

    [java] view plaincopy
     
     
    1. protected boolean tryAcquire(int arg) {    
    2.     throw new UnsupportedOperationException();    
    3. }    
    4. protected boolean tryRelease(int arg) {    
    5.     throw new UnsupportedOperationException();    
    6. }    
    7. protected int tryAcquireShared(int arg) {    
    8.     throw new UnsupportedOperationException();    
    9.     
    10. }    
    11. protected boolean tryReleaseShared(int arg) {    
    12.     throw new UnsupportedOperationException();    
    13. }    

    从以上源码可以看出AQS实现基本的功能:

    AQS虽然实现了acquire,和release方法是可能阻塞的,但是里面调用的tryAcquire和tryRelease是由子类来定制的且是不阻塞的可。以认为同步状态的维护、获取、释放动作是由子类实现的功能,而动作成功与否的后续行为时有AQS框架来实现。

     

    3、状态获取、释放成功或失败的后续行为:线程的阻塞、唤醒机制

     

    有别于wait和notiry。这里利用 jdk1.5 开始提供的 LockSupport.park() 和 LockSupport.unpark() 的本地方法实现,实现线程的阻塞和唤醒。

    得到锁的线程禁用(park)和唤醒(unpark),也是直接native实现(这几个native方法的实现代码在hotspotsrcsharevmprimsunsafe.cpp文件中,但是关键代码park的最终实现是和操作系统相关的,比如windows下实现是在os_windows.cpp中,有兴趣的同学可以下载jdk源码查看)。唤醒一个被park()线程主要手段包括以下几种
    1. 其他线程调用以被park()线程为参数的unpark(Thread thread).
    2. 其他线程中断被park()线程,如waiters.peek().interrupt();waiters为存储线程对象的队列.
    3. 不知原因的返回。

    park()方法返回并不会报告到底是上诉哪种返回,所以返回好最好检查下线程状态,如

    [java] view plaincopy
     
     
    1. LockSupport.park();  //禁用当前线程  
    2. if(Thread.interrupted){  
    3. //doSomething  
    4. }  

    AbstractQueuedSynchronizer(AQS)对于这点实现得相当巧妙,如下所示

    [java] view plaincopy
     
     
    1. private void doAcquireSharedInterruptibly(int arg)throwsInterruptedException {  
    2.     final Node node = addWaiter(Node.SHARED);  
    3.     try {  
    4.          for (;;) {  
    5.              final Node p = node.predecessor();  
    6.              if (p == head) {  
    7.                  int r = tryAcquireShared(arg);  
    8.                  if (r >= 0) {  
    9.                      setHeadAndPropagate(node, r);  
    10.                      p.next = null// help GC  
    11.                      return;  
    12.                  }  
    13.             }  
    14.              //parkAndCheckInterrupt()会返回park住的线程在被unpark后的线程状态,如果线程中断,跳出循环。  
    15.              if (shouldParkAfterFailedAcquire(p, node) &&  
    16.                  parkAndCheckInterrupt())  
    17.                  break;  
    18.       }  
    19.      } catch (RuntimeException ex) {  
    20.           cancelAcquire(node);  
    21.           throw ex;  
    22.  }  
    23.   
    24.      // 只有线程被interrupt后才会走到这里  
    25.      cancelAcquire(node);  
    26.      throw new InterruptedException();  
    27. }  
    28.   
    29. //在park()住的线程被unpark()后,第一时间返回当前线程是否被打断  
    30. private final boolean parkAndCheckInterrupt() {  
    31.     LockSupport.park(this);  
    32.     return Thread.interrupted();  
    33. }  



    4、线程阻塞队列的维护

     

    阻塞线程节点队列 CHL Node queue 。

    根据论文里描述, AQS 里将阻塞线程封装到一个内部类 Node 里。并维护一个 CHL Node FIFO 队列。 CHL队列是一个非阻塞的 FIFO 队列,也就是说往里面插入或移除一个节点的时候,在并发条件下不会阻塞,而是通过自旋锁和 CAS 保证节点插入和移除的原子性。实现无锁且快速的插入。关于非阻塞算法可以参考  Java 理论与实践: 非阻塞算法简介 。CHL队列对应代码如下:

     

    [java] view plaincopy
     
     
    1.  /**  
    2.  * CHL头节点  
    3.  */     
    4. rivate transient volatile Node head;    
    5. /**  
    6.  * CHL尾节点  
    7.  */    
    8. private transient volatile Node tail;    

      Node节点是对Thread的一个封装,结构大概如下:

    [java] view plaincopy
     
     
    1. static final class Node {    
    2.     /** 代表线程已经被取消*/    
    3.     static final int CANCELLED =  1;    
    4.     /** 代表后续节点需要唤醒 */    
    5.     static final int SIGNAL    = -1;    
    6.     /** 代表线程在等待某一条件/  
    7.     static final int CONDITION = -2;  
    8.     /** 标记是共享模式*/    
    9.     static final Node SHARED = new Node();    
    10.     /** 标记是独占模式*/    
    11.     static final Node EXCLUSIVE = null;    
    12.     
    13.     /**  
    14.      * 状态位 ,分别可以使CANCELLED、SINGNAL、CONDITION、0  
    15.      */    
    16.     volatile int waitStatus;    
    17.     
    18.     /**  
    19.      * 前置节点  
    20.      */    
    21.     volatile Node prev;    
    22.     
    23.     /**  
    24.      * 后续节点  
    25.      */    
    26.     volatile Node next;    
    27.     
    28.     /**  
    29.      * 节点代表的线程  
    30.      */    
    31.     volatile Thread thread;    
    32.     
    33.     /**  
    34.      *连接到等待condition的下一个节点  
    35.      */    
    36.     Node nextWaiter;    
    37.     
    38. }    

    5、小结

    从源码可以看出AQS实现基本的功能:

    1.同步器基本范式、结构

    2.线程的阻塞、唤醒机制

    3.线程阻塞队列的维护

     

    AQS虽然实现了acquire,和release方法,但是里面调用的tryAcquire和tryRelease是由子类来定制的。可以认为同步状态的维护、获取、释放动作是由子类实现的功能,而动作成功与否的后续行为时有AQS框架来实现

     

    还有以下一些私有方法,用于辅助完成以上的功能:

    final boolean acquireQueued(final Node node, int arg) :申请队列

    private Node enq(final Node node) : 入队

    private Node addWaiter(Node mode) :以mode创建创建节点,并加入到队列

    private void unparkSuccessor(Node node) : 唤醒节点的后续节点,如果存在的话。

    private void doReleaseShared() :释放共享锁

    private void setHeadAndPropagate(Node node, int propagate):设置头,并且如果是共享模式且propagate大于0,则唤醒后续节点。

    private void cancelAcquire(Node node) :取消正在获取的节点

    private static void selfInterrupt() :自我中断

    private final boolean parkAndCheckInterrupt() : park 并判断线程是否中断

     

    三、AQS在各同步器内的Sync与State实现

     

    1、什么是state机制:

     

    提供 volatile 变量 state;  用于同步线程之间的共享状态。通过 CAS 和 volatile 保证其原子性和可见性。对应源码里的定义:

    [java] view plaincopy
     
     
    1. /**  
    2.  * 同步状态  
    3.  */    
    4. private volatile int state;    
    5.     
    6. /**  
    7.  *cas  
    8.  */    
    9. protected final boolean compareAndSetState(int expect, int update) {    
    10.     // See below for intrinsics setup to support this    
    11.     return unsafe.compareAndSwapInt(this, stateOffset, expect, update);    
    12. }    



    2、不同实现类的Sync与State:

     

    基于AQS构建的Synchronizer包括ReentrantLock,Semaphore,CountDownLatch, ReetrantRead WriteLock,FutureTask等,这些Synchronizer实际上最基本的东西就是原子状态的获取和释放,只是条件不一样而已。

    2.1、ReentrantLock

     

    需要记录当前线程获取原子状态的次数,如果次数为零,那么就说明这个线程放弃了锁(也有可能其他线程占据着锁从而需要等待),如果次数大于1,也就是获得了重进入的效果,而其他线程只能被park住,直到这个线程重进入锁次数变成0而释放原子状态。以下为ReetranLock的FairSync的tryAcquire实现代码解析。

    [java] view plaincopy
     
     
    1. //公平获取锁  
    2. protected final boolean tryAcquire(int acquires) {  
    3.     final Thread current = Thread.currentThread();  
    4.     int c = getState();  
    5.     //如果当前重进入数为0,说明有机会取得锁  
    6.     if (c == 0) {  
    7.         //如果是第一个等待者,并且设置重进入数成功,那么当前线程获得锁  
    8.         if (isFirst(current) &&  
    9.             compareAndSetState(0, acquires)) {  
    10.             setExclusiveOwnerThread(current);  
    11.             return true;  
    12.      }  
    13.  }  
    14.     //如果当前线程本身就持有锁,那么叠加重进入数,并且继续获得锁  
    15.     else if (current == getExclusiveOwnerThread()) {  
    16.         int nextc = c + acquires;  
    17.         if (nextc < 0)  
    18.             throw new Error("Maximum lock count exceeded");  
    19.         setState(nextc);  
    20.         return true;  
    21.  }  
    22.      //以上条件都不满足,那么线程进入等待队列。  
    23.      return false;  
    24. }  



    2.2、Semaphore

     

    则是要记录当前还有多少次许可可以使用,到0,就需要等待,也就实现并发量的控制,Semaphore一开始设置许可数为1,实际上就是一把互斥锁。以下为Semaphore的FairSync实现

     
    [java] view plaincopy
     
     
    1. protected int tryAcquireShared(int acquires) {  
    2.     Thread current = Thread.currentThread();  
    3.     for (;;) {  
    4.          Thread first = getFirstQueuedThread();  
    5.          //如果当前等待队列的第一个线程不是当前线程,那么就返回-1表示当前线程需要等待  
    6.          if (first != null && first != current)  
    7.               return -1;  
    8.          //如果当前队列没有等待者,或者当前线程就是等待队列第一个等待者,那么先取得semaphore还有几个许可证,并且减去当前线程需要的许可证得到剩下的值  
    9.          int available = getState();  
    10.          int remaining = available - acquires;  
    11.          //如果remining<0,那么反馈给AQS当前线程需要等待,如果remaining>0,并且设置availble成功设置成剩余数,那么返回剩余值(>0),也就告知AQS当前线程拿到许可,可以继续执行。  
    12.          if (remaining < 0 ||compareAndSetState(available, remaining))  
    13.              return remaining;  
    14.  }  
    15. }  



    2.3、CountDownLatch

     

    闭锁则要保持其状态,在这个状态到达终止态之前,所有线程都会被park住,闭锁可以设定初始值,这个值的含义就是这个闭锁需要被countDown()几次,因为每次CountDown是sync.releaseShared(1),而一开始初始值为10的话,那么这个闭锁需要被countDown()十次,才能够将这个初始值减到0,从而释放原子状态,让等待的所有线程通过。

     
    [java] view plaincopy
     
     
    1. //await时候执行,只查看当前需要countDown数量减为0了,如果为0,说明可以继续执行,否则需要park住,等待countDown次数足够,并且unpark所有等待线程  
    2. public int tryAcquireShared(int acquires) {  
    3.      return getState() == 01 : -1;  
    4. }  
    5.   
    6. //countDown 时候执行,如果当前countDown数量为0,说明没有线程await,直接返回false而不需要唤醒park住线程,如果不为0,得到剩下需要 countDown的数量并且compareAndSet,最终返回剩下的countDown数量是否为0,供AQS判定是否释放所有await线程。  
    7. public boolean tryReleaseShared(int releases) {  
    8.     for (;;) {  
    9.          int c = getState();  
    10.          if (c == 0)  
    11.              return false;  
    12.          int nextc = c-1;  
    13.          if (compareAndSetState(c, nextc))  
    14.              return nextc == 0;  
    15.  }  
    16. }  



    2.4、FutureTask

     

    需要记录任务的执行状态,当调用其实例的get方法时,内部类Sync会去调用AQS的acquireSharedInterruptibly()方法,而这个方法会反向调用Sync实现的tryAcquireShared()方法,即让具体实现类决定是否让当前线程继续还是park,而FutureTask的tryAcquireShared方法所做的唯一事情就是检查状态,如果是RUNNING状态那么让当前线程park。而跑任务的线程会在任务结束时调用FutureTask 实例的set方法(与等待线程持相同的实例),设定执行结果,并且通过unpark唤醒正在等待的线程,返回结果。

     
    [java] view plaincopy
     
     
    1. //get时待用,只检查当前任务是否完成或者被Cancel,如果未完成并且没有被cancel,那么告诉AQS当前线程需要进入等待队列并且park住  
    2. protected int tryAcquireShared(int ignore) {  
    3.      return innerIsDone()? 1 : -1;  
    4. }  
    5.   
    6. //判定任务是否完成或者被Cancel  
    7. boolean innerIsDone() {  
    8.     return ranOrCancelled(getState()) &&    runner == null;  
    9. }  
    10.   
    11. //get时调用,对于CANCEL与其他异常进行抛错  
    12. V innerGet(long nanosTimeout) throws InterruptedException, ExecutionException, TimeoutException {  
    13.     if (!tryAcquireSharedNanos(0,nanosTimeout))  
    14.         throw new TimeoutException();  
    15.     if (getState() == CANCELLED)  
    16.         throw new CancellationException();  
    17.     if (exception != null)  
    18.         throw new ExecutionException(exception);  
    19.     return result;  
    20. }  
    21.   
    22. //任务的执行线程执行完毕调用(set(V v))  
    23. void innerSet(V v) {  
    24.      for (;;) {  
    25.         int s = getState();  
    26.         //如果线程任务已经执行完毕,那么直接返回(多线程执行任务?)  
    27.         if (s == RAN)  
    28.             return;  
    29.         //如果被CANCEL了,那么释放等待线程,并且会抛错  
    30.         if (s == CANCELLED) {  
    31.             releaseShared(0);  
    32.             return;  
    33.      }  
    34.         //如果成功设定任务状态为已完成,那么设定结果,unpark等待线程(调用get()方法而阻塞的线程),以及后续清理工作(一般由FutrueTask的子类实现)  
    35.         if (compareAndSetState(s, RAN)) {  
    36.             result = v;  
    37.             releaseShared(0);  
    38.             done();  
    39.             return;  
    40.      }  
    41.  }  
    42. }  



    以上4个AQS的使用是比较典型,然而有个问题就是这些状态存在哪里呢?并且是可以计数的。从以上4个example,我们可以很快得到答案,AQS提供给了子类一个int state属性。并且暴露给子类getState()和setState()两个方法(protected)。这样就为上述状态解决了存储问题,RetrantLock可以将这个state用于存储当前线程的重进入次数,Semaphore可以用这个state存储许可数,CountDownLatch则可以存储需要被countDown的次数,而Future则可以存储当前任务的执行状态(RUNING,RAN,CANCELL)。其他的Synchronizer存储他们的一些状态。

    AQS留给实现者的方法主要有5个方法,其中tryAcquire,tryRelease和isHeldExclusively三个方法为需要独占形式获取的synchronizer实现的,比如线程独占ReetranLock的Sync,而tryAcquireShared和tryReleasedShared为需要共享形式获取的synchronizer实现。

    ReentrantLock内部Sync类实现的是tryAcquire,tryRelease, isHeldExclusively三个方法(因为获取锁的公平性问题,tryAcquire由继承该Sync类的内部类FairSync和NonfairSync实现)Semaphore内部类Sync则实现了tryAcquireShared和tryReleasedShared(与CountDownLatch相似,因为公平性问题,tryAcquireShared由其内部类FairSync和NonfairSync实现)。CountDownLatch内部类Sync实现了tryAcquireShared和tryReleasedShared。FutureTask内部类Sync也实现了tryAcquireShared和tryReleasedShared。

  • 相关阅读:
    syslog日志格式解析
    Linux打补丁的一个简单例子
    Linux打补丁的一些问题
    安全漏洞整改解决方案(很不错网络文章)
    Linux系统启动过程
    chkconfig命令主要用来更新(启动或停止)和查询系统服务的运行级信息
    主机名/etc/hosts文件的作用
    Linux中如何配置IP相关文件
    /bin、/sbin、/usr/bin、/usr/sbin目录Linux执行文档的区别
    日志生成控制文件syslog.conf
  • 原文地址:https://www.cnblogs.com/zengdan-develpoer/p/3389006.html
Copyright © 2011-2022 走看看