【高并发系列-3】AQS和Condition详解

引言:接上一篇博文java线程池的阻塞队列详解,讲到线程池复用时用的阻塞队列时用到了Lock和Condition,这一篇对这AQS和Condition进行讲解

AQS是什么

AQS的全称是AbstractQueuedSynchronizer,是抽象队列同步器,是一个用来构建锁和同步器的框架,java自带的synchronized关键字之外的锁机制,常用的有ReentrantLock、 CountDownLatch(栅栏)、Semaphore(信号量)都基于他实现,它核心思想在于“如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态,其他线程进入到aqs队列中进行排队等待”,他的核心数据结构是,int state同步状态,双向队列(Node tail +Node head)、以及cas自旋。

定义了两种资源共享方式:

  • 独占:ReentrantLock
  • 共享:CountDownLatch、Semaphore

 AbstractQueuedSynchronizer维护了一个state状态和一个先进先出的队列:

public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer implements java.io.Serializable {
	...
	/**
	 * The synchronization state.
	 */
	private volatile int state;

    /**
     * Head of the wait queue, lazily initialized.  Except for
     * initialization, it is modified only via method setHead.  Note:
     * If head exists, its waitStatus is guaranteed not to be
     * CANCELLED.
     */
    private transient volatile Node head;

    /**
     * Tail of the wait queue, lazily initialized.  Modified only via
     * method enq to add new wait node.
     */
    private transient volatile Node tail;

	/**
     * Wait queue node class.
     **/
     static final class Node {
		...
	}
	...
}

因为AbstractQueuedSynchronizer是一个抽象类,他采用模板方法的设计模式,规定了独占和共享模式需要实现的方法,并且将一些通用的功能已经进行了实现,所以不同模式的使用方式,只需要自己定义好实现共享资源的获取与释放即可,至于具体线程在等待队列中的维护(获取资源入队列、唤醒出队列等),AQS已经实现好了。

根据根据共享资源的模式一般实现的方法有如下几个:

isHeldExclusively();// 是否为独占模式;但是只有使用到了Condition的,才需要去实现它。例如:ReentrantLock。
boolean tryAcquire(int arg); // 独占模式;尝试获取资源,成功返回true,失败返回false。
boolean tryRelease(int arg) ; // 独占模式;尝试释放资源,成功返回true,失败返回false。
int tryAcquireShared(int arg); // 共享模式;尝试获取资源,负数表示失败;0表示成功,但是没有剩余可用资源了;正数表示成功,且有剩余可用资源。
boolean tryReleaseShared(int arg) ; // 共享欧式;尝试释放资源,若释放资源后允许唤醒后续等待节点返回true,否则返回false。
上面的这几个方法在AbstractQueuedSynchronizer这个抽象类中,都没有被定义为abstract的,说明这些方法都是可以按需实现的,共享模式下可以只实现共享模式的方法(例如CountDownLatch),独占模式下可以只实现独占模式的方法(例如ReentrantLock),也支持两种都实现,两种模式都使用(例如ReentrantReadWriteLock)。
 

AQS源码解析-以ReentrantLock为例子

ReentrantLock中实现了Lock接口,他的内部类Sync继承自AbstractQueuedSynchronizer,具体的实现又由FairSync和unFairSync继承自Sync抽象类,当调用reentrantLock.lock方法时,实质上是调用了aqs的acquire方法:

由于RentrantLock默认是非公平锁,就是说,线程在竞争锁的时候并不是按照先来后到的顺序来获取锁的,但是ReentrantLock也是支持公平锁的,在创建的时候传入一个参数值即可。下面以为非公锁为例

java.util.concurrent.locks.ReentrantLock.NonfairSync#lock

static final class NonfairSync extends Sync {
        private static final long serialVersionUID = 7316153563782823691L;

        /**
         * Performs lock.  Try immediate barge, backing up to normal
         * acquire on failure.
         */
        final void lock() {
            if (compareAndSetState(0, 1))
                setExclusiveOwnerThread(Thread.currentThread());
            else
                acquire(1);
        }
}

compareAndSetState的逻辑很简单,就是通过cas设置到state同步状态字段中,如果A线程设置成功了就直接返回了,那么B线程在A释放前执行本方法进入acquire(1)逻辑中,里面会阻塞当前线程,从而进入阻塞状态

AbstractQueuedSynchronizer#acquire 

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

java.util.concurrent.locks.ReentrantLock.NonfairSync#tryAcquire

 由上可知,同一个线程多次设置,均可以返回true,从而acquire()方法能够正常返回,而其他线程设置失败返回false,也就是竞争锁失败的线程,则需要执行addWaiter方法和acquireQueued方法

 java.util.concurrent.locks.AbstractQueuedSynchronizer#addWaiter

/**
     * Creates and enqueues node for current thread and given mode.
     *
     * @param mode Node.EXCLUSIVE for exclusive, Node.SHARED for shared
     * @return the new node
     */
    private Node addWaiter(Node mode) {
        Node node = new Node(Thread.currentThread(), mode);
        // Try the fast path of enq; backup to full enq on failure
        Node pred = tail;
        if (pred != null) {
            node.prev = pred;
            if (compareAndSetTail(pred, node)) {
                pred.next = node;
                return node;
            }
        }
        enq(node);
        return node;
    }

addWaiter()方法是将当前线程封装成Node节点,然后进行入队列的操作,并对头尾节点,next节点等进行设置

java.util.concurrent.locks.AbstractQueuedSynchronizer#acquireQueued

acquireQueued会不断尝试从waiter队列中尝试出队(for死循环),只要出队了意味着当前线程可以执行同步语句块中的内容(由lock和unlock包嵌的代码块),底层是通过LockSupport.park  unpark()进行线程间的通讯,线程A调用unlock时会调用unpark()

注意:只有前驱节点是head节点的Node,才有机会执行tryAcquire()方法尝试获取锁,其他进入waiter队列的其他节点只能继续等待

以上就是整个AQS的ReentrantLock如何使用同步队列实现的线程阻塞和唤醒操作的流程代码。对于调用lock()方法的线程,阻塞的线程状态是WAITING态,调用带timeout参数的tryLock方法,则是TIMED_WAITING状态,而synchronized同步语句块阻塞的线程则是BLOCKED状态.

简单来说,就是AQS维护了一个同步队列,所有抢占锁失败的线程都在加入到队列尾部,随后线程会阻塞;释放锁时会唤醒头结点的后继结点,使其加入对同步状态的争夺中。其中head节点就是当前抢占到了锁正在执行同步代码的节点(除开第一次lock直接compareAndSetstate成功的)

在这里插入图片描述

PS:  调用ReentrantLock的lock()方法和unlock()方法,是不会调用到condition.await()和condition.signal()方法的,调用的是LockSupport.park(thread) 和LockSupport.unpark(thread)方法!!!!!!!!!!!!

什么是Condition?

首先Condition是一个抽象类,他的具体实现是在AbstractQueuedSynchronizer的内部类ConditionObject作了具体实现,通过ReentrantLock的newCondition()方法可以获取到一个condition实例,通过condition,可以实现类似Object中的wait()  notify()相同的效果,来达到线程之间通讯的目的。

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();

我们在上一段中说到的都是aqs的同步队列,存储的是被lock()方法阻塞住的线程,所有被lock阻塞的线程都会进入同步队列中,直到拿到锁的线程释放后才可以执行。但我们知道拿到锁的线程也可以主动调用await方法,来释放锁,这时候,这个主动调用await方法的线程,又会存储到哪里?答案就是condition的条件等待队列。

一开始A拿到了同步锁,B获取锁失败,因此进入到AQS的同步等待队列中,此时同步队列和条件队列如下图所示:

在这里插入图片描述

后来A主动调用condition.await方法释放了锁,此时B会尝试获取锁,此时两个队列状态如下:

在这里插入图片描述

当B运行时候,会调用signal方法,将Condition队列中的线程加入到AQS队列中

在这里插入图片描述

B执行完毕后,A会尝试获取同步锁。最终得以继续执行。 

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值