06. J.U.C 核心——AQS


前言

AQS(AbstractQueuedSynchronizer)java.util.concurrent(J.U.C)包下的一个抽象类。concurrent包下提供了一系列同步组件大大提高了并发的性能,包括ReentrantLockCountDownLatchsemaphore以及CyclicBarrier等,这些同步组件都直接或间接基于 AQS 提供的独占锁共享锁等待队列实现了各自的同步需求,所以 AQS 被认为是 J.U.C的核心。


一、AQS 简介

AQS 全名:AbstractQueuedSynchronizer,是java.util.concurrent.locks包下的一个类。

  • 在同步组件的实现中,AQS 是核心部分,同步组件通过使用 AQS 提供的模板方法实现同步组件的语义;
  • AQS 则实现了对同步状态的管理、对阻塞线程进行排队以及等待和唤醒等一些底层的实现处理;
  • AQS 的核心主要包括:同步队列、独占锁的获取和释放、共享锁的获取和释放、可中断锁,以及超时等待获取锁这些特性的实现,而这些特性实际上是 AQS 提供的模板方法。

二、AQS 源码分析

1. AQS 的核心思想

如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态;如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,AQS 就是通过 CLH 队列来实现这个机制的,也就是将暂时获取不到锁的线程加入到等待队列中。

2. 资源 state

AQS 使用一个 int 型的成员变量来表示同步状态,即 state(上图中的资源 state),内部声明如下:

/**
* The synchronization state.
*/
private volatile int state;

不同的 state 值表示不同的状态,对 state 变量值的修改由其子类实现。比如,在 ReentrantLock 中,state == 0 表示还没有线程获取锁,state == 1 表示有线程获取了锁,state > 1 表示重入锁的数量。

AQS 的两个方法 acquire(int arg)release(int arg) 分别通过增大和减小 state 的值来获取和释放一定量的资源。子类通过继承 AQS 并实现这两个方法管理状态:

  • acquire() 方法中,若 state 的大小符合特定需求(具体逻辑由子类实现),则线程会锁定同步器;否则,将当前线程加入到同步队列中。
  • release() 方法中,若 state 的大小符合特定需求,则释放掉当前线程占有的资源,唤醒同步队列中的线程。

状态信息通过 AQSgetState()setState()compareAndSetState() 进行操作,方法的源码如下:

 	/**
     * Returns the current value of synchronization state.
     * 
     * 返回同步状态的当前值
     */
    protected final int getState() {
        return state;
    }

    /**
     * Sets the value of synchronization state.
     * 
     * 设置同步状态的值
     */
    protected final void setState(int newState) {
        state = newState;
    }

    /**
     * Atomically sets synchronization state to the given updated
     * value if the current state value equals the expected value.
     * 
     * 如果当前同步状态的值等于期望值(expect),通过CAS操作,将同步状态值设定为给定值update
     */
    protected final boolean compareAndSetState(int expect, int update) {
        // See below for intrinsics setup to support this
        return unsafe.compareAndSwapInt(this, stateOffset, expect, update);
    }

3. 独占锁与共享锁

concurrent 包下提供的同步组件的加锁模式分为独占锁和共享锁。独占锁模式下,每次只能有一个线程持有锁,例如 ReentrantLock 实现的就是独占锁;共享锁模式下,则允许多个线程同时获取锁,并发地访问共享资源,例如 ReadWriteLockcountdownlatchsemaphore 等组件实现的是共享锁,锁的数量由用户指定。

显然,独占锁是一种悲观保守的加锁策略。如果某个只读线程获取锁,则其它读线程都只能等待,这种情况下就限制了不必要的并发性,因为读操作并不会影响数据的一致性。而共享锁则是一种乐观锁,它放宽了加锁策略,允许多个线程同时访问资源。比如并发包下的 ReadWriteLock(读写锁),它允许一个资源可以被多个读操作访问,或者被一个写操作访问,但两者不能同时执行。

独占锁:

  • void acquire(int arg):独占式获取同步状态,如果获取失败则插入同步队列等待;
  • void acquireInterruptibly(int arg):与 acquire() 方法相同,但在同步队列中等待时可以响应中断;
  • boolean tryAcquireNanos(int arg, long nanosTimeout):在可响应中断的 acquire() 方法基础上增加了超时功能,在超时时间内成功获取锁则返回 true,否则返回 false;
  • boolean tryAcquire(int arg):尝试获取锁,获取锁成功返回 true,否则返回 false;
  • boolean release(int arg):释放同步状态,该方法会唤醒在同步队列中的下一个节点(线程)。

共享锁:

  • void acquireShared(int arg):共享式获取同步状态,与独占锁的区别在于同一时刻有多个线程获取同步状态;
  • void acquireSharedInterruptibly(int arg):增加了响应中断的功能;
  • boolean tryAcquireSharedNanos(int arg, long nanosTimeout):增加了超时等待功能;
  • boolean releaseShared(int arg):共享锁释放同步状态。

4. 同步队列

当共享资源被某个线程占有,其它请求该资源的线程将会阻塞,从而进入同步队列。就数据结构而言,AQS 实现的是一个 FIFO 的队列,底层实现是一个带头节点的双向链表。如下图所示:

在这里插入图片描述

AQS 有一个静态内部类 Node,定义了同步队列中每个具体的节点。部分源码如下:

static final class Node {
        // 标识共享模式的节点,共享模式下Node节点的nextWaiter变量设置为这个值
        static final Node SHARED = new Node();
        
        // 标识独占模式的节点,独占模式nextWaiter变量是null
        static final Node EXCLUSIVE = null;

        // 当前节点从同步队列中取消
        static final int CANCELLED =  1;
        
        // 后继节点的线程处于等待状态,如果当前节点释放同步状态会通知后继节点,使得后继 节点的线程继续运行
        static final int SIGNAL    = -1;
        
        // 节点在等待队列中,节点线程等待在Condition上,当其他线程对Condition调用了 signal()方法后,该节点将会从等待队列中转移到同步队列中,加入到对同步状态的获取中。
        static final int CONDITION = -2;
        
        // 表示下一次共享式同步状态将会无条件地被传播下去
        static final int PROPAGATE = -3;

        /**
         * 节点状态
         */
        volatile int waitStatus;

        /**
         * Link to predecessor node that current node/thread relies on
         * for checking waitStatus. 
         * 
         * 当前节点的前驱节点
         */
        volatile Node prev;

        /**
         * Link to the successor node that the current node/thread
         * unparks upon release.
         * 
         * 当前节点的后继节点
         */
        volatile Node next;

        /**
         * The thread that enqueued this node. 
         * 
         * 当前节点锁包装的线程对象
         */
        volatile Thread thread;

        /**
         * Link to next node waiting on condition, or the special
         * value SHARED.  
         * 
         * 等待队列中的下一个节点
         */
        Node nextWaiter;
    }

类中的几个 static final 变量表示节点的状态,几个由 volatile 修饰的属性表示每个节点的属性。可以看到每个节点都有前驱节点和后继节点,所以同步队列是一个双向链表。AQS 实际上通过头尾指针来管理同步队列,同时实现包括将获取锁失败的线程入队、释放锁时唤醒同步队列中的等待线程等和新方法

5. 独占锁的获取

下面我们通过 ReentrantLock 的源码分析独占锁的获取和释放。

调用 lock() 方法获取的是独占锁,获取成功则线程执行,获取失败则调用 AQS 提供的 acquire(int arg) 模板方法将当前线程加入同步队列。 lock() 方法源码:

final void lock() {    
	if (compareAndSetState(0, 1))        
		setExclusiveOwnerThread(Thread.currentThread());   
	else       
		acquire(1); 
}

lock() 方法使用 CAS 操作尝试将同步状态改为 1,如果成功则将同步状态持有线程置为当前线程,否则就调用 AQS 提供的 acquire(int arg) 方法。

public final void acquire(int arg) {
 	    // 再次尝试获取同步状态,如果成功则方法直接返回    
 	    // 如果失败则先调用addWaiter()方法再调用acquireQueued()方法
        if (!tryAcquire(arg) &&
            acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
            selfInterrupt();
}

tryAcquire(int arg):再次尝试获取同步状态,获取成功方法直接返回;获取失败则调用 addWaiter() 方法;
addWaiter(Node.EXCLUSIVE, arg):将当前线程以指定模式(独占式、共享式)封装为 Node 节点并将其入队。

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;
                // CAS尾插成功,返回当前Node节点
                return node;
            }
        }
        // 尾节点为空 || CAS尾插失败
        enq(node);
        return node;
    }

分析 addWaiter() 方法,程序的逻辑主要分为两部分:

  • 当前同步队列的尾节点为 null,则调用 enq() 方法插入节点;
  • 当前同步队列的尾节点不为 null,则采用尾插法将当前封装的节点插入到同步队列中。当然,如果 if(compareAndSetTail(pred, node)) 为 false 的话,则说明 CAS 操作失败,就会继续执行到 enq() 方法。

下面我们再看 enq() 方法:

private Node enq(final Node node) {
    	// 直到将当前节点插入同步队列成功为止
        for (;;) {
            Node t = tail;
            // 初始化同步队列
            if (t == null) { // Must initialize
                if (compareAndSetHead(new Node()))
                    tail = head;
            } else {
            	// 不断CAS将当前节点尾插入同步队列中
                node.prev = t;
                if (compareAndSetTail(t, node)) {
                    t.next = node;
                    return t;
                }
            }
       }
}

我们知道同步队列是带头节点的链式存储结构,在创建 Node 类时就创建了头节点。带头节点的队列的初始化时机是:当 tail == null 时,即第一次往同步队列插入线程的时候。并且 enq() 方法中是一个 for(;;) 死循环,compareAndSetTail(t, node) 方法会利用 CAS 操作操作设置尾节点,如果失败则在循环中不断重新尝试,直至成功返回。

因此, enq() 方法的两个功能:

  • 在当前线程是第一个加入同步队列时,调用 compareAndSetHead(new Node()) 方法,完成链式队列头节点的初始化;
  • 自旋不断尝试 CAS 尾插入节点直至成功。

通过上面的分析我们已经知道了将获取独占锁失败的线程包装成 Node 节点然后插入同步队列的过程。下面我们来看看:在同步队列中的节点(线程)是怎样保证自己有机会获得独占锁的。

final boolean acquireQueued(final Node node, int arg) {
        boolean failed = true;
        try {
            boolean interrupted = false;
            // 自旋
            for (;;) {
            	// 获取当前节点的前驱节点
                final Node p = node.predecessor();
                // 获取同步状态成功条件
                // 前驱节点为头结点并且获取同步状态成功
                if (p == head && tryAcquire(arg)) {
                	// 将当前节点设置为头结点
                    setHead(node);
                    // 删除原来的头结点
                    p.next = null; // help GC
                    failed = false;
                    return interrupted;
                }
                if (shouldParkAfterFailedAcquire(p, node) &&
                    parkAndCheckInterrupt())
                    interrupted = true;
            }
        } finally {
        	// 获取失败将当前节点取消
            if (failed)
                cancelAcquire(node);
        }
}

acquireQueued() 方法内部依然是是一个通过 for(;;) 自旋的过程:首先获取当前节点的前驱节点,如果前驱节点是头节点并且成功获得同步状态时(if(p == head && tryAcquire(arg))),表示当前节点指向的线程能够获取锁;反之,获取锁失败则进入等待状态,先不断自旋将前驱节点状态置为 SIGINAL,然后调用 LockSupport.park() 方法将当前线程阻塞。 下面我们详细分析这两个处理过程:

获取锁成功并将头节点出队的过程:

AQS 实际是通过头尾指针来管理同步队列的,示意图如下:

在这里插入图片描述

我们看 acquireQueued() 方法内获取锁成功并将头节点出队的逻辑:

// 当前节点前驱为头结点并且再次获取同步状态成功
if (p == head && tryAcquire(arg)) {
	//队列头结点引用指向当前节点 
	setHead(node); 
	//释放前驱节点 
	p.next = null; // help GC
	failed = false;
	return interrupted;
}

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

将当前节点通过 setHead() 方法设置为队列的头节点,然后将之前的头节点的 prenext 均指向 null,即将其与队列断开,垃圾回收器会将其回收。如下图:

在这里插入图片描述

获取锁失败后自旋处理的过程:

当节点在同步队列中获取锁失败的时候会调用 shouldParkAfterFailedAcquire() 方法。该方法的主要逻辑是:使用 CAS 操作将前驱节点的状态由 INITIAL 置为 SIGNAL,表示需要将当前节点阻塞。如果失败,则会在 acquireQueued() 方法中自旋直到将前驱节点状态置为 SIGNAL 为止。只有当 shouldParkAfterFailedAcquire() 方法返回 true 时才会执行 parkAndCheckInterrupt() 方法。

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

该方法会调用 LookSupport.park() 方法,用来阻塞当前线程。

总结下来,acquireQueued() 在自旋过程中主要完成了两件事:

  • 如果当前节点的前驱节点是头节点,并且能够获得同步状态的话,当前线程能够获得锁并在该方法执行结束后退出;
  • 获取锁失败的话,先将节点的状态置为 SIGNAL,然后调用 LookSupport.park() 方法将当前线程阻塞。

6. 独占锁的释放(release() 方法)

独占锁释放锁时调用了 unlock() 方法,unlock() 方法实际上调用了 AQSrelease() 方法:

public void unlock() {    
	sync.release(1); 
}

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() 返回 true),则会执行 if 块中的代码。当 head 指向的头节点不为 null,且该节点的状态值不为 0 时才执行 unparkSuccessor() 方法。

private void unparkSuccessor(Node node) {
        /*
         * If status is negative (i.e., possibly needing signal) try
         * to clear in anticipation of signalling.  It is OK if this
         * fails or if status is changed by waiting thread.
         */
        int ws = node.waitStatus;
        if (ws < 0)
            compareAndSetWaitStatus(node, ws, 0);

        /*
         * Thread to unpark is held in successor, which is normally
         * just the next node.  But if cancelled or apparently null,
         * traverse backwards from tail to find the actual
         * non-cancelled successor.
         */
         // 头结点的后继节点 
        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;
        }
        if (s != null)
        	 // 后继节点不为null时唤醒 
            LockSupport.unpark(s.thread);
    }

首先获取头节点的后继节点,当后继节点不为空时会调用 LookSupport.unpark() 方法,该方法会唤醒该节点的后继节点所包装的线程。因此,每一次释放锁之后,都会唤醒队列中该节点的后继节点所引用的线程

7. 独占锁获取与释放总结

  • 线程获取锁失败,则调用 addWaiter() 方法将线程封装成 Node 进行入队操作。addWaiter() 方法内部会调用 enq() 方法完成对同步队列头节点的初始化以及 CAS 尾插失败的自旋处理。
  • 入队之后排队获取锁的核心方法是 acquireQueued() 方法,节点排队获取锁是一个自旋过程。当且仅当当前节点的前驱节点为头节点并且成功获取同步状态时,头节点出队,当前节点引用的线程获取到锁。否则,不满足条件时就不断自旋将前驱节点的状态置为 SIGNAL 后调用 LockSupport.part() 方法将线程阻塞。
  • 释放锁时会唤醒后继节点。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值