Java AbstractQueuedSynchronizer(AQS)浅析之一

本篇文章对Java中的AbstractQueuedSynchronizer(AQS)进行分析和学习。若有不正之处请多多谅解,并欢迎批评指正。

为叙述方便,下文都以AQS替代AbstractQueuedSynchronizer。

使用的Java版本

src git:(master) ✗ java -version 
java version "1.8.0_201"
Java(TM) SE Runtime Environment (build 1.8.0_201-b09)
Java HotSpot(TM) 64-Bit Server VM (build 25.201-b09, mixed mode)

AQS是干什么的呢?

下面是AQS类的部分介绍,咱也看不懂,只能用百度翻译一下哈哈,建议英文好的直接看源码里的类注释。

AQS提供一个框架,用于实现依赖先进先出(FIFO)等待队列的阻塞锁和相关同步器(semaphores、events等)。对于大多数依赖单个原子{@code int}值来表示状态的同步器来说,这个类是一个有用的基础。子类必须定义更改此状态的受保护方法,以及定义此状态对于获取或释放此对象意味着什么。鉴于这些,AQS类中的其他方法执行所有排队和阻塞机制。

AQS的子类应该被定义成一个非公开的内部辅助类,用来实现其所在的外部类的属性同步。AQS没有实现任何同步接口。相反,AQS定义了一些方法例如{@link #acquireInterruptibly}让具体的锁和同步器来调用来实现同步机制。

一句话:本篇文章只需要知道AQS可以用来实现锁即可。

我们一般不会直接使用AQS,所以我们以ReentrantLock(可重入锁)来引出AQS。明白了AQS就明白了ReentrantLock是如何获取锁以及释放锁的了。

先说一下大致流程:

  • Java中的ReentrantLock的获取锁和释放锁是通过AQS来实现的。

  • AQS内部维护了一个int类型的值来表示同步状态和一个先进先出(FIFO)的等待队列

/**
 * 同步状态
 */
private volatile int state;

/**
 * 等待队列的head,延迟初始化。除了初始化之外,head只能通过setHead方法来修改。
 * 注意,如果head存在可以保证head的waitStatus不是CANCELLED.
 */
private transient volatile Node head;

/**
 * 等待队列的尾,惰性初始。只有在使用enq方法添加新的等待节点的时候修改。
 */
private transient volatile Node tail;

  • 对于非公平锁,线程总是会先尝试获取锁,如果获取成功就直接执行,如果获取失败会进入等待队列。进入等待队列中的线程会休眠,等待被唤醒。
  • 对于公平锁,如果已经有线程在等待获取锁了,那么新的线程就会直接排在等待队列后面等待获取锁。
  • 持有锁的线程执行完毕释放锁,唤醒等待队列中的线程。
  • 线程被唤醒后会尝试获取锁,如果成功获取锁那么线程就执行,否则线程会再次休眠等待被唤醒。

我们在使用ReentrantLock的过程中,既可以构建一个使用非公平策略的ReentrantLock实例,也可以构建一个使用公平策略的ReentrantLock实例。

ReentrantLock的类结构

public class ReentrantLock implements Lock, java.io.Serializable {

    //Sync成员变量
    private final Sync sync;
     
    //AQS的子类
    abstract static class Sync extends AbstractQueuedSynchronizer {
    
    }

    //非公平策略
    static final class NonfairSync extends Sync {

    }
    //公平策略
    static final class FairSync extends Sync {
    
    }

}

我们看到ReentrantLock类中有一个Sync类型的成员变量,Sync类继承了AQS,然后
NonfairSync和FairSync都继承了Sync,分别实现非公平锁和公平锁。

FairSync.png

NonfairSync.png

ReentrantLock的构造函数

public ReentrantLock() {
   //使用非公平策略
   sync = new NonfairSync();
}

public ReentrantLock(boolean fair) {
    //使用公平策略
    sync = fair ? new FairSync() : new NonfairSync();
}

我们可以选择构建公平的或非公平的ReentrantLock实例,ReentrantLock中获取锁和释放锁相关的方法如下所示。我们先看非公平锁的情况。

    void lock();
    
    boolean tryLock(); 
    
    void lockInterruptibly() throws InterruptedException; 
    
    boolean tryLock(long time, TimeUnit unit) throws InterruptedException; 
    
    void unlock();
   

非公平的ReentrantLock

ReentrantLock的lock方法

public void lock() {
     sync.lock();
}

ReentrantLock的lock方法内调用了sync的lock方法。NonfairSync的实现如下所示。

NonfairSync的lock方法

final void lock() {
    //注释1处,
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        //注释2处
        acquire(1);
}

注释1处,首先调用AQS的compareAndSetState方法以CAS的方式修改AQS的state变量,如果修改成功,说明当前线程成功获取了锁,然后将当前线程设置为锁的持有者。注意是以独占模式持有锁的

protected final boolean compareAndSetState(int expect, int update) {
    return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
}
protected final void setExclusiveOwnerThread(Thread thread) {
    exclusiveOwnerThread = thread;
}

如果修改AQS的state变量失败,说明此时有线程已经持有了锁,那么就调用acquire(int arg)方法获取锁,注意我们传入的参数是1。

AQS的acquire(int arg)方法

public final void acquire(int arg) {
  if (!tryAcquire(arg) &&
      //标记为独占模式
      acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
      selfInterrupt();
}

这个步骤可以分为3步(把大象放进冰箱里需要几步?)
步骤1: 调用tryAcquire(arg) 尝试获取锁,获取成功直接返回
步骤2: 尝试获取锁失败将当前线程以独占锁的方式加入等待队列
步骤3: 为已经加入队列中的线程尝试获取锁

步骤1:调用tryAcquire(arg) 尝试获取锁

AQS没有实现这个方法,需要子类来实现

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

我们看下ReentrantLock.NonfairSync类的实现

protected final boolean tryAcquire(int acquires) {
    //调用了父类ReentrantLock.Sync的nonfairTryAcquire(acquires)方法
    return nonfairTryAcquire(acquires);
}

ReentrantLock.Sync的nonfairTryAcquire(acquires)方法

final boolean nonfairTryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    //获取同步状态值
    int c = getState();
    //注释1处
    if (c == 0) {
        if (compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
        }
    }
    else if (current == getExclusiveOwnerThread()) {
        //注释2处
        int nextc = c + acquires;
        if (nextc < 0) // overflow
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    //注释3处
    return false;
}
 

在注释1处,如果同步状态值为0,说明没有线程持有锁,那么就以CAS的方式修改AQS的state变量,如果修改成功,说明当前线程成功获取了锁,然后将当前线程设置为锁的持有者,然后返回true,获取锁成功。

注释2处,如果有线程持有锁,并且持有锁的线程是当前线程,那么就将同步状态值加1然后重新赋值给同步状态值state,然后返回true,获取锁成功。

AQS的setState(int newState)方法

protected final void setState(int newState) {
      state = newState;
}

注意:调用这个方法的前提是当前线程就是锁的持有者,所以可以修改state值,并不需要方法同步。

注释3处,获取锁失败。

到此步骤1结束,如果步骤1中获取锁失败,就会进入步骤2。

步骤2: 获取失败将当前线程加入等待队列

在这里我们要提一下AQS的一个内部类Node。Node类是对每一个等待获取锁的线程的封装,其包含了线程本身及其等待状态,如是否被阻塞、是否等待唤醒、是否已经被取消等。还包括指向当前节点的前驱节点的指针和后继节点的指针(双向链表)。Node类的成员变量waitStatus则表示当前Node节点的等待状态,共有5种取值CANCELLED、SIGNAL、CONDITION、PROPAGATE、0。

static final int CANCELLED =  1;

static final int SIGNAL    = -1;

static final int CONDITION = -2;

static final int PROPAGATE = -3;

//默认是0
volatile int waitStatus;
  • CANCELLED:表示当前节点由于超时或者中断而被取消。进入该状态后的节点状态将不会再变化。特别的,取消节点的线程不会被再次阻塞。

  • SIGNAL:当前节点的后继节点被阻塞了,所以当前节点在释放锁或者取消的时候必须唤醒后继节点。后继节点入队时,会将父节点的状态更新为SIGNAL。

  • CONDITION:表示节点正在一个条件队列中,本篇文章暂时忽略。

  • PROPAGATE:共享模式下,节点不仅会唤醒其后继节点,同时也可能会唤醒后继节点的后继节点。比如当前节点释放了10个资源,当前节点的后继节点只需要6个节点,那么当前节点在释放的时候就会唤醒后继节点和后继节点的后继节点。

  • 0:新节点进入等待队列时的默认状态。

注意,负值表示节点处于有效等待状态,而正值表示节点已被取消。所以源码中很多地方用>0、<0来判断节点的状态是否正常。

private Node addWaiter(Node mode) {
    //以独占模式加入等待队列
    Node node = new Node(Thread.currentThread(), mode);
    // 先尝试最快的入队方式
    Node pred = tail;
    //注释1处
    if (pred != null) {
        node.prev = pred;
        if (compareAndSetTail(pred, node)) {
            pred.next = node;
            return node;
        }
    }
    //注释2处
    enq(node);
    return node;
}

在注释1处,如果尾节点不为null,就直接将当前节点使用CAS的方式更新为尾节点,如果更新成功就返回node,这是最快的入队方式。

如果尾节点为null,或者将当前节点使用CAS的方式更新为尾节点失败,就调用注释2处的enq(final Node node)方法将node加入队列。

AQS的enq(final Node node)方法,注意,这个方法是一个无限循环,只有成功将加入到队列尾部才会返回。

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // 如果队列不存在,就新建一个node然后初始化队列
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

首先如果尾节点为null,说明队列此时还不存在,就新建一个节点然后以CAS的方式将新创建的节点设置为头节点,如果成功则让尾节点也指向node。如果如果尾节点不为null,就以CAS的方式将node更新为尾节点。

注意,这个方法是一个无限循环,只有成功将node加入到队列尾部才会返回。

将node加入到等待队列成功以后会进入到AQS的acquire(int arg)方法的步骤3

步骤3: 为已经加入队列中的线程尝试获取锁

final boolean acquireQueued(final Node node, int arg) {
    boolean failed = true;//标记是否成功获取锁
    try {
        boolean interrupted = false;//标记线程是否被中断
        //无限循环
        for (;;) {//注释1处
            //获取前驱节点
            final Node p = node.predecessor();
            if (p == head && tryAcquire(arg)) {
                //如果前驱节点是head并且尝试获取锁成功,就将当前节点更新为head节点
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //注释2处
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //标记线程在阻塞过程中是否被中断
                interrupted = true;
        }
    } finally {//注释3处
        if (failed)
            cancelAcquire(node);
    }
}

注释1处,如果前驱节点是head并且调用tryAcquire(int arg)方法获取锁成功,就将当前节点更新为head节点,然后返回。

private void setHead(Node node) {
    head = node;
    node.thread = null;
    node.prev = null;
}

在setHead方法中将node的thread和prev变量都置为了null,是为了帮助GC和避免不必要的唤醒和遍历。

在注释2处,如果获取锁失败后则判断是否应该阻塞当前线程

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //获取前驱节点的状态
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL)
        /*
         * node节点已经设置了状态告诉前驱节点在释放锁的时候通知自己,所以node节点可以被安全的阻塞。
         */
        return true;
    if (ws > 0) {
        /*
         * 前驱节点已经被取消了,向前寻找状态有效的前驱节点,然后将node设置为有效前驱节点的后继节点。
         * 注意:已经被取消的节点会被GC,这些节点相当于一个无引用链。
         */
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * 以CAS的方式更新前驱节点的waitStatus为Node.SIGNAL,告诉前驱节点在释放锁的时候通知自己。
         * 可能会失败。
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

如果shouldParkAfterFailedAcquire方法返回false,那么重新循环。如果返回true则调用parkAndCheckInterrupt方法。

AQS的parkAndCheckInterrupt()方法,注意这个方法会阻塞线程,并在线程 被唤醒后,通过调用Thread.interrupted()返回在阻塞过程中线程是否被中断。

private final boolean parkAndCheckInterrupt() {
    //注释1处
    LockSupport.park(this);
    //唤醒后,返回在阻塞过程中是否被中断
    return Thread.interrupted();
}

LockSupport.park(this);

注释1处这行代码会阻塞当前线程,Thread.interrupted()这行代码就不会执行了,只有被唤醒后Thread.interrupted()这行代码才会执行。

在线程被唤醒后,返回在阻塞过程中是否被中断。注意Thread.interrupted()方法会将线程的中断状态清空。

当线程被唤醒后,也会重新循环。

到现在AQS的acquire方法就结束了。

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

这里要注意一下,因为Thread.interrupted()方法会将线程的中断状态清空,所以我们这里要判断一下,如果线程在阻塞过程中被中断了,我们在这里要调用selfInterrupt()方法来中断当前线程,也就是将当前线程的中断状态置为true。

现在总结一下AQS的acquire(int arg)方法的流程。
  1. 调用子类的tryAcquire(int acquires)方法先尝试获取锁,如果成功则直接返回;
  2. 获取失败,则调用addWaiter(Node mode)方法将该线程加入等待队列的尾部,并标记为独占模式;
  3. 将该线程加入等待队列后,调用acquireQueued(final Node node, int arg)方法来尝试获取锁,在这个过程中,线程可能会被多次阻塞、唤醒。如果成功获取锁,就将当前节点更新为head节点,然后返回。如果在整个等待过程中被中断过,则返回true,否则返回false。
  4. 如果线程在等待过程中被中断过,它是不响应的。只是获取锁后才再进行自我中断selfInterrupt(),将中断补上。

到此,非公平的ReentrantLock的lock() 方法分析完毕。

boolean tryLock(); 

void lockInterruptibly() throws InterruptedException; 

boolean tryLock(long time, TimeUnit unit) throws InterruptedException;

这三个获取锁的方法过程也是类似的,就不进行分析了,接下来看一看非公平的ReentrantLock释放锁的过程。

ReentrantLock的unlock()方法

public void unlock() {
    //调用AQS的release方法
    sync.release(1);
}

其实我们这里可以看到,一个线程可以多次获取锁(可重入锁),每获取一次锁就会将state加1,每释放一次锁,就会将state减1,当前线程将state减到0的时候,说明当前线程释放了锁。

AQS的release(int arg)方法

public final boolean release(int arg) {
    if (tryRelease(arg)) {
        Node h = head;
        if (h != null && h.waitStatus != 0)
            //唤醒后继节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}

首先调用tryRelease(int arg)方法,AQS没有实现这个方法,我们直接看ReentrantLock.Sync的实现

protected final boolean tryRelease(int releases) {
    //同步状态每次减1
    int c = getState() - releases;
   //如果当前线程不是锁的持有者,抛出异常,没资格释放锁,哈哈
    if (Thread.currentThread() != getExclusiveOwnerThread())
        throw new IllegalMonitorStateException();
    boolean free = false;
    if (c == 0) {//同步状态为0,表示成功释放了锁
        free = true;
        setExclusiveOwnerThread(null);
    }
    //更新同步状态的值
    setState(c);
    return free;
}

方法首先将同步状态值减去1,如果如果当前线程不是锁的持有者,抛出异常。如果同步状态值减到了0,说明表示成功释放了锁,然后我们将锁的持有者设置为null,最后更新同步状态值,然后返回。

如果tryRelease返回了false,说明没有成功释放锁,如果返回true,表示成功释放了锁,那么我们要唤醒后继节点。

private void unparkSuccessor(Node node) {
    /*
     * 将头节点的waitStatus置为0
     */
    int ws = node.waitStatus;
    if (ws < 0)
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * 后继节点持有要被唤醒的线程,通常就是下一个节点。
     * 但是如果下一个节点已经被取消了或者为null,就从后向前遍历寻找最靠前的没有被取消的后继节点。  
     */
    Node s = node.next;
    if (s == null || s.waitStatus > 0) {
        s = null;
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    //如果有效的后继节点不为null,就唤醒该节点持有的线程。
    if (s != null)
        LockSupport.unpark(s.thread);
}

被唤醒的线程会从上面的parkAndCheckInterrupt方法中第二行代码恢复执行

private final boolean parkAndCheckInterrupt() {
    //注释1处
    LockSupport.park(this);
    //唤醒后,在这里恢复执行,返回在阻塞过程中是否被中断
    return Thread.interrupted();
}
总结一下AQS的release(int arg)方法的流程。

release方法每次释放锁就会将state值减1,如果彻底释放了(即state==0),就会唤醒等待队列里的其他线程来获取锁。

看完了非公平的ReentrantLock获取锁和释放锁的过程,接下来我们看看公平的ReentrantLock获取锁和释放锁过程。

公平的ReentrantLock

public ReentrantLock(boolean fair) {
    sync = fair ? new FairSync() : new NonfairSync();
}

当我们使用上面的构造函数创建ReentrantLock实例的时候,如果传入的参数是true,那么构建的是公平的ReentrantLock

public void lock() {
    sync.lock();
}

ReentrantLock.FairSync的lock方法

final void lock() {
    //获取锁
    acquire(1);
}

AQS的acquire(int arg)方法

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

这个步骤和非公平策略获取锁是一样的可以分为3步
步骤1: 调用tryAcquire(arg) 尝试获取锁,获取成功直接返回
步骤2: 获取失败将当前线程以独占锁的方式加入等待队列
步骤3: 为已经加入队列中的线程尝试获取锁

步骤1: 调用tryAcquire(arg) 尝试获取锁

ReentrantLock.FairSync的tryAcquire(int acquires)方法

protected final boolean tryAcquire(int acquires) {
    final Thread current = Thread.currentThread();
    int c = getState();
    if (c == 0) {
        //注释1处
        if (!hasQueuedPredecessors() &&
            compareAndSetState(0, acquires)) {
            setExclusiveOwnerThread(current);
            return true;
       }
    }
    //注释2处
    else if (current == getExclusiveOwnerThread()) {
        int nextc = c + acquires;
        if (nextc < 0)
            throw new Error("Maximum lock count exceeded");
        setState(nextc);
        return true;
    }
    return false; 
}

在上面方法的注释1处,c==0表示没有线程持有锁,首先调用hasQueuedPredecessors方法判断等待队列里面是否有节点,如果有等待节点,返回true,那么当前线程就不去获取锁(体现了公平)。如果没有等待节点并且以CAS的方式获取锁成功则将当前线程赋值为持有锁的线程,返回true。

在注释2处,c!=0表示有线程持有锁,如果是当前线程持有锁的话,那么就将同步状态值加1,返回true。

步骤2和步骤3和非公平的ReentrantLock是一样的,就不再赘述了。

公平的ReentrantLock和非公平的ReentrantLock的release(int arg)方法也是一样的就不再赘述了。

公平的ReentrantLock和非公平的ReentrantLock的差异

  1. 公平的ReentrantLock和非公平的ReentrantLock的差异由ReentrantLock.FairSyncReentrantLock.NonfairSync体现。感兴趣的可以直接看源码,一看就明白。

  2. 如果等待队列里有节点等待获取锁,公平的ReentrantLock就会直接进入等待队列排队。非公平的ReentrantLock无论等待队列里是否有节点等待获取锁,总是先尝试获取锁,如果获取失败才进入等待队列进行排队。

  3. 对于已经加入到等待队列中的节点,公平的ReentrantLock只有在当前节点没有前驱节点的情况下才会去获取锁,而非公平的ReentrantLock不管当前节点有没有前驱节点,总是会去尝试获取锁。

结尾:本篇文章通过ReentrantLock引出了AQS是如何帮助ReentrantLock实现的获取锁和释放锁的。下一篇文章打算分析一下AQS是如何帮助ReentrantLock实现Condition功能的。

参考链接:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java 允许您玩在线游戏,与世界各地的人聊天,计算您的按揭利息,并以 3D 形式查看图像。这些以 Java 编程语言编写的应用程序和可从浏览中访问的应用程序称为小程序。公司还将 Java 小程序用于内部网应用程序和其他电子商务的解决方案。所有主要行业的公司都在使用 Java。无论是部署在手机、嵌入式设备、游戏机、笔记本电脑还是数据中心,Java 都提供了当今网络应用所需要的丰富的用户界面、性能、多功能性、可移植性和安全性。Java 平台标准版软件是快速开发和部署安全、便携式应用程序的首选平台,可跨大多数操作系统的服务器和桌面系统。 Java 平台标准版(Java SE)可让您在台式机和服务器以及当今苛刻的嵌入式环境中开发和部署 Java 应用程序。Java 提供了当今应用程序所需的丰富的用户界面、性能、通用性、可移植性和安全性。 Java SE Runtime Environment(运行时环境)包含了运行以 Java 编程语言编写的程序所必需的 Java 虚拟机、运行时类库和 Java 应用程序启动器。 Java 平台的安全性 一个由角色提供的安全信息的描述。适用于开发人员,系统管理员,家庭用户和安全专业人士。 主要特点: - 在一个平台上编写软件,并可在几乎任何其它平台上运行 - 创建的程序可在 Web 浏览器和 Web 服务中运行 - 可开发用于在线论坛、商店、投票、HTML 表单处理等服务器端应用程序 - 结合基于 Java 技术的应用程序或服务来创建高度可定制的应用程序或服务 - 为手机、远程处理器、低成本消费产品和几乎任何具有数字内核的设备编写功能强大且高效的应用程序 Java SE 8 现在可用 Java 平台标准版 8(Java SE 8)是一个主要的功能版本。它包含了很多功能领域的新功能和增强功能。Java SE 8 通过减少样板代码,改进了集合和注释,简单的并行编程模型和更有效地利用现代多核处理器,提高了开发人员的工作效率和显着的增强了应用程序的性能。 Java SE 8u172 版本更新:2018年4月17日
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值