JUC学习之AQS抽象队列同步器

一、简介

AQS是AbstractQueuedSynchronizer的简写,翻译过来就是:抽象队列同步器。AbstractQueuedSynchronizer在java.util.concurrent.locks包中,声明如下:

public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements Serializable

可见,AbstractQueuedSynchronizer继承自AbstractOwnableSynchronizer同步器,并且实现了序列化接口。其实很多同步器都可以通过AQS很容易并且高效地构造出来,如常用的ReentrantLock、Semaphore、CountDownLatch等。

  • 核心思想:基于volatile int state这样的volatile变量,配合Unsafe工具对其原子性的操作来实现对当前锁状态进行修改。同步器内部依赖一个FIFO的双向队列来完成资源获取线程的排队工作。

JDK官网解释如下:

  • AbstractQueuedSynchronizer提供一个框架来实现基于先进先出(FIFO)等待队列的阻塞锁和相关同步器(信号量、事件等),AbstractQueuedSynchronizer被设计成大多数类型的同步器的基础,这些同步器依赖于单个原子int值来表示状态。子类必须定义改变这个状态的受保护的方法,这些方法定义了这个状态对于被获取或释放的对象意味着什么。子类可以维护其他状态字段,但是只有使用getState()、setState(int)和compareAndSetState(int, int)方法自动更新的int值才会被同步跟踪。子类应该定义为非公共的内部帮助类,用于实现其封闭类的同步属性。
  • AbstractQueuedSynchronizer类不实现任何同步接口。相反,它定义了像acquireInterruptibly(int)这样的方法,这些方法可以被具体的锁和相关的同步器调用来实现它们的公共方法。
  • AbstractQueuedSynchronizer支持默认独占模式和共享模式中的一种或两种。当以独占模式获取时,其他线程尝试的获取将无法成功。由多个线程获取的共享模式可能(但不一定)成功。在不同模式下等待的线程共享相同的FIFO队列。
  • AbstractQueuedSynchronizer提供内部队列的检查、检测和监视方法,以及条件对象的方法。可以根据需要使用AbstractQueuedSynchronizer将它们导出到类中,以实现同步机制。

要使用AbstractQueuedSynchronizer作为同步器的基础,通过使用下面三个方法:

  • getState():获取当前的同步状态;
  • setState(int):设置当前同步状态;
  • compareAndSetState(int, int):使用CAS设置当前状态,该方法能够保证状态设置的原子性;

重新定义以下方法:

这些方法在默认情况下都会抛出UnsupportedOperationException。这些方法的实现必须是内部线程安全的,并且通常应该是简短的,而不是阻塞的。

使用案例:一个不可重入的互斥锁类,它使用值0表示解锁状态,使用值1表示锁定状态。虽然非重入锁并不严格要求记录当前所有者线程,但是这个类这样做是为了更容易监视使用情况。

class Mutex implements Lock, java.io.Serializable {

   // Our internal helper class
   private static class Sync extends AbstractQueuedSynchronizer {
     // 记录是否处于锁定状态  1:锁定   0:非锁定
     protected boolean isHeldExclusively() {
       return getState() == 1;
     }

     // 如果锁定状态为0,则获取锁
     public boolean tryAcquire(int acquires) {
       assert acquires == 1; // Otherwise unused
       //cas设置锁定状态
       if (compareAndSetState(0, 1)) {
         setExclusiveOwnerThread(Thread.currentThread());
         return true;
       }
       return false;
     }

     // 通过将锁定状态设置为0来释放锁
     protected boolean tryRelease(int releases) {
       assert releases == 1; // Otherwise unused
       if (getState() == 0) throw new IllegalMonitorStateException();
       setExclusiveOwnerThread(null);
       // 重置到非锁定状态
       setState(0);
       return true;
     }

     // Provides a Condition
     Condition newCondition() { return new ConditionObject(); }

     // 反序列化
     private void readObject(ObjectInputStream s)
         throws IOException, ClassNotFoundException {
       s.defaultReadObject();
       setState(0); // 重置到非锁定状态
     }
   }

   // 同步对象完成所有的工作
   private final Sync sync = new Sync();

   public void lock()                { sync.acquire(1); }
   public boolean tryLock()          { return sync.tryAcquire(1); }
   public void unlock()              { sync.release(1); }
   public Condition newCondition()   { return sync.newCondition(); }
   public boolean isLocked()         { return sync.isHeldExclusively(); }
   public boolean hasQueuedThreads() { return sync.hasQueuedThreads(); }
   public void lockInterruptibly() throws InterruptedException {
     sync.acquireInterruptibly(1);
   }
   public boolean tryLock(long timeout, TimeUnit unit)
       throws InterruptedException {
     return sync.tryAcquireNanos(1, unit.toNanos(timeout));
   }
 }

AQS默认提供了独占式和共享式两种模式,JDK对应的实现有ReentrantLock和ReentrantReadWriteLock。即除了提供acquire方法之外,还提供了acquireShare。带shared都是共享模式相关操作,默认则是独占模式。

二、AQS锁的类别

  • 独占锁:锁在一个时间点只能被一个线程占有。根据锁的获取机制,又分为“公平锁”和“非公平锁”。等待队列中按照FIFO的原则获取锁,等待时间越长的线程越先获取到锁,这就是公平锁。而非公平锁,线程获取的锁的时候,无视等待队列直接获取锁。ReentrantLock和ReentrantReadWriteLock.Writelock是独占锁。
  • 共享锁:同一个时候能够被多个线程获取的锁,能被共享的锁。JUC包中ReentrantReadWriteLock.ReadLock,CyclicBarrier,CountDownLatch和Semaphore都是共享锁。

三、常用API

【a】构造方法 :只提供了一个构造方法,但是是受保护访问权限的。

protected

AbstractQueuedSynchronizer()

创建初始同步状态为0的AbstractQueuedSynchronizer 实例

【b】常用方法

方法返回值类型

方法描述

void

acquire(int arg)

以独占模式下获取,忽略中断

void

acquireInterruptibly(int arg)

以独占模式获取,如果中断则中止

void

acquireShared(int arg)

忽略中断,以共享模式获取

void

acquireSharedInterruptibly(int arg)

以共享模式获取,如果中断将中止

protected boolean

compareAndSetState(int expect, int update)

如果当前状态值等于期望值,则自动将同步状态设置为给定的更新值

Collection<Thread>

getExclusiveQueuedThreads()

返回一个集合,其中包含可能正在等待以独占模式获取的线程

Thread

getFirstQueuedThread()

返回队列中的第一个(等待时间最长的)线程,如果当前没有线程排队,则返回null

Collection<Thread>

getQueuedThreads()

返回一个包含可能正在等待获取的线程的集合

int

getQueueLength()

返回等待获取的线程数量的估计值

Collection<Thread>

getSharedQueuedThreads()

返回一个集合,其中包含可能在共享模式下等待获取的线程

protected int

getState()

返回同步状态的当前值

Collection<Thread>

getWaitingThreads(AbstractQueuedSynchronizer.ConditionObject condition)

返回一个集合,其中包含可能正在等待与此同步器关联的给定条件的线程

int

getWaitQueueLength(AbstractQueuedSynchronizer.ConditionObject condition)

返回与此同步器关联的给定条件上等待的线程数的估计值

boolean

hasContended()

查询是否有任何线程争用过此同步器;也就是说,如果一个获取方法曾经被阻塞

boolean

hasQueuedPredecessors()

查询是否有任何线程等待获取的时间比当前线程长

boolean

hasQueuedThreads()

查询是否有线程正在等待获取

boolean

hasWaiters(AbstractQueuedSynchronizer.ConditionObject condition)

查询是否有线程正在等待与此同步器关联的给定条件

protected boolean

isHeldExclusively()

如果仅针对当前(调用)线程保持同步,则返回true

boolean

isQueued(Thread thread)

如果给定线程当前正在排队,则返回true

boolean

owns(AbstractQueuedSynchronizer.ConditionObject condition)

查询给定的条件对象是否使用此同步器作为其锁

boolean

release(int arg)

以独占模式释放

boolean

releaseShared(int arg)

以共享模式释放

protected void

setState(int newState)

设置同步状态的值

String

toString()

返回标识此同步器及其状态的字符串

protected boolean

tryAcquire(int arg)

尝试以独占模式获取

boolean

tryAcquireNanos(int arg, long nanosTimeout)

尝试以独占模式获取,如果中断将中止,如果给定超时超时将失败

protected int

tryAcquireShared(int arg)

尝试以共享模式获取

boolean

tryAcquireSharedNanos(int arg, long nanosTimeout)

尝试以共享模式获取,如果中断将中止,如果给定超时超时将失败

protected boolean

tryRelease(int arg)

试图将状态设置为以独占模式释放

protected boolean

tryReleaseShared(int arg)

尝试将状态设置为以共享模式释放

四、源码阅读

AbstractQueuedSynchronizer底层是通过一个双向链表实现,入队则将尾部节点向前移动一个节点,再把新节点接入,然后接入尾部节点。AQS是通过CAS(比较并替换)操作解决多线程安全问题,即通过Unsafe完成的。

  • CAS : compareAndSet,Unsafe的CAS方法的语义是先与期望值进行比较,若不相等返回false;若相等的话,则会把原值修改新值,再返回true。

【a】重要属性说明:

/**
 * 等待队列的头,延迟初始化。除了初始化之外,它只通过setHead方法进行修改。注意:如果head存在,则保证不会取消它的等待状态.
 */
private transient volatile Node head;

/**
 * 等待队列的尾部,延迟初始化。仅通过方法enq修改以添加新的等待节点.
 */
private transient volatile Node tail;

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

/**
 * 一般用于申请资源时判断是否自旋的一个时间阈值,当请求设置的超时时间小于该时间时,就直接自旋而不是等待.
 */
static final long spinForTimeoutThreshold = 1000L;

/**
 * cas设置同步状态的值.
 * 直接操作内存地址
 */
private static final Unsafe unsafe = Unsafe.getUnsafe();
//上述成员变量在对应对象的内存地址中的偏移量
private static final long stateOffset;
private static final long headOffset;
private static final long tailOffset;
private static final long waitStatusOffset;
private static final long nextOffset;

【b】Node节点类

在AQS内部,如果锁被获取了,也就是被用了,还有很多其他的要来获取锁,这时候就需要他们排队,这里就需要一个队列,在AQS中叫CLH锁队列。CLH锁队列其实就是一个先进先出(FIFO)的队列,AQS中内部使用内部类Node来实现,是一个链表队列,每个节点代表着一个需要获取锁的线程,该node中有两个常量SHARED共享模式,EXCLUSIVE独占模式,共享模式允许多个线程可以获取同一个锁,独占模式则一个锁只能被一个线程持有,其他线程必须要等待。

下面是内部类Node的源码: 

/**
 * 等待队列节点类.
 * 等待队列是“CLH”(Craig、Landin和Hagersten)锁队列的变体。CLH锁通常用于自旋锁。相反,我们使用它们来阻塞同步器,但是使用相同的基本策略,即在其节点的前身中保存关于线程的一些控制信息。每个节点中的“status”字段跟踪线程是否应该阻塞。一个节点在它的前一个节点释放时发出信号。队列的每个节点作为一个特定通知样式的监视器,
 * 它持有一个等待线程。状态字段不控制线程是否被授予锁等.
 */
static final class Node {
    /** 指示节点在共享模式下等待的标记 */
    static final Node SHARED = new Node();
    /** 指示节点正在独占模式中等待的标记 */
    static final Node EXCLUSIVE = null;

    /** 表示线程已取消的等待状态值 */
    static final int CANCELLED =  1;
    /** 等待状态值,表示当前节点的后继节点的线程需要被唤醒 */
    static final int SIGNAL    = -1;
    /** 表示线程处于等待状态 */
    static final int CONDITION = -2;
    /**
     * 表示下一个被默认对象的等待状态值应该无条件传播
     */
    static final int PROPAGATE = -3;

    /**
     * 等待状态.
     */
    volatile int waitStatus;

    /**
     * 当前节点的前面一个节点.
     */
    volatile Node prev;

    /**
     * 当前节点的后面一个节点.
     */
    volatile Node next;

    /**
     * 当前节点的线程.
     */
    volatile Thread thread;

    /**
     * 链接到正在等待状态的下一个节点,或共享的特殊值.
     */
    Node nextWaiter;

    /**
     * 如果节点在共享模式下等待,则返回true.
     */
    final boolean isShared() {
        return nextWaiter == SHARED;
    }

    /**
     * 当前节点的前面一个节点
     */
    final Node predecessor() throws NullPointerException {
        Node p = prev;
        if (p == null)
            throw new NullPointerException();
        else
            return p;
    }

    Node() {    // 用于建立初始标头或共享标头
    }

    Node(Thread thread, Node mode) {     // Used by addWaiter
        this.nextWaiter = mode;
        this.thread = thread;
    }

    Node(Thread thread, int waitStatus) { // Used by Condition
        this.waitStatus = waitStatus;
        this.thread = thread;
    }
}

【c】核心方法说明

(1)、acquire(int arg) : 独占锁,并且忽略中断

/**
 * 在独占模式下获取,忽略中断。通过至少调用一次tryAcquire来实现,成功后返回。
 *   否则,线程将排队,可能会重复阻塞和取消阻塞,调用tryAcquire直到成功.
 *   当一个线程成功的获取到锁(同步状态),其他线程无法获取到锁,而是被构造成节点(包含当前线程,等待状态)加入到同步队列中等待获取到锁的线程释放锁
 */
public final void acquire(int arg) {
    //tryAcquire必须由子类去实现,返回true/false
    //获取不成功,执行acquireQueued使线程进入等待队列
    if (!tryAcquire(arg) &&
        acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 尝试获取同步状态(锁)失败,构造同步节点加入到链表队尾
        selfInterrupt();
}

//子类必须实现此方法,返回true/false
protected boolean tryAcquire(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * 为当前线程和给定模式创建排队节点
 * 将节点加入到队列中进行等待
 */
private Node addWaiter(Node mode) {
    //当前线程,指定的mode,共享或者独占
    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;
}

//入队
private Node enq(final Node node) {
    //自旋
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            //cas设置头节点
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            //通过CAS将节点设置成为尾节点
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

/**
 * 以独占模式、不可中断模式获取队列中已存在的线程
   独占模式处理正在排队等待的线程
 */
final boolean acquireQueued(final Node node, int arg) {
    //标识当前是否获取成功
    boolean failed = true;
    try {
        //标识是否被中断
        boolean interrupted = false;
        //自旋,直至获取成功才返回
        for (;;) {
            //返回前一个节点,如果为空则抛出NullPointerException
            final Node p = node.predecessor();
            //当前节点的前驱节点是头节点,当前节点就应该执行tryAcquire尝试获取资源
            //注意:只有前驱节点是头节点才会去尝试获取同步状态(锁)
            if (p == head && tryAcquire(arg)) {
                //将当前节点设置为头结点,移除之前的头节点
                setHead(node);
                p.next = null; // help GC
                failed = false;
                return interrupted;
            }
            //shouldParkAfterFailedAcquire: 检查和更新未能获取的节点的状态。如果线程阻塞,返回true
            if (shouldParkAfterFailedAcquire(p, node) &&
            //parkAndCheckInterrupt: 调用LockSupport.park阻塞自己,等待其他线程释放资源时唤醒自己
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            //取消正在进行的获取尝试
            cancelAcquire(node);
    }
}

//检查和更新未能获取的节点的状态。如果线程阻塞,返回true
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
    //前面一个节点中保存的等待状态
    int ws = pred.waitStatus;
    if (ws == Node.SIGNAL) //如果前面一个节点的waitStatus等于-1,则阻塞线程
        /*
         * This node has already set status asking a release
         * to signal it, so it can safely park.
         */
        return true;
    if (ws > 0) {
        /*
         * Predecessor was cancelled. Skip over predecessors and
         * indicate retry.
         */
         //循环查找,直到找到不是cancelled状态的节点,然后把自己放在它后面
        do {
            node.prev = pred = pred.prev;
        } while (pred.waitStatus > 0);
        pred.next = node;
    } else {
        /*
         * waitStatus must be 0 or PROPAGATE.  Indicate that we
         * need a signal, but don't park yet.  Caller will need to
         * retry to make sure it cannot acquire before parking.
         */
        compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
    }
    return false;
}

//阻塞自己,等待其他线程释放资源时唤醒
private final boolean parkAndCheckInterrupt() {
    //挂起当前线程
    LockSupport.park(this);
    return Thread.interrupted();
}

独占锁的获取大体流程:

  •    (1) 当前线程实现通过tryAcquire()方法尝试获取锁,获取成功的话直接返回,如果尝试失败的话,进入等待队列排队等待,可以保证线程安全(CAS)的获取同步状态。
  •     (2) 如果尝试获取锁失败的话,构造同步节点(独占式的Node.EXCLUSIVE),通过addWaiter(Node node,int args)方法,将节点加入到同步队列的队列尾部。
  •     (3) 最后调用acquireQueued(final Node node, int args)方法,使该节点以死循环的方式获取同步状态,如果获取不到,则阻塞节点中的线程。acquireQueued方法当前线程在死循环中获取同步状态,而只有前驱节点是头节点的时候才能尝试获取锁(同步状态)( p == head && tryAcquire(arg))。

同理,acquireInterruptibly(int arg)方法的实现与acquire(int arg)大体类似,不同的是:acquireInterruptibly当线程interrupt的时候,会抛出InterruptedException。

(2)、acquireShared(int arg):获取共享锁,忽略中断

/**
 * 忽略中断,以共享模式获取。首先调用至少一次tryacquirered来实现,成功后返回。
    否则,线程将排队,可能会反复阻塞和取消阻塞,调用tryacquirered直到成功。.
 */
public final void acquireShared(int arg) {
    if (tryAcquireShared(arg) < 0)
        //子类返回负值表示失败,然后执行doAcquireShared方法
        doAcquireShared(arg);
}

//同样需要子类去实现
protected int tryAcquireShared(int arg) {
    throw new UnsupportedOperationException();
}

/**
 * 以共享模式、不可中断模式获取
 */
private void doAcquireShared(int arg) {
    //添加一个共享节点
    final Node node = addWaiter(Node.SHARED);
    boolean failed = true;
    try {
        boolean interrupted = false;
        //自旋
        for (;;) {
            //获取前面一个节点
            final Node p = node.predecessor();
            if (p == head) {
                //tryAcquireShared也是由子类实现
                int r = tryAcquireShared(arg);
                if (r >= 0) {
                    setHeadAndPropagate(node, r);
                    p.next = null; // help GC
                    if (interrupted)
                        selfInterrupt();
                    failed = false;
                    return;
                }
            }
            //同acquire(int arg) ,判断是否需要阻塞
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                interrupted = true;
        }
    } finally {
        if (failed)
            ///取消正在进行的获取尝试
            cancelAcquire(node);
    }
}

同理,acquireSharedInterruptibly(int arg)与acquireShared(int arg)基本类似,不同之处就是acquireSharedInterruptibly会抛出InterruptedException异常。

(3)、 release(int arg):独占模式释放,忽略中断。

/**
 * 以独占的模式释放。如果tryRelease返回true,则通过解除一个或多个线程的阻塞来实现.
 */
public final boolean release(int arg) {
    //子类实现如果返回true,则释放
    //tryRelease:尝试释放当前线程的同步状态(锁)
    if (tryRelease(arg)) {
        //h: 头部节点
        Node h = head;
        //如果h不为空并且waitStatus 不等于0
        if (h != null && h.waitStatus != 0)
            //唤醒node的后继节点
            unparkSuccessor(h);
        return true;
    }
    return false;
}

/**
 * 唤醒node的后继节点(如果存在的话)
 */
private void unparkSuccessor(Node node) {
    //头节点中保存的waitStatus
    int ws = node.waitStatus;
    if (ws < 0)
        //cas重置头节点状态为0
        compareAndSetWaitStatus(node, ws, 0);

    /*
     * 获取node的下一个节点.
     */
    Node s = node.next;
    //后面一个节点为空或者已取消
    if (s == null || s.waitStatus > 0) {
        s = null;
        //从尾部节点开始往前找,直到找到waitStatus <= 0的节点
        for (Node t = tail; t != null && t != node; t = t.prev)
            if (t.waitStatus <= 0)
                s = t;
    }
    if (s != null)
        //唤醒node后面一个节点中的线程
        LockSupport.unpark(s.thread);
}

独占锁释放总结:

同步队列遵循FIFO,头节点是获取锁(同步状态)成功的节点,头节点在释放同步状态的时候,会唤醒后继节点,而后继节点将会在获取锁(同步状态)成功时候将自己设置为头节点。设置头节点是由获取锁(同步状态)成功的线程来完成的,由于只有一个线程能够获取同步状态,则设置头节点的方法不需要CAS保证,只需要将头节点设置成为原首节点的后继节点 ,并断开原头结点的next引用。

五、总结

AQS抽象队列同步器是很多锁实现的基础,像常用的ReentrantLock,CountDownLatch等底层都是 通过AQS来实现获取锁和释放锁的,具体可以参考对应的源码。AQS在获取锁时,同步器维护一个同步队列,获取锁失败的线程会构建成一个节点添加到同步队列的尾部,然后自旋判断前面一个节点是头节点,直到成功获取到锁。在释放锁的时候,首先会尝试释放同步状态,然后唤醒头节点的后面一个节点。AQS主要是通过volatile修饰的status状态来标识状态的。

参考资料:

https://www.jianshu.com/p/a8d27ba5db49

https://www.cnblogs.com/200911/p/6031350.html

https://www.jianshu.com/p/5ba3874f691b

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 数字20 设计师:CSDN官方博客 返回首页
实付 19.90元
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值