在上一章,我们学习了信号量(Semaphore)是如何请求许可证的,下面我们来看看要如何归还许可证。

可以看到当我们要归还许可证时,不论是调用release()或是release(int permits),都会调用AQS实现的releaseShared(int arg)方法。在releaseShared(int arg)方法中会先调用子类实现的tryReleaseShared(int arg)方法,这个方法会向信号量归还许可证,在归还完毕后,会调用doReleaseShared()方法尝试唤醒信号量等待队列中需要许可证的线程,这也印证了笔者之前所说的线程在归还信号量后,会尝试唤醒等待队列中等待许可证的线程。

那我们来看看信号量(Semaphore)静态内部类Sync实现的tryReleaseShared(int releases)是怎么完成归还许可证,首先会调用getState()获取信号量当前剩余的许可证,加上外部线程归还的许可证数量算出总许可证数量:current + releases,如果能用CAS的方式修改成功,则退出方法,否则一直轮询直到归还成功,这里CAS失败的原因有可能是外部也在请求和归还许可证,可能在执行完代码<1>处后和执行代码<2>处之前,信号量内部的许可证数量已经变了,所以CAS失败。归还信号量成功后就会调用doReleaseShared(),这个方法前面已经讲解过了,这里就不再赘述了。

  1. public class Semaphore implements java.io.Serializable {
  2. //...
  3. abstract static class Sync extends AbstractQueuedSynchronizer {
  4. //...
  5. protected final boolean tryReleaseShared(int releases) {
  6. for (;;) {
  7. int current = getState();//<1>
  8. int next = current + releases;
  9. if (next < current) // overflow
  10. throw new Error("Maximum permit count exceeded");
  11. if (compareAndSetState(current, next))//<2>
  12. return true;
  13. }
  14. }
  15. //...
  16. }
  17. //...
  18. public void release() {
  19. sync.releaseShared(1);
  20. }
  21. //...
  22. public void release(int permits) {
  23. if (permits < 0) throw new IllegalArgumentException();
  24. sync.releaseShared(permits);
  25. }
  26. //...
  27. }
  28.  
  29. public abstract class AbstractQueuedSynchronizer
  30. extends AbstractOwnableSynchronizer
  31. implements java.io.Serializable {
  32. //...
  33. public final boolean releaseShared(int arg) {
  34. if (tryReleaseShared(arg)) {
  35. doReleaseShared();
  36. return true;
  37. }
  38. return false;
  39. }
  40. //...
  41. protected boolean tryReleaseShared(int arg) {
  42. throw new UnsupportedOperationException();
  43. }
  44. //...
  45. }

  

下面我们再来看看tryAcquire(long timeout, TimeUnit unit)和tryAcquire(int permits, long timeout, TimeUnit unit)的实现,这两个方法会在给定的时间范围内尝试获取许可证,如果获取成功则返回true,获取失败则返回false。

这两个方法都会调用AQS实现的tryAcquireSharedNanos(int arg, long nanosTimeout),这个方法其实和先前讲得doAcquireShared(int arg)十分相似,只是多了一个超时返回的功能。

这里笔者简单过一下这个方法的实现:先在代码<1>处算出超时时间,然后封装线程对应的节点Node并将其入队,如果判断节点的前驱节点是头节点,且申请许可证成功,这里会调用setHeadAndPropagate(node, r)将头节点指向当前节点,并尝试唤醒下一个节点对应的线程。如果申请许可证失败,会在<2>处算出还剩多少的阻塞时间nanosTimeout,如果剩余阻塞时间小于等于0,代表线程获取许可证失败,这里会调用<3>处的cancelAcquire(node) 将节点从等待队列中移除,具体的移除逻辑可以看笔者写的ReentrantLock源码解析第二章。如果剩余阻塞时间大于0,则会执行shouldParkAfterFailedAcquire(p, node)将前驱节点的等待状态改为SIGNAL,在第二次循环时,如果前驱节点的状态为SIGNAL,且剩余阻塞时间大于SPIN_FOR_TIMEOUT_THRESHOLD(1000ns),则陷入阻塞,直到被中断抛出异常,或者被唤醒,检查是否能获取许可证,如果不能获取许可证且超时,则会返回false表示在超时时间内没有获取到许可证。

  1. public class Semaphore implements java.io.Serializable {
  2. //...
  3. public boolean tryAcquire(int permits, long timeout, TimeUnit unit)
  4. throws InterruptedException {
  5. if (permits < 0) throw new IllegalArgumentException();
  6. return sync.tryAcquireSharedNanos(permits, unit.toNanos(timeout));
  7. }
  8. //...
  9. public boolean tryAcquire(long timeout, TimeUnit unit)
  10. throws InterruptedException {
  11. return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
  12. }
  13. //...
  14. }
  15.  
  16. public abstract class AbstractQueuedSynchronizer
  17. extends AbstractOwnableSynchronizer
  18. implements java.io.Serializable {
  19. //...
  20. public final boolean tryAcquireSharedNanos(int arg, long nanosTimeout)
  21. throws InterruptedException {
  22. if (Thread.interrupted())
  23. throw new InterruptedException();
  24. return tryAcquireShared(arg) >= 0 ||
  25. doAcquireSharedNanos(arg, nanosTimeout);
  26. }
  27. //...
  28. private boolean doAcquireSharedNanos(int arg, long nanosTimeout)
  29. throws InterruptedException {
  30. if (nanosTimeout <= 0L)
  31. return false;
  32. final long deadline = System.nanoTime() + nanosTimeout;//<1>
  33. final Node node = addWaiter(Node.SHARED);
  34. try {
  35. for (;;) {
  36. final Node p = node.predecessor();
  37. if (p == head) {
  38. int r = tryAcquireShared(arg);
  39. if (r >= 0) {
  40. setHeadAndPropagate(node, r);
  41. p.next = null; // help GC
  42. return true;
  43. }
  44. }
  45. nanosTimeout = deadline - System.nanoTime();//<2>
  46. if (nanosTimeout <= 0L) {
  47. cancelAcquire(node);//<3>
  48. return false;
  49. }
  50. if (shouldParkAfterFailedAcquire(p, node) &&
  51. nanosTimeout > SPIN_FOR_TIMEOUT_THRESHOLD)
  52. LockSupport.parkNanos(this, nanosTimeout);
  53. if (Thread.interrupted())
  54. throw new InterruptedException();
  55. }
  56. } catch (Throwable t) {
  57. cancelAcquire(node);
  58. throw t;
  59. }
  60. }
  61. //...
  62. }

 

下面我们对照一下FairSync和NonfairSync,其实NonfairSync基本没有什么实现,都是调用其父类Sync的方法,以非公平的方式竞争许可证也是调用其父类nonfairTryAcquireShared(acquires)方法。而FairSync自身是有实现以公平的方式获取许可证,实现逻辑也非常简单。先判断信号量的等待队列是否有节点,有的话则返回获取失败,如果没有再获取当前的可用许可证数量available,扣去申请的许可证数量available - acquires,用CAS的方式把扣减完的值remaining存放进state,由于扣减的时候可能存在其他线程也在申请/归还许可证,所以available的值并非一直有效,如果在获取available后有其他线程也申请和归还许可证,那么这里的CAS很可能会失败,判断CAS失败后,又会开始新的一轮尝试获取许可证逻辑。

  1. static final class FairSync extends Sync {
  2. private static final long serialVersionUID = 2014338818796000944L;
  3.  
  4. FairSync(int permits) {
  5. super(permits);
  6. }
  7.  
  8. protected int tryAcquireShared(int acquires) {
  9. for (;;) {
  10. if (hasQueuedPredecessors())
  11. return -1;
  12. int available = getState();
  13. int remaining = available - acquires;
  14. if (remaining < 0 ||
  15. compareAndSetState(available, remaining))
  16. return remaining;
  17. }
  18. }
  19. }
  20.  
  21. static final class NonfairSync extends Sync {
  22. private static final long serialVersionUID = -2694183684443567898L;
  23.  
  24. NonfairSync(int permits) {
  25. super(permits);
  26. }
  27.  
  28. protected int tryAcquireShared(int acquires) {
  29. return nonfairTryAcquireShared(acquires);
  30. }
  31. }

    

对照完公平FairSync和非公平NonfairSync的差别后,我们来看看Sync类实现的方法,Sync类的实现其实也不算复杂,主要就下面4个方法,其中:nonfairTryAcquireShared(int acquires)和tryReleaseShared(int releases)先前已经将结果了,下面我们专注:reducePermits(int reductions)和drainPermits()。

  1. abstract static class Sync extends AbstractQueuedSynchronizer {
  2. final int nonfairTryAcquireShared(int acquires) {
  3. //...
  4. }
  5. protected final boolean tryReleaseShared(int releases) {
  6. //...
  7. }
  8. final void reducePermits(int reductions) {
  9. //...
  10. }
  11. final int drainPermits() {
  12. //...
  13. }
  14. }

  

Sync类实现的的reducePermits(int reductions)的作用是降低许可证数量,比如当双11来临时,淘宝京东可以对一些服务进行扩容和配置升级,使得原本可以承受10W并发量的服务提高到可以承受50W,这里可以在不调用acquire()的前提下,调用release()方法增加信号量的许可证,当双11的压力过去后,需要对服务进行缩容,由50W的并发量回到10W,这里可以用reducePermits(int reductions)降低许可证数量。在这个方法中会先获取当前许可证数量,减去我们要扣除的许可证数量current - reductions,并判断其结果是否溢出,如果溢出则抛出异常,没有溢出用CAS的方式设置最新的许可证数量。

  1. public class Semaphore implements java.io.Serializable {
  2. //...
  3. abstract static class Sync extends AbstractQueuedSynchronizer {
  4. //...
  5. final void reducePermits(int reductions) {
  6. for (;;) {
  7. int current = getState();
  8. int next = current - reductions;
  9. if (next > current) // underflow
  10. throw new Error("Permit count underflow");
  11. if (compareAndSetState(current, next))
  12. return;
  13. }
  14. }
  15. //...
  16. }
  17. //...
  18. protected void reducePermits(int reduction) {
  19. if (reduction < 0) throw new IllegalArgumentException();
  20. sync.reducePermits(reduction);
  21. }
  22. //...
  23. }

  

需要注意两点:

  1. 这个方法的访问权限是protected,如果要使用此方法需要用一个类去继承,并修改此方法的访问权限。
  2. 这个方法可能导致信号量的剩余许可证数量为负,比如一个信号量原先的许可证数量为10,且被借走了9个许可证,当前许可证数量为1。这时想把许可证数量从原先的10扣降到3,向reducePermits(int reduction)传入7,此时current-reductions=1-7=-6,如果CAS成功,那么信号量目前的许可证数量为-6,不过没关系,如果前面借走的9个许可证最终会归还,信号量的许可证数量最终会回到3。
  1. class MySemaphore extends Semaphore {
  2. public MySemaphore(int permits) {
  3. super(permits);
  4. }
  5.  
  6. @Override
  7. public void reducePermits(int reduction) {
  8. super.reducePermits(reduction);
  9. }
  10. }
  11.  
  12. public static void main(String[] args) {
  13. MySemaphore semaphore = new MySemaphore(8);
  14. System.out.println("初始信号量的许可证数量:" + semaphore.availablePermits());
  15. //初始化完信号量后,增加信号量的许可证数量
  16. int add = 2;
  17. semaphore.release(add);
  18. System.out.printf("增加%d个许可证后,许可证数量:%d\n", add, semaphore.availablePermits());
  19. //申请9个许可证
  20. int permits = 9;
  21. try {
  22. semaphore.acquire(permits);
  23. System.out.printf("申请%d个许可证后剩余许可证数量:%d\n", permits, semaphore.availablePermits());
  24. } catch (InterruptedException e) {
  25. e.printStackTrace();
  26. }
  27. //这里要将原先10个许可证扣除到只剩3个,所以传入7,扣除7个许可证
  28. semaphore.reducePermits(7);
  29. System.out.println("扣除7个许可证数量后,剩余许可证数量:" + semaphore.availablePermits());
  30. //归还原先出借的9个许可证
  31. semaphore.release(permits);
  32. System.out.printf("归还原先出借的%d信号量后,剩余信号量:%d\n", permits, semaphore.availablePermits());
  33. }

    

执行结果:

  1. 初始信号量的许可证数量:8
  2. 增加2个许可证后,许可证数量:10
  3. 申请9个许可证后剩余许可证数量:1
  4. 扣除7个许可证数量后,剩余许可证数量:-6
  5. 归还原先出借的9信号量后,剩余信号量:3

  

Sync类实现的drainPermits()可以一次性扣除信号量目前所有的许可证数量并返回,通过这个API,我们可以得知资源目前最大的访问限度。还是拿上一章远程服务为例,判定服务能承受的并发是5000,用于限流的semaphore信号量的最大许可证数量也是5000。假设目前信号量剩余的许可证数量为2000,即有3000个线程正在并发访问远程服务,我们可以通过drainPermits()方法获取剩余的允许访问数量2000,然后创建2000个线程访问远程服务,这个API一般用于计算量大且计算内容比较独立的场景。

  1. public class Semaphore implements java.io.Serializable {
  2. //...
  3. abstract static class Sync extends AbstractQueuedSynchronizer {
  4. //...
  5. final int drainPermits() {
  6. for (;;) {
  7. int current = getState();
  8. if (current == 0 || compareAndSetState(current, 0))
  9. return current;
  10. }
  11. }
  12. //...
  13. }
  14. //...
  15. public int drainPermits() {
  16. return sync.drainPermits();
  17. }
  18. //...
  19. }

最后,笔者介绍一个Semaphore在JDK1.6.0_17时期的BUG,便结束对Semaphore的源码解析。

当时AQS的setHeadAndPropagate(Node node, int propagate)和releaseShared(int arg) 两个方法的实现是下面这样的,这个代码可能导致队列被阻塞。

  1. private void setHeadAndPropagate(Node node, int propagate) {
  2. setHead(node);
  3. if (propagate > 0 && node.waitStatus != 0) {
  4. Node s = node.next;
  5. if (s == null || s.isShared())
  6. unparkSuccessor(node);
  7. }
  8. }
  9.  
  10. public final boolean releaseShared(int arg) {
  11. if (tryReleaseShared(arg)) {
  12. Node h = head;
  13. if (h != null && h.waitStatus != 0)
  14. unparkSuccessor(h);
  15. return true;
  16. }
  17. return false;
  18. }

  

按照上面代码的实现,会让下面的代码出现队列被阻塞的情况。t1和t2线程用于请求许可证,t3和t4线程用于归还许可证,循环10000000次只是为了增加出现阻塞的概率,现在说说什么样的场景下会出现队列被阻塞的情况。

程序开始时,信号量的许可证数量为0,所以t1和t2只能进入队列等待,t1和t2在队列中的节点对应N1和N2,节点的排序为:head->N1->N2(tail)。t3归还许可证时发现头节点不为null且头节点的等待状态为SIGNAL,于是会调用unparkSuccessor(h)方法唤醒头节点的后继节点N1对应的线程t1,在执行unparkSuccessor(h)的时候会把head的等待状态改为0。

t1被唤醒后获取到许可证,返回剩余许可证数量为0,即之后调用setHeadAndPropagate(Node node, int propagate)方法传入的propagate为0,但尚未调用。此时t4也归还了许可证,但发现head节点的等待状态为0,就不会调用unparkSuccessor(h)。

t1执行setHeadAndPropagate(Node node, int propagate),将头节点指向自身线程对应的节点N1,虽然此时信号量里有剩余的许可证,但t1原先拿到的propagate为0,所以不会执行unparkSuccessor(node)唤醒t4。

那么新版本的setHeadAndPropagate(Node node, int propagate)和releaseShared(int arg)又是如何保证有许可证被归还时唤醒队列中被阻塞的线程呢?这里其实和PROPAGATE有关,让我们按照新版的setHeadAndPropagate和releaseShared走一遍上面的流程。

t1和t2进入队列中等待,t3归还许可证发现头节点不为null,且头节点等待状态为SIGNAL,于是调用unparkSuccessor(h)方法唤醒头节点的后继节点N1对应的线程t1,在执行unparkSuccessor(h)的时候会把head的等待状态改为0。

t1被唤醒后获取到许可证,返回剩余许可证数量为0,在调用setHeadAndPropagate(Node node, int propagate)之前,t4归还了许可证,发现头节点的等待状态为0,将其改为PROPAGATE。

t1执行setHeadAndPropagate(Node node, int propagate),获取原先头节点h,并将头节点指向N1,此时虽然propagate为0,但原先头节点h的等待状态<0,可以执行doReleaseShared()唤醒后继节点N2对应的线程t2。

  1. import java.util.concurrent.Semaphore;
  2.  
  3. public class TestSemaphore {
  4.  
  5. private static Semaphore sem = new Semaphore(0);
  6.  
  7. private static class Thread1 extends Thread {
  8. @Override
  9. public void run() {
  10. sem.acquireUninterruptibly();
  11. }
  12. }
  13.  
  14. private static class Thread2 extends Thread {
  15. @Override
  16. public void run() {
  17. sem.release();
  18. }
  19. }
  20.  
  21. public static void main(String[] args) throws InterruptedException {
  22. for (int i = 0; i < 10000000; i++) {
  23. Thread t1 = new Thread1();
  24. Thread t2 = new Thread1();
  25. Thread t3 = new Thread2();
  26. Thread t4 = new Thread2();
  27. t1.start();
  28. t2.start();
  29. t3.start();
  30. t4.start();
  31. t1.join();
  32. t2.join();
  33. t3.join();
  34. t4.join();
  35. System.out.println(i);
  36. }
  37. }
  38. }

  

至此,Semaphore的源码解析就到此结束了。笔者在这里并没有全部介绍完所有Semaphore的API,例如:acquireUninterruptibly()和acquireUninterruptibly(int permits),因为这两个方法实在与之前介绍的acquire(),如果大家能理解清楚前面讲解的内容,这两个API相信对大家不在话下。

本章我们也初次见到AQS内部类Node的不同状态和使用方式,即节点除了独占(Node.EXCLUSIVE),还会有共享的状态(Node.SHARED),这里我们也首次见到等待状态为PROPAGATE的节点,代表传播的意思,通过这个状态,不但可以提升信号量整体的吞吐量,还可以避免高并发场景下节点没有被唤醒的情况。

Java并发之Semaphore源码解析(二)的更多相关文章

  1. Java并发之Semaphore源码解析(一)

    Semaphore 前情提要:在学习本章前,需要先了解笔者先前讲解过的ReentrantLock源码解析,ReentrantLock源码解析里介绍的方法有很多是本章的铺垫.下面,我们进入本章正题Sem ...

  2. Java并发之ReentrantReadWriteLock源码解析(一)

    ReentrantReadWriteLock 前情提要:在学习本章前,需要先了解笔者先前讲解过的ReentrantLock源码解析和Semaphore源码解析,这两章介绍了很多方法都是本章的铺垫.下面 ...

  3. Java并发之ReentrantReadWriteLock源码解析(二)

    先前,笔者和大家一起了解了ReentrantReadWriteLock的写锁实现,其实写锁本身实现的逻辑很少,基本上还是复用AQS内部的等待队列思想.下面,我们来看看ReentrantReadWrit ...

  4. Java并发之ThreadPoolExecutor源码解析(二)

    ThreadPoolExecutor ThreadPoolExecutor是ExecutorService的一种实现,可以用若干已经池化的线程执行被提交的任务.使用线程池可以帮助我们限定和整合程序资源 ...

  5. Java并发之ReentrantLock源码解析(二)

    在了解如何加锁时候,我们再来了解如何解锁.可重入互斥锁ReentrantLock的解锁方法unlock()并不区分是公平锁还是非公平锁,Sync类并没有实现release(int arg)方法,这里会 ...

  6. Java并发之ReentrantLock源码解析(四)

    Condition 在上一章中,我们大概了解了Condition的使用,下面我们来看看Condition再juc的实现.juc下Condition本质上是一个接口,它只定义了这个接口的使用方式,具体的 ...

  7. Java并发之ReentrantLock源码解析(三)

    ReentrantLock和BlockingQueue 首先,看到这个标题,不要怀疑自己进错文章,也不要怀疑笔者写错,哈哈.本章笔者会从BlockingQueue(阻塞队列)的角度,看看juc包下的阻 ...

  8. Java并发之ThreadPoolExecutor源码解析(三)

    Worker 先前,笔者讲解到ThreadPoolExecutor.addWorker(Runnable firstTask, boolean core),在这个方法中工作线程可能创建成功,也可能创建 ...

  9. Java并发之ReentrantLock源码解析(一)

    ReentrantLock ReentrantLock是一种可重入的互斥锁,它的行为和作用与关键字synchronized有些类似,在并发场景下可以让多个线程按照一定的顺序访问同一资源.相比synch ...

随机推荐

  1. [DB] 关系型数据库

    名词 数据库(database):保存有组织的数据的容器,是通过DBMS创建的容器 表(table):某种特定类型数据的结构化清单 元组(tuple):行,一条数据库记录,对应一个事物 属性(prop ...

  2. createrepo 建立本地yum源

    linux使用createrepo制作本地yum源   目录 linux使用createrepo制作本地yum源 安装createrepo软件包 进入本地rpm包目录 执行完后可以看到生成的repod ...

  3. SPECCPU2006 Spec2006 使用说明

    http://www.vimlinux.com/lipeng/author/penglee5.html Spec2006使用说明 五 10 十月 2014 By penglee 工具介绍 SPEC C ...

  4. Linux服务之nginx服务篇五(静态/动态文件缓存)

    一.nginx实现静态文件缓存实战 1.nginx静态文件缓存 如果要熟练使用nginx来实现文件的缓存,那下面的几个指令你必须要牢记于心 (1)指令1:proxy_cache_path 作用:设置缓 ...

  5. Java 常见转义字符

    什么是转义符 计算机某些特殊字符是无法直接用字符表示,可以通过转义符 ( \ ) 的方式表示,也就是将原字符的含义转为其他含义. 比如,如果想要输出一个单引号,你可能会想到 char letter = ...

  6. selenium多表单切换以及多窗口切换、警告窗处理

    selenium表单切换 在做UI自动化,有时候要定位的元素属性在页面上明明是唯一的.却怎么也不执行对元素的操作动作,这时候多半是iframe表单在作怪. 切入表单:iddriver.switch_t ...

  7. python3 smtplib发送邮件

    使用smtp包发送邮件还依赖email的一些方法 发送邮件主要分为三步: 1,定义邮箱参数:邮箱服务器地址,邮箱用户名,邮箱密码,邮件发送方,邮件接收方,邮件标题,邮件内容 2,配置发送内容 3,实例 ...

  8. STM32程序的启动

    普及: 不同位置启动首需要硬件上的配合:BOOT1与BOOT0 引脚电平配合,一般默认使用主闪存存储: 也就是BOOT0 = 0; 启动时将现在起始模式的初始地址映射到了0x0000 0000,内部S ...

  9. 『政善治』Postman工具 — 14、NewMan工具的使用详解

    目录 1.NewMan工具的介绍 2.NewMan的安装 (1)安装 (2)验证NewMan环境: (3)NewMan卸载命令 3.NewMan执行Postman测试集 (1)导出collection ...

  10. GO汇编-函数

    GO汇编-函数 终于到函数了!因为Go汇编语言中,可以也建议通过Go语言来定义全局变量,那么剩下的也就是函数了.只有掌握了汇编函数的基本用法,才能真正算是Go汇编语言入门.本章将简单讨论Go汇编中函数 ...