AQS(AbstractQueuedSynchronizer)源代码分析(二)——非共享的aquired和release方法

      经过上一篇文章AQS(AbstractQueuedSynchronizer)源代码分析(一)——实现逻辑概览,可以大致的了解AQS的一个运行机制。本次将会具体看一下AQS的代码实现。首先说明一下,AQS的aquired和release方法大致分为两类:

     1、非共享的,如:aquire,tryaquired,release, tryrelease等;

     2、共享的,如:acquireShared,tryAcquireShared,releaseShared,tryReleaseShared等。

本次我们将重点看非共享的实现,至于共享的实现,我们在下一篇文章再讨论。


AQS实现逻辑概览图:


关于这个图的解释可以去上一篇文章(实现逻辑概览)中了解,下文统称为“概览图”。

一、acquire方法

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

上面是acquire方法的代码实现。方法说明:

        方法体中使用了一个快速与(&&)。首先执行tryAquire(arg)方法,这个方法在AQS中默认是抛出异常的,所以其具体的执行是由AQS的子类实现的。因此我们需要找一个子类看一下tryAquire(arg)方法的实现。我们这里采用的类是:ReentrantLock----->Sync(ReentrantLock的内部抽象类)----->NonfairSync(Sync的子类)。NonfairSync的tryAquire(arg)方法的实现如下:

//ReentrantLock.NonfairSync类
protected final boolean tryAcquire(int acquires) {
	return nonfairTryAcquire(acquires);
}


final boolean nonfairTryAcquire(int acquires) {
	final Thread current = Thread.currentThread();
	int c = getState(); //获取当前的sate值,getSate方法是AQS中的方法
	
	if (c == 0) { //判断sate值是否为0,(在这里0就表示初始状态,在其他类的实现中,sate的初始值可能是别的值,不要认为初始值一定是0);
	
		if (compareAndSetState(0, acquires)) { //采用CAS原子操作修改sate的值,
		
			//如果修改成功,则将AQS的执行者设置为currentThread;这里的执行者其实就是获得执行权的线程
			//这个属性主要是用来确定当前正在执行的线程的
			setExclusiveOwnerThread(current); 
			return true;//如果修改成功,返回true
		}
	}
	else if (current == getExclusiveOwnerThread()) {  
	
	        //如果sate状态不为初始值0,说明已经有线程正在执行。
		//此时就需要做一个区分,正在执行的那个线程与当前进入的线程是否是同一个线程
		//如果是同一个线程,比如:递归调用或者重入等等,则任然可以继续执行,但是应该记录重入的次数。
		//如果不是同一个线程,那么就应该挂起这个线程,返回false
		
		int nextc = c + acquires; //这里就是用来记录重入的次数的,每次重入都需要修改sate
		if (nextc < 0) // overflow
			throw new Error("Maximum lock count exceeded");
		setState(nextc);
		return true;
	}
	return false;//如果修改失败,返回false
}

       上面的代码说明,已经解释了tryAquire(arg)方法的实现逻辑(仅仅是其中一种)。这里再回到AQS中的acqured方法。在acquired方法中:

        如果tryAquire(arg)成功,返回true; 则acquire方法就立即返回了。概览图标示:尝试修改sate---->(是)------>执行被锁定的内容。

       如果tryAquire(arg)失败,返回false; 则快速与(&&)就会执行acquireQueued方法。概览图标示:尝试修改sate---->(否)------>增加node节点,挂起线程。

      根据上面的描述,在acquireQueued方法中需要完成两件事:第一,增加node节点;第二,挂起线程,使线程处于WAITING状态,等待被唤醒。acquireQueued方法的代码如下:

//在执行acquireQueued之前,先执行了addWaiter(Node.EXCLUSIVE)方法获得了第一个参数。

acquireQueued(addWaiter(Node.EXCLUSIVE), arg)

 先看一下addWaiter方法的实现:

/**
  * 在node队列的队尾追加一个新节点
  */
private Node addWaiter(Node mode) {  
	Node node = new Node(Thread.currentThread(), mode); //使用Node.EXCLUSIVE和currentThread创建一个节点
	// Try the fast path of enq; backup to full enq on failure
	
	//node队列的第一个节点被称为:head;  最后一个节点被称为;tail
	Node pred = tail; 
	
	if (pred != null) { 
		node.prev = pred; //node.prex = tail, tail.next = node; 调换引用,切换队列的末尾节点。
		
		//因为是多线程,所以会有多个线程同时追加node,所以用CAS修改尾节点,如果成功,则返回
		if (compareAndSetTail(pred, node)) { 
			pred.next = node;
			return node;
		}
	}
	enq(node);//CAS修改tail可能会失败,所以需要在enq方法中处理
	return node;
}

private Node enq(final Node node) {
	for (;;) { //死循环,保证一定可以追加成功
		Node t = tail;
		
		//当AQS第一次被访问时,还没有tail和head节点,所以是null;  此时就需要新增一个空节点来保证队列的初始化
		if (t == null) { 
			if (compareAndSetHead(new Node())) //CAS修改,如果失败,死循环会保证继续尝试
				tail = head;
		} else {
			node.prev = t;
			if (compareAndSetTail(t, node)) { //CAS修改,如果失败,死循环会保证继续尝试
				t.next = node;
				return t;
			}
		}
	}
}

上面对addWaiter方法的分析,已经完成了第一个功能:node节点的增加;接下来看一下如何挂起线程。这部分不容易理解,先用图说明一下:


如上图所示,head表示正在执行的线程。当head执行完成之后,会调用release方法唤起head的nextNode节点线程。线程被唤醒,之后就会再次去tryqcquired;如果tryqcquired成功,则就会将head替换成node节点,然后执行node。接着看一下acquireQueued的代码:

/**
  * 追加节点之后,循环尝试修改sate
  */
final boolean acquireQueued(final Node node, int arg) {
	boolean failed = true;
	try {
		boolean interrupted = false;
		for (;;) { //死循环保证这个过程将会一直持续,直到return语句执行
		
			final Node p = node.predecessor(); //获得node的prev(前一个节点)
			
			 //如果前一个节点是head,则进行tryacquired
			 //如果正在执行的head节点没有调用release方法,这里的tryacquired将永远是false,不可能成功;所以就会执行下面的挂起方法
			if (p == head && tryAcquire(arg)) { 
			
				setHead(node);  //替换head节点,准备执行
				p.next = null; // help GC
				failed = false;
				return interrupted;
			}
			
			// shouldParkAfterFailedAcquire这个方法是用来判断线程是否可以挂起的,下文单独说明
			//因为没有tryacquired成功,所以会执行parkAndCheckInterrupt挂起线程,通过park方法
			//当再次被唤醒时,会继续上面的操作
			if (shouldParkAfterFailedAcquire(p, node) &&  
				parkAndCheckInterrupt()) 
				interrupted = true;		
		}
	} finally {
		if (failed)
			cancelAcquire(node);
	}
}

 private final boolean parkAndCheckInterrupt() {
	LockSupport.park(this);  //挂起线程,类似object.wait方法;挂起之后将会等待被唤醒,以便于执行上面的tryAcquire
	return Thread.interrupted();
}

接下来是shouldParkAfterFailedAcquire是用来判断线程是否能够挂起的。这里涉及了node节点的几个状态:

1、SIGNAL,这个值小于0 , 表明该node的存在nextNode后续节点,相当于告诉node:有nextNode需要处理,你处理完之后,去唤醒nextNode。

2、CANCELLED,这个值是大于0的,表明这个节点的线程已经取消了,不需要再执行

3、CONDITION,表明node在执行被锁定内容之前,需要等待某个条件。这个会在后面的文章讨论。这里不做分析

shouldParkAfterFailedAcquire的代码:

private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
	int ws = pred.waitStatus; //获取node的状态
	if (ws == Node.SIGNAL)
		return true; //已经是SIGNAL,可以挂起,返回true
		
	if (ws > 0) { //节点已经被取消,需要从队列当中移除
		do {
			node.prev = pred = pred.prev; //调换prev引用,移除节点
		} while (pred.waitStatus > 0); 
		pred.next = node;
	} else {
		
		//如果没有取消,也不是SIGNAL状态,则尝试修改为SIGNAL,
        //node是prevNode的nextNode,说明prevNode存在后续节点,也就表明prevNode需要是SIGNAL状态,在prevNode执行完之后,
		//需要去唤醒nextNode。因此,如果prevNode不是SIGNAL状态的话,需要修改为SIGNAL状态
		compareAndSetWaitStatus(pred, ws, Node.SIGNAL); 
	}
	return false; //返回false,线程不能挂起
}
二、release方法


      上面已经介绍完acquire方法,接下来看一下release方法:

public final boolean release(int arg) {
	if (tryRelease(arg)) {  //尝试还原sate,如果成功则执行if内语句
		Node h = head;
		if (h != null && h.waitStatus != 0)
			unparkSuccessor(h);
		return true;
	}
	return false;
}

private void unparkSuccessor(Node node) {
 
	int ws = node.waitStatus; //这里的node就是head节点
	if (ws < 0) //head节点是正在执行的node,其状态不可能是小于0,也就是不能是SIGNAL
		compareAndSetWaitStatus(node, ws, 0);

	Node s = node.next; //取出head节点的nextNode节点
	
	if (s == null || s.waitStatus > 0) { //判断nextNode是否为Null,或者是否已经取消
		s = null;
		for (Node t = tail; t != null && t != node; t = t.prev) //如果已经取消,则循环取出最近的一个状态为SIGNAL的节点
			if (t.waitStatus <= 0) //waitStatus<0,node是SIGNAL状态。新建的,未经过挂起和其他处理的节点,状态是0。
				s = t;
	}
	if (s != null)
		LockSupport.unpark(s.thread); //通过unpark唤起node中的线程
	}
}

上文采用ReentrantLock.NonfairSync说明了tryacquired方法。这里也采用ReentrantLock.NonfairSync类中的tryRelease方法说明一下tryRelease的实现。不过ReentrantLock.NonfairSync的tryRelease方法在其父类Sync中,代码如下:

类ReentrantLock.Sync
protected final boolean tryRelease(int releases) {
	int c = getState() - releases; //在tryacquired方法中时增加sate,  而这里是减sate
	if (Thread.currentThread() != getExclusiveOwnerThread())
		throw new IllegalMonitorStateException();
	boolean free = false;
	if (c == 0) {
		free = true;
		setExclusiveOwnerThread(null); //释放AQS的执行权
	}
	
	//修改sate状态值,这里没有CAS操作,
	//因为上面的acquire方法已经挂起了所有后续线程,所以当前修改sate的线程只有一个,不会有并发修改问题
	setState(c); 
	return free;
}

    总结:AQS总的来说就是一个操作node队列和控制sate状态的过程,以此来实现多个线程的同步。AQS优秀的地方在于,它将所有对队列的操作封装到了自身,向开发者隐藏了队列操作的复杂性。同时AQS又将过sate的修改和控制暴露给了子类,让子类自定义个这个sate的控制。这样做很好的封装了复杂性,同时又保证了足够的扩展性。





  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值