Java并发包核心框架AQS之一同步阻塞与唤醒续

本文承接未完上文。

四、AQS共享获取/释放源码分析

 在上文中对Du占方式获取和释放共享资源相关的源码进行了分析,本节接着开始对共享式获取/释放资源的源码进行分析。共享式与Du占式的最主要区别在于同一时刻Du占式只能有一个线程成功获取同步资源,而共享式在同一时刻可以有多个线程成功获取同步资源。例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。

4.1 void acquireShared(int arg)

此方法是共享模式下线程获取共享资源的顶层入口。它会通过自定义共享资源获取方法acquireShared(int)获取指定量的资源,获取成功则直接返回,获取失败则通过doAcquireShared(int)被加入同步等待队列,直到获取到资源为止,整个过程忽略中断。 

public final void acquireShared(int arg) {
        if (tryAcquireShared(arg) < 0)
            doAcquireShared(arg);
}

   这里tryAcquireShared()依然需要自定义同步器去实现。但是AQS已经把其返回值的语义定义好了:

  1. 负值,代表获取资源失败
  2. 0,代表获取资源成功,但没有剩余资源
  3. 正值,表示获取资源成功,并且还有剩余资源,其他线程还可以尝试去获取。

4.1.1 doAcquireShared(int) 

此方法用于将当前线程加入同步等待队列尾部阻塞,直到被其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。

private void doAcquireShared(int arg) {
	 //addWaiter方法在上一文Du占模式中已经分析过,就是将当前线程加入到同步等待队列的队尾,并返回当前线程所在的节点。
	//从这里可以看出,不管是共享模式还是Du占模式,都共享同一个等待队列。
	final Node node = addWaiter(Node.SHARED);
	boolean failed = true; //标记是否成功获取资源
	try {
		boolean interrupted = false; //标记在等待过程中是否被中断过
		for (;;) {   //又是一个CAS“自旋”
			final Node p = node.predecessor();//拿到前驱节点
			if (p == head) {  //如果前驱是头节点即该节点是第二节点,那么就有资格去尝试获取资源
				int r = tryAcquireShared(arg); //尝试获取资源
				if (r >= 0) {  //0表示获取成功,大于0表示获取成功还有剩余资源
					setHeadAndPropagate(node, r);  //将head指向自己,还有剩余资源或者后继节点状态小于0时,可以再唤醒之后的线程
					p.next = null; // help GC
					if (interrupted) //如果等待过程中被打断过,此时将中断补上。
						selfInterrupt();
					failed = false;
					return;
				}
			}
			//重排序(如果有必要的话),然后阻塞进入waiting状态,等待被唤醒
			if (shouldParkAfterFailedAcquire(p, node) && 
				parkAndCheckInterrupt())
				interrupted = true;
		}
	} finally {
		if (failed)
			cancelAcquire(node);
	}
}

    通过分析其源码可以发现其逻辑个Du占式获取资源的过程基本一致,唯一的区别就在于这里有一个新的方法setHeadAndPropagate()出现,该方法的作用是重新设置头节点为当前成功获取资源的线程节点,并且如果还有剩余资源或者后继节点告诉了当前节点需要被唤醒,则还要继续唤醒后面的线程,这也是所谓共享式获取资源的最直接的体现。那么它到底是如何继续唤醒后面的线程的,我们接着看起源码:

private void setHeadAndPropagate(Node node, int propagate) {
	Node h = head; 
	setHead(node); //设置当前节点为头节点,并且会把当前节点的前驱节点置为null
	//如果还有剩余资源 或者 后继节点正在等待被唤醒 或者没有后继节点 或者后继节点为共享,就尝试唤醒下一个节点
	if (propagate > 0 || h == null || h.waitStatus < 0 ||
		(h = head) == null || h.waitStatus < 0) {//这里的一堆判断条件有些保守,在有多个线程竞争获取/释放时可能导致不必要的唤醒但是不会造成任何危害。
		Node s = node.next;
		if (s == null || s.isShared())
		//如果后继是独占模式,那么即使剩下的许可大于0也不会继续往后传递唤醒操作,即使后面有结点是共享模式。
                //但是当没有后继节点(s==null)时,还是会去自旋中继续尝试将来新加入进来的不论是共享还是Du占模式的节点。
		doReleaseShared();
	}
}

    通过以上源码可见,此方法在setHead()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源等多个保守条件),还会尝试去唤醒后继结点,因为这是是共享模式!至于最终是如何唤醒后继节点的逻辑这里依然是通过调用doReleaseShared()方法实现的,那么我们继续往下看它的源码:

private void doReleaseShared() {
	for (;;) { //"自旋" 操作
		Node h = head; //现在的头节点是已经成功获取共享资源的节点
		if (h != null && h != tail) {
			int ws = h.waitStatus;
			if (ws == Node.SIGNAL) { //如果后继节点正在等待同步资源,并要求被唤醒
				if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //在唤醒后继节点之前先将自身同步状态置为0
					continue;            //如果头结点状态被改变(例如取消了等待或者等待新的条件满足),不再是SIGNAL,需要重新进行自旋,找到另外的合适的后继节点
				unparkSuccessor(h); //如果设置节点同步状态的CAS操作成功,表示后继节点确实需要被唤醒,那么就唤醒h的后继节点
			}/**
	                  为什么这里要把state状态修改为Node.PROPAGATE?
                          我觉得应该是出于在多线程并发情况下尽可能提高运行效率,达到能够快速释放资源,并及时响应新加入节点的唤醒传播。
                         **/
                        else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) 
				continue; // CAS更改节点同步状态失败,需要重新进行自旋
		}
                /**
	          为什么这里要加个h == head?
	          什么情况下这里的头节点会被改变?
	          假设当前AQS队列没有任何等待的节点,即head==tail。这时候上面的if判断不成立,执行到这里适合再次判断h==head,如果有新节点添加
	          进来,则h!=head,会重新尝试释放。另外如果在高并发下,头节点也可能会被其他线程获取到资源之后更改。所以这里的判断估计是考虑到并发竞争的情况。
                **/
		if (h == head) 
			break;
	}
}

    可能一开始有点看不明白到底是怎样唤醒后继的,我这里就简单梳理如下:

  1. 首先从doAcquireShared()方法成功获取共享资源开始,发现还有剩余资源,执行setHeadAndPropagate()方法。
  2. setHeadAndPropagate()方法重新设置当前成功获取资源的线程的节点为head节点,并发现其下一个紧接着的节点是共享模式或者已经没有后继节点时,继续执行doReleaseShared()方法。
  3. doReleaseShared()方法发现后继节点确实需要被唤醒,则执行unparkSuccessor()方法唤醒其后继节点。
  4. 被unparkSuccessor()方法唤醒的后继节点从doAcquireShared()方法中的parkAndCheckInterrupt()方法返回之后(假设没有被执行中断)继续尝试获取共享资源,如果成功获取资源,发现依然还有共享资源则重复过程1.2,3,4. 直到某一个被唤醒的线程没有成功获取到指定量的资源,或者其后继节点不是共享模式,继续向后唤醒线程的过程就终止。但在没有后继节点的时候也会继续自旋唤醒被新加入进来的节点,即使其不是共享模式而是Du占模式。

由此可见在共享式的获取同步资源的时候,由于需要在获取资源之后,如果还有剩余资源,还需要对后继节点的线程进行唤醒,所以其实现逻辑比起独占式单纯的获取资源更复杂,如果存在多线程并发的话,那情况将更加复杂。值得一提的是,在共享式获取同步资源的过程中,如果一个线程成功获取到共享资源之后,发现还有剩余的资源,于是唤醒排在它后面的节点,但是被唤醒的线程发现剩余的资源并不足以满足自己的需要(例如剩余5个资源,但是它却需要6个资源). 这时候线程就会再次进入阻塞等待状态,这时候即使剩余的资源满足排在更后面的线程的需要,也不会跳过这个线程去唤醒更后面的线程。因为AQS保证严格按照入队顺序唤醒罢了(保证公平,但降低了并发)。

4.2 boolean releaseShared(int arg)

4.1 小节讲述了共享式获取同步资源的过程,这里开始共享式释放资源的逻辑releaseShared(),此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。

public final boolean releaseShared(int arg) {
        if (tryReleaseShared(arg)) { //通过自定义共享资源释放方法尝试释放资源
            doReleaseShared();  //唤醒后继节点
            return true;
        }
        return false;
}
   有了前面章节的理解,对这个方法应该就比较简单了,首先还是会执行被覆写过的共享资源释放方法tryReleaseShared(),该方法返回true表示成功释放掉指定量的资源,释放成功之后,又调用了doReleaseShared()方法,该方法在上面的4.1.1中已经详细的分析过了,其作用就是将后继节点的线程从doAcquireShared()方法中的阻塞点唤醒,继续尝试获取资源,如果成功则又执行setHeadAndPropagate()方法,若还有剩余则继续执行doReleaseShared(),这样传播式的向后唤醒线程的过程又和4.1.1中的过程一致了。值得一提的是,在doReleaseShared()方法以及unparkSuccessor()方法中并没有对节点模式进行判断,所以只要是在等待的线程都会被唤醒去尝试获取资源,不论它是共享模式还是Du占模式。   由于ASQ这种基于模板方式的设计实现,已经对大量细节进行了实现,当我们对其内部原理以及需要被不同类型的自定义同步器实现的模板方法有了了解之后,我们可以很方面的实现我们自己的同步器。  该例以银行柜台业务办理窗口为例,假设总共有3个窗口,一共来了十个人排队等候,那么每三个人就可以同时办理业务(共享银行窗口资源),这是典型的共享式同步模式。实现代码如下: 1. 首先定义一个外部接口BankServiceWindows
public interface BankServiceWindows {

	public void handle();
	
	public void release();
}
2.  通过私有静态内部类实现同步组件的接口实例BankServiceWindowsImpl
public class BankServiceWindowsImpl implements BankServiceWindows{
	
	private Sync sync;
	
	public BankServiceWindowsImpl(int count){
		sync = new Sync(count);
	}

	@Override
	public void handle() {
		sync.acquireShared(1);
	}

	@Override
	public void release() {
		sync.releaseShared(1);
	}
	
	private static class Sync extends AbstractQueuedSynchronizer{
		
		private  Sync(int count){
			setState(count);
		}

		@Override
		protected int tryAcquireShared(int arg) {
			for (;;) {
				int current = getState();
				int newCount = current - 1;
				if (newCount < 0 || compareAndSetState(current, newCount)) {
					return newCount;
				}
			}
		}

		@Override
		protected boolean tryReleaseShared(int arg) {
			for (;;) {
				int current = getState();
				int newCount = current + 1;
				if (compareAndSetState(current, newCount)) {
					return true;
				}
			}
		}
		
	}

}
3.  测试同步组件类BankServiceWindowsTest
public class BankServiceWindowsTest {
	
	static class handleThread extends Thread{
		
		private BankServiceWindows windows;
		
		public handleThread(BankServiceWindows windows, String name) {
			super();
			this.windows = windows;
			setName(name);
		}
		
		@Override
        public void run() {
			System.out.println(Thread.currentThread().getName() +" 开始等候");
			windows.handle();
			try {
				System.out.println(Thread.currentThread().getName() +" 开始办理");
				Thread.currentThread().sleep(5000);
				System.out.println(Thread.currentThread().getName() +" 办理结束");
			} catch(Exception e){
				e.printStackTrace();
			}finally{
				windows.release();
			}
		}
	}

	public static void main(String[] args) {
		BankServiceWindows windows =new BankServiceWindowsImpl(3);
		for (int i = 0; i < 10 ; i++) {
			new handleThread(windows, "线程"+i).start();
		}
	}
}
  通过运行上面的测试代码,可以看到,我们的共享锁可以支持3个线程同时运行。这就是共享式同步组件的意义。      

猜你喜欢

转载自pzh9527.iteye.com/blog/2419457