Java并发包核心框架AQS之一同步阻塞与唤醒续
四、AQS共享获取/释放源码分析
在上文中对Du占方式获取和释放共享资源相关的源码进行了分析,本节接着开始对共享式获取/释放资源的源码进行分析。共享式与Du占式的最主要区别在于同一时刻Du占式只能有一个线程成功获取同步资源,而共享式在同一时刻可以有多个线程成功获取同步资源。例如读操作可以有多个线程同时进行,而写操作同一时刻只能有一个线程进行写操作,其他操作都会被阻塞。
4.1 void acquireShared(int arg)
此方法是共享模式下线程获取共享资源的顶层入口。它会通过自定义共享资源获取方法acquireShared(int)获取指定量的资源,获取成功则直接返回,获取失败则通过doAcquireShared(int)被加入同步等待队列,直到获取到资源为止,整个过程忽略中断。
1 public final void acquireShared(int arg) { 2 if (tryAcquireShared(arg) < 0) 3 doAcquireShared(arg); 4 }
这里tryAcquireShared()依然需要自定义同步器去实现。但是AQS已经把其返回值的语义定义好了:
- 负值,代表获取资源失败
- 0,代表获取资源成功,但没有剩余资源
- 正值,表示获取资源成功,并且还有剩余资源,其他线程还可以尝试去获取。
4.1.1 doAcquireShared(int)
此方法用于将当前线程加入同步等待队列尾部阻塞,直到被其他线程释放资源唤醒自己,自己成功拿到相应量的资源后才返回。
1 private void doAcquireShared(int arg) { 2 //addWaiter方法在上一文Du占模式中已经分析过,就是将当前线程加入到同步等待队列的队尾,并返回当前线程所在的节点。 3 //从这里可以看出,不管是共享模式还是Du占模式,都共享同一个等待队列。 4 final Node node = addWaiter(Node.SHARED); 5 boolean failed = true; //标记是否成功获取资源 6 try { 7 boolean interrupted = false; //标记在等待过程中是否被中断过 8 for (;;) { //又是一个CAS“自旋” 9 final Node p = node.predecessor();//拿到前驱节点 10 if (p == head) { //如果前驱是头节点即该节点是第二节点,那么就有资格去尝试获取资源 11 int r = tryAcquireShared(arg); //尝试获取资源 12 if (r >= 0) { //0表示获取成功,大于0表示获取成功还有剩余资源 13 setHeadAndPropagate(node, r); //将head指向自己,还有剩余资源或者后继节点状态小于0时,可以再唤醒之后的线程 14 p.next = null; // help GC 15 if (interrupted) //如果等待过程中被打断过,此时将中断补上。 16 selfInterrupt(); 17 failed = false; 18 return; 19 } 20 } 21 //重排序(如果有必要的话),然后阻塞进入waiting状态,等待被唤醒 22 if (shouldParkAfterFailedAcquire(p, node) && 23 parkAndCheckInterrupt()) 24 interrupted = true; 25 } 26 } finally { 27 if (failed) 28 cancelAcquire(node); 29 } 30 }
通过分析其源码可以发现其逻辑个Du占式获取资源的过程基本一致,唯一的区别就在于这里有一个新的方法setHeadAndPropagate()出现,该方法的作用是重新设置头节点为当前成功获取资源的线程节点,并且如果还有剩余资源或者后继节点告诉了当前节点需要被唤醒,则还要继续唤醒后面的线程,这也是所谓共享式获取资源的最直接的体现。那么它到底是如何继续唤醒后面的线程的,我们接着看起源码:
1 private void setHeadAndPropagate(Node node, int propagate) { 2 Node h = head; 3 setHead(node); //设置当前节点为头节点,并且会把当前节点的前驱节点置为null 4 //如果还有剩余资源 或者 后继节点正在等待被唤醒 或者没有后继节点 或者后继节点为共享,就尝试唤醒下一个节点 5 if (propagate > 0 || h == null || h.waitStatus < 0 || 6 (h = head) == null || h.waitStatus < 0) {//这里的一堆判断条件有些保守,在有多个线程竞争获取/释放时可能导致不必要的唤醒但是不会造成任何危害。 7 Node s = node.next; 8 if (s == null || s.isShared()) 9 //如果后继是独占模式,那么即使剩下的许可大于0也不会继续往后传递唤醒操作,即使后面有结点是共享模式。 10 //但是当没有后继节点(s==null)时,还是会去自旋中继续尝试将来新加入进来的不论是共享还是Du占模式的节点。 11 doReleaseShared(); 12 } 13 }
通过以上源码可见,此方法在setHead()的基础上多了一步,就是自己苏醒的同时,如果条件符合(比如还有剩余资源等多个保守条件),还会尝试去唤醒后继结点,因为这是是共享模式!至于最终是如何唤醒后继节点的逻辑这里依然是通过调用doReleaseShared()方法实现的,那么我们继续往下看它的源码:
1 private void doReleaseShared() { 2 for (;;) { //"自旋" 操作 3 Node h = head; //现在的头节点是已经成功获取共享资源的节点 4 if (h != null && h != tail) { 5 int ws = h.waitStatus; 6 if (ws == Node.SIGNAL) { //如果后继节点正在等待同步资源,并要求被唤醒 7 if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0)) //在唤醒后继节点之前先将自身同步状态置为0 8 continue; //如果头结点状态被改变(例如取消了等待或者等待新的条件满足),不再是SIGNAL,需要重新进行自旋,找到另外的合适的后继节点 9 unparkSuccessor(h); //如果设置节点同步状态的CAS操作成功,表示后继节点确实需要被唤醒,那么就唤醒h的后继节点 10 }/** 11 为什么这里要把state状态修改为Node.PROPAGATE? 12 我觉得应该是出于在多线程并发情况下尽可能提高运行效率,达到能够快速释放资源,并及时响应新加入节点的唤醒传播。 13 **/ 14 else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE)) 15 continue; // CAS更改节点同步状态失败,需要重新进行自旋 16 } 17 /** 18 为什么这里要加个h == head? 19 什么情况下这里的头节点会被改变? 20 假设当前AQS队列没有任何等待的节点,即head==tail。这时候上面的if判断不成立,执行到这里适合再次判断h==head,如果有新节点添加 21 进来,则h!=head,会重新尝试释放。另外如果在高并发下,头节点也可能会被其他线程获取到资源之后更改。所以这里的判断估计是考虑到并发竞争的情况。 22 **/ 23 if (h == head) 24 break; 25 } 26 }
可能一开始有点看不明白到底是怎样唤醒后继的,我这里就简单梳理如下:
- 首先从doAcquireShared()方法成功获取共享资源开始,发现还有剩余资源,执行setHeadAndPropagate()方法。
- setHeadAndPropagate()方法重新设置当前成功获取资源的线程的节点为head节点,并发现其下一个紧接着的节点是共享模式或者已经没有后继节点时,继续执行doReleaseShared()方法。
- doReleaseShared()方法发现后继节点确实需要被唤醒,则执行unparkSuccessor()方法唤醒其后继节点。
- 被unparkSuccessor()方法唤醒的后继节点从doAcquireShared()方法中的parkAndCheckInterrupt()方法返回之后(假设没有被执行中断)继续尝试获取共享资源,如果成功获取资源,发现依然还有共享资源则重复过程1.2,3,4. 直到某一个被唤醒的线程没有成功获取到指定量的资源,或者其后继节点不是共享模式,继续向后唤醒线程的过程就终止。但在没有后继节点的时候也会继续自旋唤醒被新加入进来的节点,即使其不是共享模式而是Du占模式。
由此可见在共享式的获取同步资源的时候,由于需要在获取资源之后,如果还有剩余资源,还需要对后继节点的线程进行唤醒,所以其实现逻辑比起独占式单纯的获取资源更复杂,如果存在多线程并发的话,那情况将更加复杂。值得一提的是,在共享式获取同步资源的过程中,如果一个线程成功获取到共享资源之后,发现还有剩余的资源,于是唤醒排在它后面的节点,但是被唤醒的线程发现剩余的资源并不足以满足自己的需要(例如剩余5个资源,但是它却需要6个资源). 这时候线程就会再次进入阻塞等待状态,这时候即使剩余的资源满足排在更后面的线程的需要,也不会跳过这个线程去唤醒更后面的线程。因为AQS保证严格按照入队顺序唤醒罢了(保证公平,但降低了并发)。
4.2 boolean releaseShared(int arg)
4.1 小节讲述了共享式获取同步资源的过程,这里开始共享式释放资源的逻辑releaseShared(),此方法是共享模式下线程释放共享资源的顶层入口。它会释放指定量的资源,如果成功释放且允许唤醒等待线程,它会唤醒等待队列里的其他线程来获取资源。
1 public final boolean releaseShared(int arg) { 2 if (tryReleaseShared(arg)) { //通过自定义共享资源释放方法尝试释放资源 3 doReleaseShared(); //唤醒后继节点 4 return true; 5 } 6 return false; 7 }
自此,共享式获取/释放同步资源的基础方法的源码就分析完毕,和独占式类似的,支持响应中断和超时机制的acquireSharedInterruptibly(int)和tryAcquireSharedNanos(int arg, long nanosTimeout)与独占式相对应的方法原理一致,这里就不再详解了。
AQS中还提供了其他一些重要或有用的方法,例如查看所有正在同步队列中等待的线程方法getQueuedThreads(),getExclusiveQueuedThreads(),getSharedQueuedThreads()等,这里也就不再做全面的介绍了。
五、自定义AQS同步器
通过前面的分析学习,我们已经对AQS的源码进行详细的分析,对其原理也有了了解即:AQS同步阻塞唤醒机制的主要思想是对共享资源state的获取与释放,以及对同步等待队列的维护。AQS本身已经对同步队列的维护进行了完整的支持,而对共享资源的获取与释放则需要被不同的自定义同步器来实现,应该实现的模板方法已经被AQS设计规范好了,其自定义同步器实现时主要实现以下几种方法:
- tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。如果允许重入,则state允许累加。
- tryRelease(int):独占方式。尝试释放资源,已经彻底释放掉资源(例如重入之后,要释放多次,直到state为0)则返回true,否则返回false。
- tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。
- tryReleaseShared(int):共享方式。尝试释放资源,如果成功释放资源则返回true,否则返回false。
- isHeldExclusively():该线程是否正在独占资源。一般只有用到condition才需要去实现它。
编写自定义同步组件需要注意的地方
1. 使用新的接口和实现包装同步组件
2. 同步组件推荐定义为静态内部类
自定义独占式同步组件实例
1 //不可重入的独占锁接口 2 public interface Mutex { 3 //获取锁 4 public void lock(); 5 //释放锁 6 public void unlock(); 7 }
2. 通过私有静态内部类实现同步组件的接口实例MutexImpl
1 //实现 2 public class MutexImpl implements Mutex{ 3 // 仅需要将操作代理到Sync上即可 4 private Sync sync=new Sync(); 5 @Override //lock<-->acquire。两者语义一样:获取资源,即便等待,直到成功才返回。 6 public void lock() { 7 sync.acquire(1); 8 } 9 10 @Override //unlock<-->release。两者语义一样:释放资源。 11 public void unlock() { 12 sync.release(1); 13 } 14 15 //私有静态内部类,独占式同步组件实现 16 private static class Sync extends AbstractQueuedSynchronizer{ 17 18 @Override 19 protected boolean tryAcquire(int arg) { 20 return compareAndSetState(0,1); //state为0才设置为1,表明不可重入! 21 } 22 23 @Override 24 protected boolean tryRelease(int arg) { 25 return compareAndSetState(1,0);//重新将state从1设置为0 26 } 27 } 28 }
3. 测试同步组件类MutexMain
1 public class MutexMain { 2 3 static class MutexThread extends Thread{ 4 private Mutex mutex; 5 6 public MutexThread(String name,Mutex mutex) { 7 this.mutex = mutex; 8 this.setName(name); 9 } 10 11 @Override 12 public void run() { 13 System.out.println(Thread.currentThread().getName()+"启动.."); 14 mutex.lock(); 15 System.out.println(Thread.currentThread().getName()+"获取锁成功.."); 16 try { 17 System.out.println(Thread.currentThread().getName()+"开始执行,当前时间:"+new Date().toLocaleString()); 18 Thread.currentThread().sleep(1000);//假设线程执行需要1秒钟 19 System.out.println(Thread.currentThread().getName()+"结束执行,当前时间:"+new Date().toLocaleString()); 20 } catch (InterruptedException e) { 21 e.printStackTrace(); 22 }finally { 23 System.out.println(Thread.currentThread().getName()+"释放锁.."); 24 mutex.unlock(); 25 } 26 } 27 } 28 29 public static void main(String[] args) { 30 Mutex mutex=new MutexImpl(); 31 for (int i = 0; i <5 ; i++) { 32 new MutexThread("线程"+i,mutex).start(); 33 } 34 } 35 36 }
自定义共享式同步组件实例
1 public interface BankServiceWindows { 2 3 public void handle(); 4 5 public void release(); 6 }
2. 通过私有静态内部类实现同步组件的接口实例BankServiceWindowsImpl
1 public class BankServiceWindowsImpl implements BankServiceWindows{ 2 3 private Sync sync; 4 5 public BankServiceWindowsImpl(int count){ 6 sync = new Sync(count); 7 } 8 9 @Override 10 public void handle() { 11 sync.acquireShared(1); 12 } 13 14 @Override 15 public void release() { 16 sync.releaseShared(1); 17 } 18 19 private static class Sync extends AbstractQueuedSynchronizer{ 20 21 private Sync(int count){ 22 setState(count); 23 } 24 25 @Override 26 protected int tryAcquireShared(int arg) { 27 for (;;) { 28 int current = getState(); 29 int newCount = current - 1; 30 if (newCount < 0 || compareAndSetState(current, newCount)) { 31 return newCount; 32 } 33 } 34 } 35 36 @Override 37 protected boolean tryReleaseShared(int arg) { 38 for (;;) { 39 int current = getState(); 40 int newCount = current + 1; 41 if (compareAndSetState(current, newCount)) { 42 return true; 43 } 44 } 45 } 46 47 } 48 49 }
3. 测试同步组件类BankServiceWindowsTest
1 public class BankServiceWindowsTest { 2 3 static class handleThread extends Thread{ 4 5 private BankServiceWindows windows; 6 7 public handleThread(BankServiceWindows windows, String name) { 8 super(); 9 this.windows = windows; 10 setName(name); 11 } 12 13 @Override 14 public void run() { 15 System.out.println(Thread.currentThread().getName() +" 开始等候"); 16 windows.handle(); 17 try { 18 System.out.println(Thread.currentThread().getName() +" 开始办理"); 19 Thread.currentThread().sleep(5000); 20 System.out.println(Thread.currentThread().getName() +" 办理结束"); 21 } catch(Exception e){ 22 e.printStackTrace(); 23 }finally{ 24 windows.release(); 25 } 26 } 27 } 28 29 public static void main(String[] args) { 30 BankServiceWindows windows =new BankServiceWindowsImpl(3); 31 for (int i = 0; i < 10 ; i++) { 32 new handleThread(windows, "线程"+i).start(); 33 } 34 } 35 }
通过运行上面的测试代码,可以看到,我们的共享锁可以支持3个线程同时运行。这就是共享式同步组件的意义。

浙公网安备 33010602011771号