《Java后端知识体系》系列之AQS详解

概览:

  • 在并发编程中不得不提到的就是AQS(AbstractQueueSynchronizer)抽象同步队列,它是实现同步器的基础组件,并发包中锁的底层实现就是使用AQS来实现的。AQS的结构图如下:

在这里插入图片描述

  • 从该图中可以看到AQS是一个FIFO(先进先出)双向队列,其内部节点通过head和tail记录队首和队尾元素,队列元素的类型为Node,其中Node中的thread变量用来存放AQS队列中的线程;Node节点内部的SHARED用来标记该线程是获取共享资源时被阻塞挂起后放入AQS队列的,EXCLUSIVE用来标记线程是获取独占资源时被挂起后放入AQS队列的;waitStatus是记录当前线程的等待状态,可以为CANCELLED(线程被取消)、SIGNAL(线程需要被唤醒)、CONDITION(线程在条件队列中等待)、PROPAGATE(释放共享资源时需要通知其它节点);prev记录当前节点的前驱节点,next记录当前节点的后继节点。
  • 在AQS中维护了一个单一的状态信息state,可以通过getState、setState、CompareAndSetState函数来修改其值。
    • 对于ReentrantLock的实现来说,state可以用来表示当前线程获取锁的可重入次数
    • 对于ReentrantReadWriteLock来说,state高16为表示状态,也就是获取该读锁的次数,低16位表示获取到写锁的的线程的可重入次数;
    • 对于Semaphore来说,state用来表示当前可用信号的个数,
    • 对于CountDownlatch来说,state用来表示计数器当前的值。
  • AQS有个内部类ConditionObject,用来结合锁实现线程同步。ConditionObject可以直接访问AQS对象内部的变量,比如state状态值和AQS队列,ConditionObject是条件变量,每个条件变量对应一个条件队列(单向链表队列),其用来存放调用条件变量的await方法后被阻塞的线程。
  • 对于AQS来说,线程同步的关键是对状态值state进行操作,根据state是否属于一个线程,操作state的方式分为

在独占方式下获取和释放资源的方法为:

void acquire( int arg) 
void acquirelnterruptibly(int arg)
boolean release( int arg)
  • 使用独占锁方式获取资源是跟线程绑定的,就是说如果一个线程获取到了资源,就会标记是这个线程获取了,其它线程再尝试操作state获取资源时发现当前资源不是自己持有的,就会获取失败后被阻塞。比如独占锁ReentrantLock的实现,当一个线程获取了ReentrantLock的锁后,在AQS内部会首先使用CAS操作把state状态值从0变为1,然后设置当前锁的持有者为当前线程,当该线程再次获取锁时发现锁的持有者是自己,那么就会把state加1,也就是可以设置重入次数,而当另一个线程获取锁时发现自己并不是该锁的持有者就会被放入AQS阻塞队列后挂起。

在共享方式下获取和释放资源的方法为:

void acquireShared(int arg) 
void acquireSharedinterruptibly(int arg)
boolean reeaseShared(int arg)
  • 对于共享式来说,当多个线程去请求共享资源时通过CAS方式竞争获取资源,当一个线程获取到资源之后,另一个线程再去获取时如果当前资源还能满足它的需要,则当前线程只需要通过CAS方法进行获取即可。比如Semaphore信号量,当一个线程通过acquire()方式获取信号量时,会首先查看当前信号量个数是否满足需要,不满足则把当前线程放入阻塞队列,如果满足则通过自旋CAS获取信号量。

在独占方式下,获取与释放资源的流程如下:

  1. 当一个线程调用acquire(int arg)方法获取独占资源时,会首先使用tryAcquire方法尝试获取资源,具体时设置状态变量state的值,成功则直接返回,失败则将当前线程封装为Node.EXCLUSIVE的Node节点后插入AQS阻塞队列的尾部,并调用LockSupport.park(this)方法挂起自己。

    public final void acquire(int arg){
    	if(!tryAcquire(arg)&&acquireQueued(addWaiter(Node.EXCLUSIVE),arg))
    	selfInterrupt();
    }
    
  2. 当一个线程调用release(int arg)方法时会尝试使用tryRelease操作释放资源,这里是设置状态变量state的值,然后调用LockSupport.unpark(thread)方法激活AQS队列里面被阻塞的一个线程(thread)。被激活的线程则使用tryAcquire尝试,看当前状态变量state的值是否满足自己的需要,满足则激活该线程,然后继续向下运行,否则还会被放入AQS队列并挂起。

    public final boolean release (int arg) {
    	if (tryRelease(arg )) {
    		Node h = head;
    		if (h 1= null && h .waitStatus != 0)
    			unparkSuccessor(h);
    			return true ;
    		}
    	return false; 
    }
    
  3. 需要注意的是,AQS类并没有提供可用的tryAcquire和tryRelease方法,正如AQS的锁阻塞和同步器的基础框架一样,tryAcquire和tryRelease需要具体的子类来实现。子类在实现tryAcquire和tryRelease时要根据具体场景使用CAS算法尝试修改state的值,成功则返回true,否则返回false。子类还需要定义在调用acquire和release方法时state状态值的增减代表什么含义。

  4. 比如继承自AQS实现的独占锁ReentrantLock,定义当state为0时表示锁空闲,不为0时表示锁已经被占用,在重写tryAcquire时,在内部需要使用CAS算法查看当前state是否为0,如果为0则使用CAS设置为1,并设置锁的持有者为当前线程,而后返回true,如果CAS失败则返回false。

在共享方式下获取与释放锁的流程如下:

  1. 当线程调用acquireShared(int arg)获取共享资源时,会首先使用trγAcquireShared尝试获取资源,具体是设置状态变量state的值,成功则直接返回,失败则将当前线程封装为Node.SHARED的Node节点后插入到AQS阻塞队列的尾部,并使用LockSupport.park(this)方法挂起自己。

    public final void acquireShared(int arg){
    	if(tryAcquireShared(arg)<0)
    		doAcquireShared(arg)	
    }
    
  2. 当一个线程调用releaseShared(int arg)时会尝试使用tryReleaseShared操作释放资源,这里是设置状态变量state的值,然后使用LockSupport.unpark(thread)激活AQS队列里面被阻塞的一个线程(thread)。被激活的线程则使用tryReleaseShared查看当前状态变量state的值是否满足自己的需要,满足则激活该线程,然后继续向下运行,否则还是放入AQS的阻塞队列并挂起。

    public final boolean releaseShared(int arg){
    	if(tryReleaseShared(arg)){
    		doReleaseShared();
    		return true;
    	}
    	return false;
    }
    
  3. 同样需要注意的是,AQS类并没有提供可用tryAcquireSharedtryReleaseShared方法,正如AQS是锁阻塞和同步器的基础框架一样,tryAcquireShared和tryReleaseShared需要由具体的子类来实现。子类在实现tryAcquireShared和tryReleaseShared时要根据具体场景使用CAS算法尝试修改state的值,成功则返回true,否则返回false。

  4. 比如继承自AQS实现的读写锁ReentrantReadWriteLock里面的读锁在重写tryAcquireShared时,首先查看写锁是否被其它线程持有,如果是则直接返回false,否则使用CAS递增state的高16位(在ReentrantReadWriteLock中,state的高16位为读锁的次数。)在重写tryReleaseShared时,其内部使用CAS算法把当前state的值高16位减1,然后返回true,否则返回false。

以上就是自己整理的AQS的一部分知识,AQS的知识点很多慢慢整理。我是会敲代码的汤姆猫!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章