Java源码分析-带你认识什么是AQS(上)

前提概要

AQS三部曲之上篇,本篇文章主要面向与对于AQS各个组件的实现的方式和原理。

了解过JUC的源码,我们就可以知道JUC下面很多工具的实现都是依靠AQS,而AQS中用于保存等待线程的队列就是CLH,下图就是并发编程AQS的基础家族谱图。

image.png

CLH队列

定义概念

CLH是一个FIFO的队列,队列的每一个节点都是一个Node对象。当前线程获取同步状态失败的时候就会进入CLH队列。而当首节点同步状态被释放的时候会通知后置节点再次去获取同步状态。

CLH也是一种基于单向链表(隐式创建)的高性能、公平的自旋锁。申请加锁的线程只需要在其前驱节点的本地变量上自旋,极大地减少了不必要的处理器缓存同步的次数,降低了总线和内存的开销。

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

算法设计

算法思路

保持时序的锁基本思路就是将等待获取锁的线程放入集合,锁释放后,等待线程之一获取到锁

如何排队

CLH使用反向单链表的形式进行排队。也就是后继节点主动询问,而不是前继节点主动通知。

是否公平

CLH是公平锁。即后申请获取锁的排在队列末尾。

如何唤醒

CLH通过每个线程自旋。每个等待线程通过不断自旋前继节点状态判断是否能获取到锁。


算法实现

CLH排队使用公平方式,锁内部需要保存【尾节点】的引用,每次排队线程加入到队列最末尾

CLH锁使用反向链表方式进行排队,那么每个线程就需要维护【自己的状态】和保持一个【前向节点的引用】

CLH使用一个【boolean值】表示当前线程获取锁状态,并且此状态存储在前置节点中,而非本节点内部。false表示当前线程释放锁;true表示当前线程等待获取锁或已经获取到锁。


AQS基本介绍

AQS将线程封装到一个Node里面,并维护一个CLH Node FIFO队列,它是一个非阻塞的FIFO队列,也就是说在并发条件下往此队列做插入或移除操作不会阻塞。是通过自旋锁和CAS保证节点插入和移除的原子性,实现无锁快速插入。

AbstractQueuedSynchronizer主要就是维护了一个state属性、一个FIFO队列和线程的阻塞与解除阻塞操作

  • state表示同步状态,它的类型为32位整型,对state的更新必须要保证原子性
  • 这里的队列是一个双向链表,每个节点里面都有一个prev和next,它们分别是前一个节点和后一个节点的引用

  • 注意的是此双向链表除了链头其他每个节点内部都包含一个线程,而链头可以理解为一个空节点。

具体结构如下图所示:

image.png

每一个节点的基本结构组成部分为:

image.png

WaitStatus的为节点状态:

waitStatus表示的是后续节点状态,这是因为AQS中使用CLH队列实现线程的结构管理,而CLH结构正是用前一节点某一属性表示当前节点的状态,这样更容易实现取消和超时功能

有五种状态:
SIGNAL:值为-1,表示当前节点的后续节点中的线程通过park被阻塞了,当前节点在

释放或取消时要通过unpark解除它的阻塞。

CANCELLED:值为1,表示当前节点的线程因为超时或中断被取消了
CONDITION:值为-2,表示当前节点在condition队列中
PROPAGATE:值为-3,共享模式的头结点可能处于此状态,表示无条件往下传播,引入此状态是为了优化锁竞争,使队列中线程有序地一个一个唤醒
INITIAL: 值为0,除了以上四种状态的第五种状态,一般是节点初始状态

前驱节点prev

主要是为了完成超时及取消语义、解锁自身等语义,前驱节点取消后,后驱节点只需向前找到一个未取消的前驱节点即可

后续节点next

主要是为了优化后续节点的查找,避免每次从尾部向前查找;

nextWaiter

用于表示condition队列的后续节点,此时prev和next属性将不再使用,而且节点状态处于Node.CONDITION;

上面是对节点及节点组成队列的结构的介绍,接着介绍AQS相关的一些操作,包括锁的获取与释放、队列的管理、同步状态的更新、线程阻塞与唤醒、取消中断与超时中断等等。


AQS运作原理

根据上面的学习,我们知道一个线程在尝试获取锁失败后将被阻塞并加入等待队列中,它是一个怎样的队列?又是如何管理此队列,我们先简单的看看大概的逻辑和思路。

什么是同步器

  • 多线程并发的执行之间通过某种共享状态来同步,只有当状态满足A条件,才能触发线程执行B。这个共同的语义可以称之为同步器。可以认为大多数的锁机制都可以基于同步器定制来实现的。而JUC(java.util.concurrent)里的思想是,将这些场景抽象出来的语义通过统一的同步框架来支持。

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

  • Lock的实现类其实都是构建在AbstractQueuedSynchronizer上,每个Lock实现类都持有自己内部类Sync的实例,而这个Sync就是继承AbstractQueuedSynchronizer(AQS)。为何要实现不同的Sync呢?另外还有AQS的State机制。以后文章会举例说明不同同步器内的Sync与state实现。

AQS框架如何构建同步器

同步器的基本功能

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

(1)获取(设置)同步状态:如果允许,则获取锁,如果不允许就阻塞线程,直到同步状态允许获取

(2)释放同步状态:修改同步状态,并且唤醒等待线程

AQS同步机制同时考虑了如下需求

(1)独占锁和共享锁两种机制

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

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

同步状态的获取与释放

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

独占模式

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

public final void acquire(int arg) { 
    if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
        //获取失败,则加入等待队列 
        selfInterrupt(); 
} 

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

public final void acquireInterruptibly(int arg) 
                         throws InterruptedException { 
    if (Thread.interrupted()) 
        throw new InterruptedException(); 
        if (!tryAcquire(arg)) 
          doAcquireInterruptibly(arg); 
 } 

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

public final boolean tryAcquireNanos(int arg, long nanosTimeout) throws 
                            InterruptedException { 
   if (Thread.interrupted()) 
       throw new InterruptedException(); 
       return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout); 
}

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


public final boolean release(int arg) { 
    if (tryRelease(arg)) { 
        Node h = head; 
        if (h != null && h.waitStatus != 0) 
            unparkSuccessor(h); 
            return true; 
    } 
    return false; 
} 

共享模式

共享模式获取

public final void acquireShared(int arg) { 
    if (tryAcquireShared(arg) < 0) 
        doAcquireShared(arg); 
 }

可中断模式共享获取

public final void acquireSharedInterruptibly(int arg) throws 
       InterruptedException { 
        if (Thread.interrupted()) 
            throw new InterruptedException(); 
        if (tryAcquireShared(arg) < 0) 
            doAcquireSharedInterruptibly(arg); 
 } 
 

共享模式带定时获取

public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout) throws 
             InterruptedException { 
    if (Thread.interrupted()) 
        throw new InterruptedException(); 
    return tryAcquireShared(arg) >= 0 || 
        doAcquireSharedNanos(arg, nanosTimeout); 
} 

共享锁释放

public final boolean releaseShared(int arg) { 
    if (tryReleaseShared(arg)) { 
        doReleaseShared(); 
        return true; 
    } 
    return false; 
} 

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


protected boolean tryAcquire(int arg) { 
    throw new UnsupportedOperationException(); 
} 

protected boolean tryRelease(int arg) { 
    throw new UnsupportedOperationException();
} 

protected int tryAcquireShared(int arg) { 
    throw new UnsupportedOperationException(); 
} 

protected boolean tryReleaseShared(int arg) { 
    throw new UnsupportedOperationException(); 
} 

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

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

得到锁的线程禁用(park)和唤醒(unpark),也是直接native实现(这几个native方法的实现代码在hotspot\src\share\vm\prims\unsafe.cpp文件中,但是关键代码park的最终实现是和操作系统相关的,比如windows下实现是在os_windows.cpp中,有兴趣的同学可以下载jdk源码查看)。

唤醒一个被park()线程主要手段包括以下几种:

  1. 其他线程调用以被park()线程为,参数的unpark(Thread thread)。
  2. 其他线程中断被park()线程,如:waiters.peek().interrupt()waiters为存储线程对象的队列.
  3. 不知原因的返回。

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

   LockSupport.park();  //禁用当前线程
   if(Thread.interrupted){
        //doSomething
   }

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

private void doAcquireSharedInterruptibly(int arg) 
             throws InterruptedException {
    final Node node = addWaiter(Node.SHARED);
    try {
        for (;;) {
         final Node p = node.predecessor();
         if (p == head) {
            int r = tryAcquireShared(arg);
            if (r >= 0) {
                setHeadAndPropagate(node, r);
                // help GC
                p.next = null; 
                return;
            }    
      }
    //parkAndCheckInterrupt()会返回park住的线程在被unpark后的线程状态,如果线程中断,跳出循环。
      if (shouldParkAfterFailedAcquire(p, node) &&
            parkAndCheckInterrupt())
            break;
      }
    } catch (RuntimeException ex) {
      cancelAcquire(node);
      throw ex;
    }    
    // 只有线程被interrupt后才会走到这里
    cancelAcquire(node);
    throw new InterruptedException();
}


//在park()住的线程被unpark()后,第一时间返回当前线程是否被打断
private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    return Thread.interrupted();
}

线程阻塞队列的维护

阻塞线程节点队列 CHL Node queue

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

/**

* CHL头节点

*/ 

private transient volatile Node head; 

/**

* CHL尾节点

*/ 

private transient volatile Node tail; 

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

static final class Node { 

 /** 代表线程已经被取消*/

    static final int CANCELLED =  1; 

    /** 代表后续节点需要唤醒 */ 

    static final int SIGNAL    = -1; 

    /** 代表线程在等待某一条件/

    static final int CONDITION = -2;

    /** 标记是共享模式*/ 

    static final Node SHARED = new Node(); 

    /** 标记是独占模式*/ 

    static final Node EXCLUSIVE = null; 

    /**
     * 状态位 ,分别可以使CANCELLED、SINGNAL、CONDITION、0
     */ 

    volatile int waitStatus; 

    /**

    * 前置节点

    */ 

    volatile Node prev; 

    /**
    * 后续节点
    */ 

    volatile Node next; 

    /**

    * 节点代表的线程

    */ 

    volatile Thread thread; 

    /**

    *连接到等待condition的下一个节点

    */ 

    Node nextWaiter; 

} 

入列

通过上面的CLH同步队列结构图我们基本可以猜到,CLH同步队列入列是怎么回事。就是将当前同步队列的最后一个节点的next指向添加的节点,并将添加节点的prev指向最后一个节点。同时需要将tail指向新添加的节点。

下面我们来查看一下具体的方法addWaiter(Node mode)的源码。


private Node addWaiter(Node mode){
    //新建node
    Node node =new Node(Thread.currentThread(), mode);
    //快速尝试添加尾节点
    Node pred = tail;
    if(pred !=null) {
      node.prev = pred;
      //使用cas设置尾节点
      if(compareAndSetTail(pred, node)) {
           pred.next = node;
            return node;
      }
    }
    //循环设置尾节点
    enq(node);
    return node;
}


在addWaiter方法中我们会尝试获取尾节点并进行尾节点的设置,如果成功就直接返回,如果没有成功就调用enq(final Node node)进行设置,下面我们来看看enq(final Node node)方法的具体实现。


private Node enq(final Node node){
  //死循环尝试,直到成功
    for(;;) {
      Node t = tail;
     //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;
       }
    }
  }
}

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

我们可以看见上面使用了CAS来进行首节点和尾节点的设置,以达到线程安全的目的。只有尾节点设置成功了才会返回,否则会一直进行下去。

出列

CLH同步队列遵循FIFO的规则,首节点的线程释放同步状态后。将会唤醒后继结点next,而后继节点将会尝试获取同步状态,如果获取同步状态成功,将会将自己设置成首节点同时需要注意的是,这个过程后继节点会断开需前节点的关联,具体流程形如下图。

[图片上传失败...(image-472a0f-1620454872224)]

总结

从源码可以看出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 并判断线程是否中断

你可能感兴趣的:(Java源码分析-带你认识什么是AQS(上))