JUC 中 Semaphore 的使用与原理分析,Semaphore 也是 Java 中的一个同步器,与 CountDownLatch 和 CycleBarrier 不同在于它内部的计数器是递增的,那么,Semaphore 的内部实现是怎样的呢?

  Semaphore 信号量也是Java 中一个同步容器,与CountDownLatch 和 CyclicBarrier 不同之处在于它内部的计数器是递增的。为了能够一览Semaphore的内部结构,我们首先要看一下Semaphore的类图,类图,如下所示:

如上类图可以知道Semaphoren内部还是使用AQS来实现的,Sync只是对AQS的一个修饰,并且Sync有两个实现类,分别代表获取信号量的时候是否采取公平策略。创建Semaphore的时候会有一个变量标示是否使用公平策略,源码如下:

    public Semaphore(int permits) {
sync = new NonfairSync(permits);
} public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new
NonfairSync(permits);
} Sync(int permits) {
setState(permits);
}

如上面代码所示,Semaphore默认使用的是非公平策略,如果你需要公平策略,则可以使用带两个参数的构造函数来构造Semaphore对象,另外和CountDownLatch一样,构造函数里面传递的初始化信号量个数 permits 被赋值给了AQS 的state状态变量,也就是说这里AQS的state值表示当前持有的信号量个数。

接下来我们主要看看Semaphore实现的主要方法的源码,如下:

  1.void acquire() 当前线程调用该方法的时候,目的是希望获取一个信号量资源,如果当前信号量计数个数大于 0 ,并且当前线程获取到了一个信号量则该方法直接返回,当前信号量的计数会减少 1 。否则会被放入AQS的阻塞队列,当前线程被挂起,直到其他线程调用了release方法释放了信号量,并且当前线程通过竞争获取到了改信号量。当前线程被其他线程调用了 interrupte()方法中断后,当前线程会抛出 InterruptedException异常返回。源码如下:

   public void acquire() throws InterruptedException {
//传递参数为1,说明要获取1个信号量资源
sync.acquireSharedInterruptibly();
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException { //(1)如果线程被中断,则抛出中断异常
if (Thread.interrupted())
throw new InterruptedException(); //(2)否者调用sync子类方法尝试获取,这里根据构造函数确定使用公平策略
if (tryAcquireShared(arg) < )
//如果获取失败则放入阻塞队列,然后再次尝试如果失败则调用park方法挂起当前线程
doAcquireSharedInterruptibly(arg);
}

如上代码可知,acquire()内部调用了sync的acquireSharedInterruptibly  方法,后者是对中断响应的(如果当前线程被中断,则抛出中断异常),尝试获取信号量资源的AQS的方法tryAcquireShared 是由 sync 的子类实现,所以这里就要分公平性了,这里先讨论非公平策略 NonfairSync 类的 tryAcquireShared 方法,源码如下:

protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
} final int nonfairTryAcquireShared(int acquires) {
for (;;) {
//获取当前信号量值
int available = getState();
//计算当前剩余值
int remaining = available - acquires;
//如果当前剩余小于0或者CAS设置成功则返回
if (remaining < ||
compareAndSetState(available, remaining))
return remaining;
}
}

如上代码,先计算当前信号量值(available)减去需要获取的值(acquires) 得到剩余的信号量个数(remaining),如果剩余值小于 0 说明当前信号量个数满足不了需求,则直接返回负数,然后当前线程会被放入AQS的阻塞队列,当前线程被挂起。如果剩余值大于 0 则使用CAS操作设置当前信号量值为剩余值,然后返回剩余值。另外可以知道NonFairSync是非公平性获取的,是说先调用aquire方法获取信号量的线程不一定比后来者先获取锁。

接下来我们要看看公平性的FairSync 类是如何保证公平性的,源码如下:

 protected int tryAcquireShared(int acquires) {
for (;;) {
if (hasQueuedPredecessors())
return -;
int available = getState();
int remaining = available - acquires;
if (remaining < || compareAndSetState(available, remaining))
return remaining;
}
}

可以知道公平性还是靠 hasQueuedPredecessors 这个方法来做的,以前的随笔已经讲过公平性是看当前线程节点是否有前驱节点也在等待获取该资源,如果是则自己放弃获取的权力,然后当前线程会被放入AQS阻塞队列,否则就去获取。hasQueuedPredecessors源码如下:

public final boolean hasQueuedPredecessors() {
Node t = tail;
Node h = head;
Node s;
return h != t && ((s = h.next) == null || s.thread != Thread.currentThread());
}

如上面代码所示,如果当前线程节点有前驱节点则返回true,否则如果当前AQS队列为空 或者 当前线程节点是AQS的第一个节点则返回 false ,其中,如果 h == t 则说明当前队列为空则直接返回 false,如果 h !=t 并且 s == null 说明有一个元素将要作为AQS的第一个节点入队列(回顾下 enq 函数第一个元素入队列是两步操作,首先创建一个哨兵头节点,然后第一个元素插入到哨兵节点后面),那么返回 true,如果  h !=t 并且 s != null 并且  s.thread != Thread.currentThread() 则说明队列里面的第一个元素不是当前线程则返回 true。

  2.void acquire(int permits) 该方法与 acquire() 不同在与后者只需要获取一个信号量值,而前者则获取指定 permits 个,源码如下:

public void acquire(int permits) throws InterruptedException {
if (permits < )
       throw new IllegalArgumentException();
sync.acquireSharedInterruptibly(permits);
}

  3.void acquireUninterruptibly() 该方法与 acquire() 类似,不同之处在于该方法对中断不响应,也就是当当前线程调用了 acquireUninterruptibly 获取资源过程中(包含被阻塞后)其它线程调用了当前线程的 interrupt()方法设置了当前线程的中断标志当前线程并不会抛出 InterruptedException 异常而返回。源码如下:

public void acquireUninterruptibly() {
sync.acquireShared();
}

  4.void acquireUninterruptibly(int permits) 该方法与 acquire(int permits) 不同在于该方法对中断不响应。源码如如下:

 public void acquireUninterruptibly(int permits) {
if (permits < ) throw new IllegalArgumentException();
sync.acquireShared(permits);
}

  5.void release() 该方法作用是把当前 semaphore对象的信号量值增加 1 ,如果当前有线程因为调用 acquire 方法被阻塞放入了 AQS的阻塞队列,则会根据公平策略选择一个线程进行激活,激活的线程会尝试获取刚增加的信号量,源码如下:

  public void release() {
//(1)arg=1
sync.releaseShared();
} public final boolean releaseShared(int arg) { //(2)尝试释放资源
if (tryReleaseShared(arg)) { //(3)资源释放成功则调用park唤醒AQS队列里面最先挂起的线程
doReleaseShared();
return true;
}
return false;
} protected final boolean tryReleaseShared(int releases) {
for (;;) { //(4)获取当前信号量值
int current = getState(); //(5)当前信号量值增加releases,这里为增加1
int next = current + releases;
if (next < current) // 移除处理
throw new Error("Maximum permit count exceeded"); //(6)使用cas保证更新信号量值的原子性
if (compareAndSetState(current, next))
return true;
}
}

如上面代码可以看到 release()方法中对 sync.releaseShared(1),可以知道release方法每次只会对信号量值增加 1 ,tryReleaseShared方法是无限循环,使用CAS保证了 release 方法对信号量递增 1 的原子性操作。当tryReleaseShared 方法增加信号量成功后会执行代码(3),调用AQS的方法来激活因为调用acquire方法而被阻塞的线程。

  6.void release(int permits) 该方法与不带参数的不同之处在于前者每次调用会在信号量值原来基础上增加 permits,而后者每次增加 1。源码如下:

public void release(int permits) {
if (permits < ) throw new IllegalArgumentException();
sync.releaseShared(permits);
}

另外注意到这里调用的是 sync.releaseShared 是共享方法,这说明该信号量是线程共享的,信号量没有和固定线程绑定,多个线程可以同时使用CAS去更新信号量的值而不会阻塞。

到目前已经知道了其原理,接下来用一个例子来加深对Semaphore的理解,例子如下:

package com.hjc;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Semaphore; /**
* Created by cong on 2018/7/8.
*/
public class SemaphoreTest { // 创建一个Semaphore实例
private static volatile Semaphore semaphore = new Semaphore(); public static void main(String[] args) throws InterruptedException { ExecutorService executorService = Executors.newFixedThreadPool(); // 加入线程A到线程池
executorService.submit(new Runnable() {
public void run() {
try { System.out.println(Thread.currentThread() + " over");
semaphore.release(); } catch (Exception e) {
e.printStackTrace();
}
}
}); // 加入线程B到线程池
executorService.submit(new Runnable() {
public void run() {
try { System.out.println(Thread.currentThread() + " over");
semaphore.release(); } catch (Exception e) {
e.printStackTrace();
}
}
}); // 等待子线程执行完毕,返回
semaphore.acquire();
System.out.println("all child thread over!"); //关闭线程池
executorService.shutdown();
}
}

运行结果如下:

类似于 CountDownLatch,上面我们的例子也是在主线程中开启两个子线程进行执行,等所有子线程执行完毕后主线程在继续向下运行。

如上代码首先首先创建了一个信号量实例,构造函数的入参为 0,说明当前信号量计数器为 0,然后 main 函数添加两个线程任务到线程池,每个线程内部调用了信号量的 release 方法,相当于计数值递增一,最后在 main 线程里面调用信号量的 acquire 方法,参数传递为 2 说明调用 acquire 方法的线程会一直阻塞,直到信号量的计数变为 2 时才会返回。

看到这里也就明白了,如果构造 Semaphore 时候传递的参数为 N,在 M 个线程中调用了该信号量的 release 方法,那么在调用 acquire 对 M 个线程进行同步时候传递的参数应该是 M+N;

对CountDownLatch,CyclicBarrier,Semaphored这三者之间的比较总结:

  1.CountDownLatch 通过计数器提供了更灵活的控制,只要检测到计数器为 0,而不管当前线程是否结束调用 await 的线程就可以往下执行,相比使用 jion 必须等待线程执行完毕后主线程才会继续向下运行更灵活。

  2.CyclicBarrier 也可以达到 CountDownLatch 的效果,但是后者当计数器变为 0 后,就不能在被复用,而前者则使用 reset 方法可以重置后复用,前者对同一个算法但是输入参数不同的类似场景下比较适用。

  3.而 semaphore 采用了信号量递增的策略,一开始并不需要关心需要同步的线程个数,等调用 aquire 时候在指定需要同步个数,并且提供了获取信号量的公平性策略。

Java并发编程笔记之Semaphore信号量源码分析的更多相关文章

  1. 【Java并发编程】16、ReentrantReadWriteLock源码分析

    一.前言 在分析了锁框架的其他类之后,下面进入锁框架中最后一个类ReentrantReadWriteLock的分析,它表示可重入读写锁,ReentrantReadWriteLock中包含了两种锁,读锁 ...

  2. Java并发编程笔记之StampedLock锁源码探究

    StampedLock是JUC并发包里面JDK1.8版本新增的一个锁,该锁提供了三种模式的读写控制,当调用获取锁的系列函数的时候,会返回一个long 型的变量,该变量被称为戳记(stamp),这个戳记 ...

  3. java并发编程基础-ReentrantLock及LinkedBlockingQueue源码分析

    ReentrantLock是一个较为常用的锁对象.在上次分析的uil开源项目中也多次被用到,下面谈谈其概念和基本使用. 概念 一个可重入的互斥锁定 Lock,它具有与使用 synchronized 相 ...

  4. 【Java并发编程】19、DelayQueue源码分析

    DelayQueue,带有延迟元素的线程安全队列,当非阻塞从队列中获取元素时,返回最早达到延迟时间的元素,或空(没有元素达到延迟时间).DelayQueue的泛型参数需要实现Delayed接口,Del ...

  5. 【Java并发编程】17、SynchronousQueue源码分析

    SynchronousQueue是一种特殊的阻塞队列,不同于LinkedBlockingQueue.ArrayBlockingQueue和PriorityBlockingQueue,其内部没有任何容量 ...

  6. 【Java并发编程】18、PriorityBlockingQueue源码分析

    PriorityBlockingQueue是一个基于数组实现的线程安全的无界队列,原理和内部结构跟PriorityQueue基本一样,只是多了个线程安全.javadoc里面提到一句,1:理论上是无界的 ...

  7. 并发编程(四)—— ThreadLocal源码分析及内存泄露预防

    今天我们一起探讨下ThreadLocal的实现原理和源码分析.首先,本文先谈一下对ThreadLocal的理解,然后根据ThreadLocal类的源码分析了其实现原理和使用需要注意的地方,最后给出了两 ...

  8. 并发编程(九)—— Java 并发队列 BlockingQueue 实现之 LinkedBlockingQueue 源码分析

    LinkedBlockingQueue 在看源码之前,通过查询API发现对LinkedBlockingQueue特点的简单介绍: 1.LinkedBlockingQueue是一个由链表实现的有界队列阻 ...

  9. 并发编程(八)—— Java 并发队列 BlockingQueue 实现之 ArrayBlockingQueue 源码分析

    开篇先介绍下 BlockingQueue 这个接口的规则,后面再看其实现. 阻塞队列概要 阻塞队列与我们平常接触的普通队列(LinkedList或ArrayList等)的最大不同点,在于阻塞队列的阻塞 ...

随机推荐

  1. 【Android】异步加载布局探索

    最近在做的项目页面复杂导致布局嵌套多层,而且又使用了百分比布局(可能主要是这个原因)导致页面加载的时候主线程会被阻塞, 那要想减少主线程阻塞,一来就是简化布局,减轻LayoutInflater的负担, ...

  2. ie每次登陆出现:Windows安全性 iexplore.exe 正在连接到 记住我的凭证不起作用

    解决方案: ie浏览器--设置--Intenet选项--安全--Internet--自定义级别--用户身份验证--登陆 勾选自动使用当前用户名和密码登陆 确定--确定

  3. mybatis进阶--一对多查询

    首先,我们还是先给出一个需求:根据订单id查询订单明细——我们知道,一个订单里面可以有多个订单的明细(需求不明确的同学,请留言或者去淘宝网上的订单处点一下就知道了).这个时候,一个订单,对应多个订单的 ...

  4. 转 node.js和 android中java加密解密一致性问题;

    原文地址,请大家去原文博客了解; http://blog.csdn.net/linminqin/article/details/19972751 我保留一份,防止删除: var crypto = re ...

  5. boost asio 学习(三)post与dispatch

    http://www.gamedev.net/blog/950/entry-2249317-a-guide-to-getting-started-with-boostasio?pg=4 本章节为io_ ...

  6. caoni大业 spring boot 跳坑记

    IDEA环境 win10 跑得刚刚,到xp系统就戈壁 报错 Caused by: java.lang.NoSuchMethodError: javax.servlet.ServletContext.g ...

  7. 《大型网站系统与Java中间件实践》

    读了一下,个人认为最好的部分,就是第四章了. CH04 服务框架 4.2 服务设计与实现 // 获取可用服务地址列表 // 确定调用服务目标机器 // 建立连接(Socket) // 请求序列化 // ...

  8. 附加题2 :git 简单练习

    目的: 练习git 基本操作 理解版本管理 Task 1:在码云上新建一个项目,项目命名 『Helloworld』 ,设置为公开访问. step 0: 在你计算机安装git.参照互联网教程<gi ...

  9. Presto + Superset 数据仓库及BI

    基于Presto和superset搭建数据分析平台. Presto可以作为数据仓库,能够连接多种数据库和NoSql,同时查询性能很高: Superset提供了Presto连接,方便数据可视化和dash ...

  10. Mybatis中输入输出映射和动态Sql

    一.输入映射 我们通过配置parameterType的值来指定输入参数的类型,这些类型可以是简单数据类型.POJO.HashMap等数据类型 1.简单类型 2.POJO包装类型 ①这是单表查询的时候传 ...