Java并发编程实战
王宝令
资深架构师
立即订阅
15092 人已学习
课程目录
已完结 50 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 你为什么需要学习并发编程?
免费
学习攻略 (1讲)
学习攻略 | 如何才能学好并发编程?
第一部分:并发理论基础 (13讲)
01 | 可见性、原子性和有序性问题:并发编程Bug的源头
02 | Java内存模型:看Java如何解决可见性和有序性问题
03 | 互斥锁(上):解决原子性问题
04 | 互斥锁(下):如何用一把锁保护多个资源?
05 | 一不小心就死锁了,怎么办?
06 | 用“等待-通知”机制优化循环等待
07 | 安全性、活跃性以及性能问题
08 | 管程:并发编程的万能钥匙
09 | Java线程(上):Java线程的生命周期
10 | Java线程(中):创建多少线程才是合适的?
11 | Java线程(下):为什么局部变量是线程安全的?
12 | 如何用面向对象思想写好并发程序?
13 | 理论基础模块热点问题答疑
第二部分:并发工具类 (14讲)
14 | Lock和Condition(上):隐藏在并发包中的管程
15 | Lock和Condition(下):Dubbo如何用管程实现异步转同步?
16 | Semaphore:如何快速实现一个限流器?
17 | ReadWriteLock:如何快速实现一个完备的缓存?
18 | StampedLock:有没有比读写锁更快的锁?
19 | CountDownLatch和CyclicBarrier:如何让多线程步调一致?
20 | 并发容器:都有哪些“坑”需要我们填?
21 | 原子类:无锁工具类的典范
22 | Executor与线程池:如何创建正确的线程池?
23 | Future:如何用多线程实现最优的“烧水泡茶”程序?
24 | CompletableFuture:异步编程没那么难
25 | CompletionService:如何批量执行异步任务?
26 | Fork/Join:单机版的MapReduce
27 | 并发工具类模块热点问题答疑
第三部分:并发设计模式 (10讲)
28 | Immutability模式:如何利用不变性解决并发问题?
29 | Copy-on-Write模式:不是延时策略的COW
30 | 线程本地存储模式:没有共享,就没有伤害
31 | Guarded Suspension模式:等待唤醒机制的规范实现
32 | Balking模式:再谈线程安全的单例模式
33 | Thread-Per-Message模式:最简单实用的分工方法
34 | Worker Thread模式:如何避免重复创建线程?
35 | 两阶段终止模式:如何优雅地终止线程?
36 | 生产者-消费者模式:用流水线思想提高效率
37 | 设计模式模块热点问题答疑
第四部分:案例分析 (4讲)
38 | 案例分析(一):高性能限流器Guava RateLimiter
39 | 案例分析(二):高性能网络应用框架Netty
40 | 案例分析(三):高性能队列Disruptor
41 | 案例分析(四):高性能数据库连接池HiKariCP
第五部分:其他并发模型 (4讲)
42 | Actor模型:面向对象原生的并发模型
43 | 软件事务内存:借鉴数据库的并发经验
44 | 协程:更轻量级的线程
45 | CSP模型:Golang的主力队员
结束语 (1讲)
结束语 | 十年之后,初心依旧
用户故事 (2讲)
用户来信 | 真好,面试考到这些并发编程,我都答对了!
3 个用户来信 | 打开一个新的并发世界
Java并发编程实战
登录|注册

38 | 案例分析(一):高性能限流器Guava RateLimiter

王宝令 2019-05-25
从今天开始,我们就进入案例分析模块了。 这个模块我们将分析四个经典的开源框架,看看它们是如何处理并发问题的,通过这四个案例的学习,相信你会对如何解决并发问题有个更深入的认识。
首先我们来看看 Guava RateLimiter 是如何解决高并发场景下的限流问题的。Guava 是 Google 开源的 Java 类库,提供了一个工具类 RateLimiter。我们先来看看 RateLimiter 的使用,让你对限流有个感官的印象。假设我们有一个线程池,它每秒只能处理两个任务,如果提交的任务过快,可能导致系统不稳定,这个时候就需要用到限流。
在下面的示例代码中,我们创建了一个流速为 2 个请求 / 秒的限流器,这里的流速该怎么理解呢?直观地看,2 个请求 / 秒指的是每秒最多允许 2 个请求通过限流器,其实在 Guava 中,流速还有更深一层的意思:是一种匀速的概念,2 个请求 / 秒等价于 1 个请求 /500 毫秒。
在向线程池提交任务之前,调用 acquire() 方法就能起到限流的作用。通过示例代码的执行结果,任务提交到线程池的时间间隔基本上稳定在 500 毫秒。
//限流器流速:2个请求/秒
RateLimiter limiter =
RateLimiter.create(2.0);
//执行任务的线程池
ExecutorService es = Executors
.newFixedThreadPool(1);
//记录上一次执行时间
prev = System.nanoTime();
//测试执行20次
for (int i=0; i<20; i++){
//限流器限流
limiter.acquire();
//提交任务异步执行
es.execute(()->{
long cur=System.nanoTime();
//打印时间间隔:毫秒
System.out.println(
(cur-prev)/1000_000);
prev = cur;
});
}
输出结果:
...
500
499
499
500
499

经典限流算法:令牌桶算法

Guava 的限流器使用上还是很简单的,那它是如何实现的呢?Guava 采用的是令牌桶算法,其核心是要想通过限流器,必须拿到令牌。也就是说,只要我们能够限制发放令牌的速率,那么就能控制流速了。令牌桶算法的详细描述如下:
取消
完成
0/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《Java并发编程实战》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(37)

  • 花儿少年
    很精髓的就是reserve方法,我来试着稍微解释一下
    首先肯定是计算令牌桶里面的令牌数量
    然后取令牌桶中的令牌数量storedPermits 与当前的需要的令牌数量 1 做比较,大于等于 1,说明令牌桶至少有一个令牌,此时下一令牌的获取是不需要等待的,表现为 next 不需要变化;而当令牌桶中的令牌没有了即storedPermits等于 0 时,next 就会变化为下一个令牌的获取时间,注意 nr 的值变化

    作者回复: 👍

    2019-06-18
    3
    7
  • null
    re:为什么令牌是从令牌桶中出的,那么 next 就无需增加一个 interval?

    next 变量的意思是下一个令牌的生成时间,可以理解为当前线程请求的令牌的生成时刻,如第一张图所示:线程 T1 的令牌的生成时刻是第三秒。

    线程 T 请求时,存在三种场景:
    1. 桶里有剩余令牌。
    2. 刚创建令牌,线程同时请求。
    3. 桶里无剩余令牌。

    场景 2 可以想象成线程请求的同时令牌刚好生成,没来得及放入桶内就被线程 T 拿走了。因此将场景 2 和场景 3 合并成一种情况,那就是桶里没令牌。即线程请求时,桶里可分为有令牌和没令牌。

    “桶里没令牌”,线程 T 需要等待;需要等待则意味着 now(线程 T 请求时刻) 小于等于 next(线程 T 所需的令牌的生成时刻)。这里可以想象一下线程 T 在苦苦等待令牌生成的场景,只要线程 T 等待那么久之后,就会被放行。放行这一刻令牌同时生成,立马被线程拿走,令牌没放入桶里。对应到代码就是 resync 方法没有进入 if 语句内。

    “桶里有令牌”,线程 T 不需要等待。说明线程 T 对应的令牌已经早早生成,已在桶内。代码就是:now > next(请求时刻大于对应令牌的生成时刻)。因此在分配令牌给线程之前,需要计算线程 T 迟到了多久,迟到的这段时间,有多少个令牌生成¹;然后放入桶内,满了则丢弃²;未来的线程的令牌在这个时刻已经生成放入桶内³(即 resync 方法的逻辑)。线程无需等待,所以不需要增加一个 interval 了。

    角标分别对应 resync 方法内的代码:
    ¹: long newPermits=(now-next)/interval;
    ²: storedPermits=min(maxPermits,
            storedPermits + newPermits);
    ³: next = now;

    作者回复: 👍条理清晰

    2019-08-09
    2
    6
  • the geek
    老师,当b>1时的reserve方法写的有问题吧,long at = next;不应该是第一行,而应该在// 重新计算下一令牌产生时间
        next = next + nr*interval;
    这行代码之后吧
    2019-06-04
    1
    4
  • 辣椒
    // 令牌净需求:首先减掉令牌桶中的令牌
         long nr = 1 - fb;
         // 重新计算下一令牌产生时间
         next = next + nr*interval;
         // 重新计算令牌桶中的令牌
         this.storedPermits -= fb;

    老师这儿没有看懂,能不能解释一下?
    2019-06-04
    9
    4
  • zsh0103
    老师好,问个问题。文中代码b=3,r=1/s时,如果在next之后同时来了3个请求,应该时都可以获得令牌的对吧。就是说这3个请求都可以执行。那岂不是违背了r=1/s的限制吗。

    作者回复: 按照令牌桶算法是这样的,所以b不能搞得太大

    2019-05-26
    3
  • 涛哥迷妹
    long interval = 1000_000_000;
    这是什么写法
    2019-05-30
    1
    2
  • 刘鸿博
    newPermits, storePermits, fb, nr 都应该是double, 而不是long.

    作者回复: 示例代码只是为了更容易理解,实际应用还是要参考guava的实现

    2019-08-26
    1
    1
  • speedy9
    老师,前一个桶大小为1的代码是不是写错了,// 返回线程需要等待的时间 应该是return Math.max(at-now,0)吧
    2019-06-11
    1
    1
  • Darren
    老师,请教一下,限流器和信号量为什么感觉一样的,那为什么2个还都存在?是因为业务场景不同吗?请老师解惑下

    作者回复: 限流器不需要释放操作,信号量没办法控制带时间范围的限流,只能用于非常简单的场景

    2019-05-26
    1
    1
  • 很精彩!老师应该去讲数据结构与算法:)

    作者回复: 何必难为自己呢,不讲了😄

    2019-05-25
    1
    1
  • 赤城
    令牌桶原理:
          从令牌桶中获取令牌,如果令牌数量大于1,则直接获取,如果令牌桶中已经没有令牌,则等待时间加一个间隔时间,同时令牌桶中新增一批令牌(数量为每秒限流数)。
    令牌桶实现:
          通过acquire获取令牌时,将当前时间传递到方法中,返回一个获取令牌时间,如果时间与当前时间相同,说明令牌时从令牌桶中直接获取的,如果返回一个大于当前时间的值,则等待一段时间(返回值-当前时间)。
           在获取令牌时间时,具体实现代码很优秀,先赞一个!如果当前时间大于上次令牌时间,则将上次令牌时间设置为当前时间,同时令牌桶中根据限流率添加一批令牌,要保证令牌总数不大于最大值。然后再从令牌桶中取一个令牌返回。
    2019-11-07
  • 18518471028
    容量为1的 synchronized long reserve(long now) 这个方法中的 Math.max(at,0)没有意义,at一直大于0
    2019-10-17
  • 静海
    老师,请教下接收rocketmq消息,能否采用限流器Guava RateLimiter进行限流? 要怎么做?

    作者回复: 没这么做过,应该是做不了的

    2019-09-18
  • 一个慢慢爬行的普通人
    老师,我刚刚应该是想错了,线程池任务提交频繁是不是导致线程池存储任务队列不断扩大,从而可能会导致系统不稳定,但是这方面线程池也可以用有界队列来控制,所以不太清楚是什么能够导致系统不稳定

    作者回复: 一个例子是,恶意攻击导致正常的请求被拒绝。一般来讲系统到达极限后会出各种意料之外的问题

    2019-09-16
  • 韩大
    guava的ratelimit好像是阻塞的,而不是抛弃请求,这样会不会导致用户响应时间过长的问题?

    作者回复: 会,限流主要是保护后端不死,慢总比死了好

    2019-08-16
  • shniu
    限流器老师讲的浅显易懂,发现有些同学有些疑问,试着解答一下。

    1. 是否会有并发问题?
    并发问题应该是不存在的,限流器的竞争资源是令牌(permit),实现中令牌是动态计算出来的,增加了并发访问控制,synchronized reverse(),这里的同步仅仅是加在了预占令牌上,非常好的设计
    2. maxPermits 大于1的代码没看懂?
    分了两种情况,下一个令牌产生时间落后于当前时间时,需要重置下一次令牌产生时间和计算令牌桶中可用的令牌;然后,所有的请求都按照相同的令牌获取算法,代码中在计算能获得令牌的时间时,又分了两种情况,令牌桶中有令牌和没有令牌,没有令牌的时候需要计算下一次产生令牌的时间,有令牌的时候需要减去令牌桶中的令牌,这就是那几行比较晦涩一些代码要做的事情

    自己的浅显理解

    作者回复: 👍

    2019-07-24
  • 令牌桶容量为一讲的很清楚,,容量大于一讲的太模糊。讲授什么东西,不需要虚无缥缈,最好脚踏实地,接地气
    2019-07-22
  • 星辰
    好厉害啊 积分函数都用上了
    2019-07-09
  • 冰激凌的眼泪
    预期时间-当前时间=等待时间
    2019-07-02
  • 加油鸭
    这节是我觉得对我而言,最有用的一节
    2019-06-05
收起评论
37
返回
顶部