引言

最近可以进行个税申报了,还没有申报的同学可以赶紧去试试哦。不过我反正是从上午到下午一直都没有成功的进行申报,一进行申报

就返回“当前访问人数过多,请稍后再试”。为什么有些人就能够申报成功,有些人就直接返回失败。这很明显申报处理资源是有限的,

只能等别人处理完了在来处理你的,你如果运气好可能重试几次就轮到你了,如果运气不好可能重试一天也可能轮不到你。

我反正已经是放弃了,等到夜深人静的时候再来试试。作为一个程序员我们肯定知道这是个税申请app的限流操作,如果还有不懂什么

是限流操作的可以参考下这个文章《高并发系统三大利器之限流》

比如个税申报系统每台机器只最多分别只能处理1000个请求,再多的请求就会把机器打挂。如果是多余的请求就把这些请求拒绝掉。直接给你返回一句温馨提示:“当前访问人数过多,请稍后再试”,如果要实现这个功能大家想想可以通过哪些方法算法来实现。

共享锁、独占锁

学习semaphore之前我们必须要先了解下什么是共享锁。在上一篇文章《Java高并发编程基础之AQS》我们介绍了公平锁于非公平锁的区别。

  • 共享锁:它是允许多个线程同时获取锁,并发的访问共享资源
  • 独占锁:也有人把它叫做“独享锁”,它是是独占的,排他的,只能被一个线程可持有,

    当独占锁已经被某个线程持有时,其他线程只能等待它被释放后,才能去争锁,并且同一时刻只有一个线程能争锁成功。

什么是Semaphore

在《Java并发编程艺术》(微信搜【java金融】回复电子书可以免费获取PDF版本)这一书中是这么说的:

Semaphore(信号量)是用来控制同时访问特定资源的线程数量,它通过协调各个线程,以保证合理的使用公共资源。很多年以来,我都觉得从字面上很难理解Semaphore所表达的含义,只能把它比作是控制流量的红绿灯,比如XX马路要限制流量,只允许同时有一百辆车在这条路上行使,其他的都必须在路口等待,所以前一百辆车会看到绿灯,可以开进这条马路,后面的车会看到红灯,不能驶入XX马路,但是如果前一百辆中有五辆车已经离开了XX马路,那么后面就允许有5辆车驶入马路,这个例子里说的车就是线程,驶入马路就表示线程在执行,离开马路就表示线程执行完成,看见红灯就表示线程被阻塞,不能执行。

  • Semaphore机制是提供给线程抢占式获取许可,所以他可以实现公平或者非公平,类似于ReentrantLock

    说了这么多我们来个实际的例子看一看,比如我们去停车场停车,停车场总共只有5个车位,但是现在有8辆汽车来停车,剩下的3辆汽车要么等其他汽车开走后进行停车,或者去找别的停车位?
/**
* @author: 公众号【Java金融】
*/
public class SemaphoreTest {
public static void main(String[] args) throws InterruptedException {
// 初始化五个车位
Semaphore semaphore = new Semaphore(5);
// 等所有车子
final CountDownLatch latch = new CountDownLatch(8);
for (int i = 0; i < 8; i++) {
int finalI = i;
if (i == 5) {
Thread.sleep(1000);
new Thread(() -> {
stopCarNotWait(semaphore, finalI);
latch.countDown();
}).start();
continue;
}
new Thread(() -> {
stopCarWait(semaphore, finalI);
latch.countDown();
}).start();
}
latch.await();
log("总共还剩:" + semaphore.availablePermits() + "个车位");
} private static void stopCarWait(Semaphore semaphore, int finalI) {
String format = String.format("车牌号%d", finalI);
try {
semaphore.acquire(1);
log(format + "找到车位了,去停车了");
Thread.sleep(10000);
} catch (Exception e) {
e.printStackTrace();
} finally {
semaphore.release(1);
log(format + "开走了");
}
} private static void stopCarNotWait(Semaphore semaphore, int finalI) {
String format = String.format("车牌号%d", finalI);
try {
if (semaphore.tryAcquire()) {
log(format + "找到车位了,去停车了");
Thread.sleep(10000);
log(format + "开走了");
semaphore.release();
} else {
log(format + "没有停车位了,不在这里等了去其他地方停车去了");
}
} catch (Exception e) {
e.printStackTrace();
} } public static void log(String content) {
// 格式化
DateTimeFormatter fmTime = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
// 当前时间
LocalDateTime now = LocalDateTime.now();
System.out.println(now.format(fmTime) + " "+content);
}
}
2021-03-01 18:54:57  车牌号0找到车位了,去停车了
2021-03-01 18:54:57 车牌号3找到车位了,去停车了
2021-03-01 18:54:57 车牌号2找到车位了,去停车了
2021-03-01 18:54:57 车牌号1找到车位了,去停车了
2021-03-01 18:54:57 车牌号4找到车位了,去停车了
2021-03-01 18:54:58 车牌号5没有停车位了,不在这里等了去其他地方停车去了
2021-03-01 18:55:07 车牌号7找到车位了,去停车了
2021-03-01 18:55:07 车牌号6找到车位了,去停车了
2021-03-01 18:55:07 车牌号2开走了
2021-03-01 18:55:07 车牌号0开走了
2021-03-01 18:55:07 车牌号3开走了
2021-03-01 18:55:07 车牌号4开走了
2021-03-01 18:55:07 车牌号1开走了
2021-03-01 18:55:17 车牌号7开走了
2021-03-01 18:55:17 车牌号6开走了
2021-03-01 18:55:17 总共还剩:5个车位

从输出结果我们可以看到车牌号5这辆车看见没有车位了,就不在这个地方傻傻的等了,而是去其他地方了,但是车牌号6车牌号7分别需要等到车库开出两辆车空出两个车位后才停进去。这就体现了Semaphoreacquire 方法如果没有获取到凭证它就会阻塞,而tryAcquire方法如果没有获取到凭证不会阻塞的。

semaphore在dubbo中的应用

Dubbo中可以给Provider配置线程池大小来控制系统提供服务的最大并行度,默认是200

<dubbo:provider  threads="200"/>

比如我现在这个订单系统有三个接口,分别为创单、取消订单、修改订单。这三个接口加起来的并发是200但是创单接口是核心接口,我想让它多分点线程来执行

让它可以有最大150个线程,取消订单和修改订单分别最大25个线程执行就可以了。dubbo提供了executes这一属性来实现这个功能

<dubbo:service interface="cn.javajr.service.CreateOrderService" executes="150"/>
<dubbo:service interface="cn.javajr.service.CancelOrderService" executes="25"/>
<dubbo:service interface="cn.javajr.service.EditOrderService" executes="25"/>

我们可以看看dubbo内部是如何来executes的,具体实现是在ExecuteLimitFilter这个类我们可以

 public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
URL url = invoker.getUrl();
String methodName = invocation.getMethodName();
Semaphore executesLimit = null;
boolean acquireResult = false;
int max = url.getMethodParameter(methodName, Constants.EXECUTES_KEY, 0);
if (max > 0) {
RpcStatus count = RpcStatus.getStatus(url, invocation.getMethodName());
// 如果当前使用的线程数量已经大于等于设置的阈值,那么直接抛出异常
// if (count.getActive() >= max) {
// throw new RpcException("Failed to invoke method " + invocation.getMethodName() + " in provider " + url + ", cause: The service // using threads greater than <dubbo:service executes=\"" + max + "\" /> limited.");
/**
* http://manzhizhen.iteye.com/blog/2386408
* use semaphore for concurrency control (to limit thread number)
*/ executesLimit = count.getSemaphore(max);
if(executesLimit != null && !(acquireResult = executesLimit.tryAcquire())) {
throw new RpcException("Failed to invoke method " + invocation.getMethodName() + " in provider " + url + ", cause: The service using threads greater than <dubbo:service executes=\"" + max + "\" /> limited.");
}
}
long begin = System.currentTimeMillis();
boolean isSuccess = true;
// 计数器+1
RpcStatus.beginCount(url, methodName);
try {
Result result = invoker.invoke(invocation);
return result;
} catch (Throwable t) {
isSuccess = false;
if (t instanceof RuntimeException) {
throw (RuntimeException) t;
} else {
throw new RpcException("unexpected exception when ExecuteLimitFilter", t);
}
} finally {
// 计数器-1
RpcStatus.endCount(url, methodName, System.currentTimeMillis() - begin, isSuccess);
if(acquireResult) {
executesLimit.release();
}
}
}

从上述代码我们也可以看出早期这个是没有采用Semaphore来实现的,而是直接采用被注释的 if (count.getActive() >= max) 这个来来实现的,由于这个count.getActive() >= max 和这个计数加1不是原子性的,所以会有问题,具体bug号可以看https://github.com/apache/dubbo/pull/582后面才采用上述代码用Semaphore来修复非原子性问题。具体更详细的分析可以参见代码的链接。不过现在最新版本(2.7.9)我看是采用采用自旋加上和CAS来实现的。

Semaphore

上面就是对Semaphore一个简单的使用以及dubbo中用到的例子,说句实话Semaphore在工作中用的还是比较少的,不过面试又有可能会被问到,所以还是有必要来一起学习一下它。我们前面《Java高并发编程基础之AQS》通过ReentrantLock 一起学习了下AQS,其实Semaphore同样也是通过AQS来是实现的,我们可以一起来对照下独占锁的方法,基本上都是有方法一一相对应的。



这里有两点稍微需要注意的地方:

  • 在独占锁模式中,我们只有在获取了独占锁的节点释放锁时,才会唤醒后继节点,因为独占锁只能被一个线程持有,如果它还没有被释放,就没有必要去唤醒它的后继节点。
  • 在共享锁模式下,当一个节点获取到了共享锁,我们在获取成功后就可以唤醒后继节点了,而不需要等到该节点释放锁的时候,这是因为共享锁可以被多个线程同时持有,一个锁获取到了,则后继的节点都可以直接来获取。因此,在共享锁模式下,在获取锁和释放锁结束时,都会唤醒后继节点。

获取凭证

我们同样还是通过非公平锁的模式来老获取凭证

我们可以看下acquire的核心方法

  public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
} // 主要看下这个方法,这个方法返回的值也就是tryAcquireShared返回的值,因为tryAcquireShared->nonfairTryAcquireShared
final int nonfairTryAcquireShared(int acquires) {
//自旋
for (;;) {
//Semaphore用AQS的state变量的值代表可用许可数
int available = getState();
//可用许可数减去本次需要获取的许可数即为剩余许可数
int remaining = available - acquires;
//如果剩余许可数小于0或者CAS将当前可用许可数设置为剩余许可数成功,则返回成功许可数
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
  • tryAcquireShared 获取返回许可书小于0时说明获取许可失败需要进入doAcquireSharedInterruptibly这个方法去休眠。
  • tryAcquireShared 获取返回许可书小于0时说明获取许可成功直接结束。

doAcquireSharedInterruptibly

```java
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 独占锁的acquireQueued调用的是addWaiter(Node.EXCLUSIVE),
// 而共享锁调用的是addWaiter(Node.SHARED),表明了该节点处于共享模式
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}

这个方法是不是跟我们上篇文章讲的AQS的独占锁的acquireQueued很像,不过独占锁它是直接调用了用了setHead(node)方法,而共享锁调用的是setHeadAndPropagate(node, r)

这个方法除了调用setHead 里面还调用了doReleaseShared(唤醒后继节点)

    private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
setHead(node);
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
}

其他的方法基本上是和ReentrantLock来实现的独占锁差不多,我相信大家对源码分析感兴趣的应该也不多,其他更多细节问题还是需要自己亲自动手去看源码的。

总结

  • 当信号量Semaphore初始化设置许可证为1 时,它也可以当作互斥锁使用。其中0、1就相当于它的状态,当=1时表示其他线程可以获取,当=0时,排他,即其他线程必须要等待。
  • SemaphoreJUC包中的一个很简单的工具类,用来实现多线程下对于资源的同一时刻的访问线程数限制
  • Semaphore中存在一个【许可】的概念,即访问资源之前,先要获得许可,如果当前许可数量为0,那么线程阻塞,直到获得许可
  • Semaphore内部使用AQS实现,由抽象内部类Sync继承了AQS。因为Semaphore天生就是共享的场景,所以其内部实际上类似于共享锁的实现
  • 共享锁的调用框架和独占锁很相似,它们最大的不同在于获取锁的逻辑——共享锁可以被多个线程同时持有,而独占锁同一时刻只能被一个线程持有。
  • 由于共享锁同一时刻可以被多个线程持有,因此当头节点获取到共享锁时,可以立即唤醒后继节点来争锁,而不必等到释放锁的时候。因此,共享锁触发唤醒后继节点的行为可能有两处,一处在当前节点成功获得共享锁后,一处在当前节点释放共享锁后。
  • 采用semaphore来进行限流的话会产生突刺现象

指在一定时间内的一小段时间内就用完了所有资源,后大部分时间中无资源可用。

比如在限流方法中的计算器算法,设置1s内的最大请求数为100,在前100ms已经永远了100个请求,则后面900ms将无法处理请求,这就是突刺现象

结束

  • 由于自己才疏学浅,难免会有纰漏,假如你发现了错误的地方,还望留言给我指出来,我会对其加以修正。
  • 如果你觉得文章还不错,你的转发、分享、赞赏、点赞、留言就是对我最大的鼓励。
  • 感谢您的阅读,十分欢迎并感谢您的关注。

    站在巨人的肩膀上摘苹果:

    https://segmentfault.com/a/1190000016447307

Java并发编程基础三板斧之Semaphore的更多相关文章

  1. Java并发编程--基础进阶高级(完结)

    Java并发编程--基础进阶高级完整笔记. 这都不知道是第几次刷狂神的JUC并发编程了,从第一次的迷茫到现在比较清晰,算是个大进步了,之前JUC笔记不见了,重新做一套笔记. 参考链接:https:// ...

  2. Java并发编程基础

    Java并发编程基础 1. 并发 1.1. 什么是并发? 并发是一种能并行运行多个程序或并行运行一个程序中多个部分的能力.如果程序中一个耗时的任务能以异步或并行的方式运行,那么整个程序的吞吐量和可交互 ...

  3. 并发-Java并发编程基础

    Java并发编程基础 并发 在计算机科学中,并发是指将一个程序,算法划分为若干个逻辑组成部分,这些部分可以以任何顺序进行执行,但与最终顺序执行的结果一致.并发可以在多核操作系统上显著的提高程序运行速度 ...

  4. Java并发编程基础-线程安全问题及JMM(volatile)

    什么情况下应该使用多线程 : 线程出现的目的是什么?解决进程中多任务的实时性问题?其实简单来说,也就是解决“阻塞”的问题,阻塞的意思就是程序运行到某个函数或过程后等待某些事件发生而暂时停止 CPU 占 ...

  5. java并发编程基础概念

    本次内容主要讲进程和线程.CPU核心数和线程数.CPU时间片轮转机制.上下文切换,并行和并发的基本概念以及并发编程的好处和注意事项,为java并发编程打下扎实基础. 1.什么是进程和线程 1.1 进程 ...

  6. 多线程(一)java并发编程基础知识

    线程的应用 如何应用多线程 在 Java 中,有多种方式来实现多线程.继承 Thread 类.实现 Runnable 接口.使用 ExecutorService.Callable.Future 实现带 ...

  7. Java并发编程基础之volatile

    首先简单介绍一下volatile的应用,volatile作为Java多线程中轻量级的同步措施,保证了多线程环境中“共享变量”的可见性.这里的可见性简单而言可以理解为当一个线程修改了一个共享变量的时候, ...

  8. java并发编程学习:用 Semaphore (信号量)控制并发资源

    并发编程这方面以前关注得比较少,恶补一下,推荐一个好的网站:并发编程网 - ifeve.com,上面全是各种大牛原创或编译的并发编程文章. 今天先来学习Semaphore(信号量),字面上看,根本不知 ...

  9. java并发编程基础——线程的创建

    一.基础概念 1.进程和线程 进程:每个进程都有独立的代码和数据空间(进程上下文),进程间的切换会有较大的开销,一个进程包含1--n个线程.(进程是资源分配的最小单位) 线程:同一类线程共享代码和数据 ...

随机推荐

  1. HDU - 2825 Wireless Password (AC自动机+状压DP)

    题目连接:http://acm.hdu.edu.cn/showproblem.php?pid=2825 题意:给一些字符串,构造出长度为n的字符串,它至少包含k个所给字符串,求能构造出的个数. 题解: ...

  2. hdu 13394 Minimum Inversion Number 线段树

    题意: 首先给你一个长度为n的序列v,你需要首先找出来逆序对(i<j && v[i]>v[j]) 然后把这个序列的最后一个元素放在第一个位置上,其他元素都向后移动一位. 一 ...

  3. Codeforces Round #529 (Div. 3) D. Circular Dance (思维)

    题意:有\(n\)个熊小孩,绕着树转圈圈,编号\(i\)的小孩可以记住\(a_{i,1}\)和\(a_{i,2}\)两个小孩,这两个小孩是顺时针相邻的,但谁前谁后不一定.现在给你每个小孩的\(a_{i ...

  4. Codeforces Round #653 (Div. 3) A. Required Remainder (数学)

    题意:有三个正整数\(x,y,n\),再\(1\)~\(n\)中找一个最大的数\(k\),使得\(k\ mod\ x=y\). 题解:先记\(tmp=n/x\),再判断\(tmp*x+y\)的值是否大 ...

  5. 二叉树增删改查 && 程序实现

    二叉排序树定义 一棵空树,或者是具有下列性质的二叉树:(1)若左子树不空,则左子树上所有结点的值均小于它的根结点的值:(2)若右子树不空,则右子树上所有结点的值均大于它的根结点的值:(3)左.右子树也 ...

  6. Ubuntu Live CD联网修复

    此模式下可以联网修复ubuntu系统下绝大多数问题.进入LIVE CD模式,打开终端执行以下命令: #此处/dev/sda1为ubuntu根分区,工作中根据实际分区情况更改 sudo mount /d ...

  7. Chapter Zero 0.1.4 计算机上常用的计算单位

    0.1 计算机硬件 计算机上常用的计算单位 容量单位: 计算机对于数据的判断依据有没有通电来记录信息,对于每个记录而言, 他只认识0或1,而0/1这个二进制单位我们成为bit. 因为bit太小,所以存 ...

  8. woj1012 Thingk and Count DP好题

    title: woj1012 Thingk and Count DP好题 date: 2020-03-12 categories: acm tags: [acm,dp,woj] 难题,dp好题,几何题 ...

  9. leetcode 12 整数转罗马数字 贪心

    额,连着两个贪心? 这是局部最优问题:能用大"罗马数表示"就不会用小的. 先构造出所有基础罗马数,然后从大到小比较 因为比较的只有1000,900,...有限并有些麻烦,构造tab ...

  10. 深入理解gradle中的task

    目录 简介 定义task tasks 集合类 Task 之间的依赖 定义task之间的顺序 给task一些描述 task的条件执行 task rule Finalizer tasks 总结 深入理解g ...