并发
4、RateLimiter
Guava 中的 RateLimiter(位于 com.google.common.util.concurrent 包)是专门用于限制流量、实现限流防护的核心工具类。
它基于 令牌桶算法(Token Bucket Algorithm) 实现,广泛应用于 API 接口限流、高并发系统保护、第三方服务调用频率限制(如限制每秒最多发送 N N N 次请求)以及后台批处理任务的平滑降速。
4.1、底层原理
令牌桶算法 (Token Bucket)

核心工作机制:
- 令牌以固定速率生成:系统以设定的速率(如每秒产生 N N N 个令牌)平滑往桶里放入令牌。
- 桶有容量上限:如果桶满了,新生成的令牌就会被丢弃。
- 请求消耗令牌:每个请求在执行前需要先从桶中获取相应数量的令牌(通常为 1 个)。如果桶中有足够令牌:请求立即通过,桶中令牌减少。如果令牌不足:请求可以选择阻塞等待直到分配到令牌,或者立即放弃/降级(非阻塞模式)。
4.2、核心API
| 分类 | 核心 API 方法 | 说明 |
|---|---|---|
| 创建工厂 | create(double permitsPerSecond) |
创建平滑突发限流器(SmoothBursty),按固定速率生成令牌 |
| 创建工厂 | create(double permitsPerSecond, Duration warmupPeriod) |
创建预热限流器(SmoothWarmingUp),在预热期内渐进增加速率 |
| 阻塞式获取 | acquire() |
获取 1 个令牌,若没有则阻塞等待,返回实际等待的秒数 |
| 阻塞式获取 | acquire(int permits) |
获取指定数量的令牌,若没有则阻塞等待 |
| 非阻塞式尝试 | tryAcquire() |
尝试获取 1 个令牌,成功返回 true,失败立即返回 false |
| 非阻塞式尝试 | tryAcquire(Duration timeout) / tryAcquire(int permits, long timeout, TimeUnit unit) |
在指定的超时时间内尝试获取令牌,超时仍未拿到返回 false |
| 动态修改 | setRate(double permitsPerSecond) |
动态调整限流速率(如根据系统 load 动态降级) |
| 动态修改 | getRate() |
获取当前的限制速率 |
4.3、使用示例
1. 基础阻塞模式(acquire)
适合任务处理不能丢失、允许请求排队等待的场景(如数据库批量插入、日志平滑上报)。
java
import com.google.common.util.concurrent.RateLimiter;
public class RateLimiterAcquireDemo {
public static void main(String[] args) {
// 创建限制为每秒 2 个令牌的 RateLimiter (即每 500ms 产生 1 个令牌)
RateLimiter limiter = RateLimiter.create(2.0);
long start = System.currentTimeMillis();
for (int i = 1; i <= 5; i++) {
// acquire() 会阻塞直至拿到令牌,返回等待的时间(秒)
double waitTime = limiter.acquire();
long elapsed = System.currentTimeMillis() - start;
System.out.printf("请求 %d 成功获取令牌 | 等待耗时: %.2f秒 | 总耗时: %dms%n",
i, waitTime, elapsed);
}
}
}
java
请求 1 成功获取令牌 | 等待耗时: 0.00秒 | 总耗时: 2ms
请求 2 成功获取令牌 | 等待耗时: 0.48秒 | 总耗时: 502ms
请求 3 成功获取令牌 | 等待耗时: 0.50秒 | 总耗时: 1002ms
请求 4 成功获取令牌 | 等待耗时: 0.50秒 | 总耗时: 1500ms
请求 5 成功获取令牌 | 等待耗时: 0.50秒 | 总耗时: 2001ms
2. 非阻塞/超时快速失败模式(tryAcquire)
适合高并发 Web 接口防刷、服务自我保护(超出处理能力时直接截流抛异常或提示"系统繁忙,请稍后再试")。
java
import com.google.common.util.concurrent.RateLimiter;
import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class RateLimiterTryAcquireDemo {
public static void main(String[] args) throws InterruptedException {
// 每秒允许 5 个请求 (QPS = 5)
RateLimiter limiter = RateLimiter.create(5.0);
ExecutorService executor = Executors.newFixedThreadPool(10);
for (int i = 1; i <= 10; i++) {
final int requestId = i;
executor.submit(() -> {
// 1. 立即尝试获取,拿不到直接失败
// boolean success = limiter.tryAcquire();
// 2. 带有超时的尝试:允许最多等待 200ms
boolean success = limiter.tryAcquire(1, Duration.ofMillis(200));
if (success) {
System.out.println("【成功】请求 " + requestId + " 拿到令牌,开始处理业务");
} else {
System.err.println("【限流降级】请求 " + requestId + " 触发限流,拒绝服务");
}
});
}
executor.shutdown();
}
}
java
【限流降级】请求 7 触发限流,拒绝服务
【限流降级】请求 1 触发限流,拒绝服务
【限流降级】请求 8 触发限流,拒绝服务
【限流降级】请求 4 触发限流,拒绝服务
【限流降级】请求 3 触发限流,拒绝服务
【限流降级】请求 2 触发限流,拒绝服务
【限流降级】请求 6 触发限流,拒绝服务
【限流降级】请求 9 触发限流,拒绝服务
【成功】请求 5 拿到令牌,开始处理业务
【成功】请求 10 拿到令牌,开始处理业务
3. 预热限流模式(SmoothWarmingUp)
在冷启动或系统刚发布上线时,直接打入峰值流量可能导致数据库缓存未建好而崩溃。冷启动预热机制会在设定时间内让 QPS 从低速率平滑提升至最高速率。
java
import com.google.common.util.concurrent.RateLimiter;
import java.time.Duration;
public class RateLimiterWarmupDemo {
public static void main(String[] args) {
// QPS 限制为 5,同时设置 3 秒的预热期 (Warmup Period)
RateLimiter limiter = RateLimiter.create(5.0, Duration.ofSeconds(3));
System.out.println("--- 阶段 1: 预热期间(获取令牌耗时逐步降低,速率逐渐提升) ---");
for (int i = 1; i <= 10; i++) {
double waitTime = limiter.acquire();
System.out.printf("请求 %d 等待时间: %.3f 秒%n", i, waitTime);
}
System.out.println("--- 阶段 2: 预热完成后(维持正常的 0.2 秒固定间隔) ---");
for (int i = 11; i <= 15; i++) {
double waitTime = limiter.acquire();
System.out.printf("请求 %d 等待时间: %.3f 秒%n", i, waitTime);
}
}
}
java
--- 阶段 1: 预热期间(获取令牌耗时逐步降低,速率逐渐提升) ---
请求 1 等待时间: 0.000 秒
请求 2 等待时间: 0.550 秒
请求 3 等待时间: 0.513 秒
请求 4 等待时间: 0.465 秒
请求 5 等待时间: 0.413 秒
请求 6 等待时间: 0.355 秒
请求 7 等待时间: 0.303 秒
请求 8 等待时间: 0.249 秒
请求 9 等待时间: 0.201 秒
请求 10 等待时间: 0.199 秒
--- 阶段 2: 预热完成后(维持正常的 0.2 秒固定间隔) ---
请求 11 等待时间: 0.198 秒
请求 12 等待时间: 0.196 秒
请求 13 等待时间: 0.195 秒
请求 14 等待时间: 0.195 秒
请求 15 等待时间: 0.198 秒
4.3、注意事项
关键特性:预消费透支机制 (Past-Debt)
RateLimiter 内部有一个非常特殊的机制:允许当次请求透支未来的令牌。
如果你一次性调用 limiter.acquire(10) 请求 10 个令牌,即使当前桶里只有 1 个令牌,本次请求也会立即通过,但是需要由"下一次请求"来承担上一次透支的等待时间。
java
import com.google.common.util.concurrent.RateLimiter;
public class RateLimiterDebtDemo {
public static void main(String[] args) {
RateLimiter limiter = RateLimiter.create(1.0); // 1秒 1 个令牌
System.out.println("请求 A 一次性索取 5 个令牌...");
double waitA = limiter.acquire(5); // 桶里不够,但请求 A 马上通过,等待 0 秒
System.out.printf("请求 A 成功,等待耗时: %.2f秒%n", waitA);
System.out.println("请求 B 索取 1 个令牌...");
double waitB = limiter.acquire(1); // 请求 B 需要为请求 A 的"透支行为"买单,阻塞等待 5 秒!
System.out.printf("请求 B 成功,等待耗时: %.2f秒%n", waitB);
}
}
java
请求 A 一次性索取 5 个令牌...
请求 A 成功,等待耗时: 0.00秒
请求 B 索取 1 个令牌...
请求 B 成功,等待耗时: 4.98秒
生产实践与局限性说明
- 单机限流 vs 分布式限流:
- Guava RateLimiter 是纯单机内存级限流工具,适用于单个 JVM 应用进程内的自我保护。
- 如果是微服务集群协同限流(如全局限制某用户每分钟只允许请求 100 次),请选用基于 Redis + Lua 脚本 或 Sentinel 等分布式限流方案。
- 多线程并发安全性:
- RateLimiter 是线程安全的,底层使用了互斥锁(synchronized)维护全局的预分配时间(nextFreeTicketMicros),在一般吞吐量下性能极高。
- 动态调速:
- 在运行时调用 limiter.setRate(newRate) 可以无缝调整限流值,下一次获取令牌时会自动按新的速率重新计算,非常适合与分布式配置中心(如 Nacos、Apollo)集成。
5、Monitor
Guava 中的 Monitor(位于 com.google.common.util.concurrent 包)用于替代传统的 synchronized 关键字以及 ReentrantLock + Condition 组合。
在传统并发编程中,处理"条件等待"需要编写复杂的 while (!condition) { wait(); } 循环,极易发生虚假唤醒(Spurious Wakeup)、死锁或忘记 signalAll() 等问题。Monitor 将互斥锁与布尔条件(Guard)深度结合,显著简化了多线程环境下的同步控制。
5.1、核心概念
Monitor 的核心设计在于将条件判断抽象为 Monitor.Guard 抽象类。
通过定义 Guard 条件,线程可以在进入临界区时指定条件:"只有当条件满足时才获取锁并进入,不满足则自动阻塞等待"。
java
Monitor monitor = new Monitor();
// 定义 Condition 评估条件
Monitor.Guard listNotFull = new Monitor.Guard(monitor) {
@Override
public boolean isSatisfied() {
return list.size() < MAX_CAPACITY;
}
};
5.3、核心API
| 分类 | 核心 API 方法 | 说明 |
|---|---|---|
| 基础加解锁 | enter(), leave() |
相当于 ReentrantLock.lock() / unlock() |
| 条件阻塞进入 | enterWhen(Guard guard) |
阻塞直到获取锁且 Guard 条件为 true |
| 条件阻塞进入 | enterWhenUninterruptibly(Guard guard) |
不可中断地阻塞直到获取锁且 Guard 条件为 true |
| 带有超时的条件进入 | enterWhen(Guard guard, Duration timeout) |
在超时时间内等待条件满足并进入 |
| 尝试非阻塞进入 | enterIf(Guard guard) |
如果 Guard 为 true 则立即获取锁进入并返回 true,否则不获取锁返回 false |
| 条件等待(已持锁) | waitFor(Guard guard) |
在已持有 Monitor 锁的情况下,释放锁并等待 Guard 为 true |
| 状态查询 | hasQueuedThreads(), isOccupiedByCurrentThread() |
查询当前锁状态与排队线程 |
5.4、使用示例
基于 Monitor 实现有界阻塞队列
传统的 Producer-Consumer 模式如果用 ReentrantLock 实现,需要手动维护 notEmpty 和 notFull 两个 Condition。使用 Monitor 后,代码会变得极其直观。
java
import com.google.common.util.concurrent.Monitor;
import java.util.LinkedList;
import java.util.Queue;
public class BoundedQueue<E> {
private final Queue<E> queue = new LinkedList<>();
private final int capacity;
private final Monitor monitor = new Monitor();
// 1. 定义 Guard 条件:队列未满
private final Monitor.Guard NOT_FULL = new Monitor.Guard(monitor) {
@Override
public boolean isSatisfied() {
return queue.size() < capacity;
}
};
// 2. 定义 Guard 条件:队列非空
private final Monitor.Guard NOT_EMPTY = new Monitor.Guard(monitor) {
@Override
public boolean isSatisfied() {
return !queue.isEmpty();
}
};
public BoundedQueue(int capacity) {
this.capacity = capacity;
}
// 生产者入队方法
public void put(E item) throws InterruptedException {
// enterWhen 会自动加锁,并在条件不满足时自动挂起等待
monitor.enterWhen(NOT_FULL);
try {
queue.add(item);
System.out.println("生产元素: " + item + " | 队列大小: " + queue.size());
} finally {
// 离开临界区时自动唤醒等待其他 Guard 的线程
monitor.leave();
}
}
// 消费者出队方法
public E take() throws InterruptedException {
monitor.enterWhen(NOT_EMPTY);
try {
E item = queue.poll();
System.out.println("消费元素: " + item + " | 队列大小: " + queue.size());
return item;
} finally {
monitor.leave();
}
}
}
5.5、注意事项
| 维度 | synchronized / wait-notify | ReentrantLock + Condition | Guava Monitor |
|---|---|---|---|
| 条件控制 | 单一等待队列,易错发 notify |
需手动创建并维护多个 Condition |
声明式定义 Guard,按需使用 |
| 条件轮询与唤醒 | 需手动编写 while(condition) |
需手动编写 while(condition) |
框架全自动处理,无需显式 signal |
| 可读性与安全性 | 代码冗长,容易发生虚假唤醒 | 中等,需要显式 await/signal |
极高,语义明确且内置安全校验 |
- 自动唤醒机制(Implicit Signaling):
在标准 Condition 模式下,程序员必须显式调用 signal() 或 signalAll()。如果漏写会导致线程死锁。而在 Monitor 中,线程执行 monitor.leave() 时,Monitor 会自动检查是否有其他挂起的 Guard 已满足条件并唤醒对应线程。 - 防虚假唤醒(Spurious Wakeup Prevention):
由于 Guard 的状态由框架托管,用户代码无需额外包裹 while(!isSatisfied()) 循环,enterWhen 保证在返回时条件必定成立。
使用建议与最佳实践
- finally 确保释放锁:
与 ReentrantLock 相同,任何包含 monitor.enterWhen(...) 的代码块,都必须通过 try-finally 保证 monitor.leave() 被执行。 - Guard.isSatisfied() 必须无侧边效应:
isSatisfied() 方法会被 Monitor 内部多次调用以评估条件,因此绝对不能在 isSatisfied() 中修改变量状态或执行耗时 IO,仅允许做布尔逻辑判断。
6、Service / ServiceManager
Guava 中的 Service 与 ServiceManager(位于 com.google.common.util.concurrent 包)用于管理长生命周期后台服务的状态切换与协同监控。
在复杂的 Java 应用(如 RPC 服务端、消息消费节点、后台任务调度器)中,组件通常包含"启动、运行、停止、异常恢复"等生命周期。Service 接口统一了服务的状态机模型,而 ServiceManager 能够将多个服务组合在一起,实现一键批量启动、统一优雅停机、状态监控与故障广播。
6.1、Service 状态机与生命周期
Guava Service 定义了一套严格的状态转换模型:
java
[NEW]
│
│ startAsync()
▼
[STARTING] ──── (发生异常) ────┐
│ │
▼ │
[RUNNING] ──── (发生异常) ────┼───► [FAILED]
│ │
│ stopAsync() │
▼ │
[STOPPING] ──── (发生异常) ────┘
│
▼
[TERMINATED]
- NEW:服务刚创建,尚未启动。
- STARTING:正在初始化或拉起资源。
- RUNNING:正常运行中(核心业务逻辑正在执行)。
- STOPPING:正在清理资源、关闭连接、等待队列任务处理完成。
- TERMINATED:服务已成功优雅停止。
- FAILED:在启动、运行或停止阶段发生未捕获异常,导致服务非正常终止。
6.2、核心 API
1. Service 核心 API
| 方法签名 | 返回类型 | 说明 |
|---|---|---|
startAsync() |
Service |
异步启动服务,状态转换为 STARTING(链式调用) |
stopAsync() |
Service |
异步停止服务,状态转换为 STOPPING(链式调用) |
awaitRunning() |
void |
阻塞当前线程,直到服务进入 RUNNING 状态或启动失败 |
awaitTerminated() |
void |
阻塞当前线程,直到服务进入 TERMINATED 或 FAILED 状态 |
state() |
Service.State |
获取服务当前的实时状态 |
addListener(Listener, Executor) |
void |
为服务绑定状态监听器(监听 starting, running, failed 等事件) |
Guava 提供了 3 个非常方便的抽象基类供开发者继承实现:
- AbstractIdleService:适用于无循环任务的服务(如 RPC 客户端连接池、数据库连接池,启动即运行,停止即释放)。
- AbstractScheduledService:适用于周期性/定时后台任务的服务。
- AbstractExecutionThreadService:适用于需要独占一个单线程循环执行的服务(如消息队列消费者)。
2. ServiceManager 核心 API
| 方法签名 | 返回类型 | 说明 |
|---|---|---|
ServiceManager(Iterable<Service>) |
构造函数 | 创建管理一组 Service 的服务管理器 |
startAsync() |
ServiceManager |
异步并行启动所有受管服务 |
stopAsync() |
ServiceManager |
异步并行停止所有受管服务 |
awaitHealthy() |
void |
阻塞直到所有服务均变为 RUNNING 状态 |
awaitStopped() |
void |
阻塞直到所有服务均变为终止状态(TERMINATED 或 FAILED) |
isHealthy() |
boolean |
查询是否所有服务都处于 RUNNING 状态 |
servicesByState() |
ImmutableMultimap |
按状态分组查询所有的服务实例 |
startupTimes() |
ImmutableMap |
获取每个服务的启动耗时统计 |
addListener(Listener, Executor) |
void |
绑定全局管理器监听器(如处理有任何服务进入 FAILED 状态) |
6.3、使用示例
1. 基于 AbstractIdleService 实现数据库连接池服务
java
import com.google.common.util.concurrent.AbstractIdleService;
public class DatabasePoolService extends AbstractIdleService {
@Override
protected void startUp() throws Exception {
System.out.println("【数据库服务】正在初始化数据库连接池...");
Thread.sleep(500); // 模拟连接池建连耗时
System.out.println("【数据库服务】连接池初始化完成!");
}
@Override
protected void shutDown() throws Exception {
System.out.println("【数据库服务】正在归还连接并关闭数据库连接池...");
System.out.println("【数据库服务】连接池已安全关闭!");
}
}
2. 基于 AbstractScheduledService 实现后台定时清理服务
java
import com.google.common.util.concurrent.AbstractScheduledService;
import java.util.concurrent.TimeUnit;
public class CacheCleanupService extends AbstractScheduledService {
@Override
protected void runOneIteration() throws Exception {
System.out.println("【缓存清理服务】触发定时任务:清理过期本地缓存中...");
}
@Override
protected Scheduler scheduler() {
// 设置每隔 2 秒循环执行一次
return Scheduler.newFixedRateSchedule(0, 2, TimeUnit.SECONDS);
}
@Override
protected void startUp() throws Exception {
System.out.println("【缓存清理服务】定时清理引擎启动...");
}
@Override
protected void shutDown() throws Exception {
System.out.println("【缓存清理服务】定时清理引擎已停止。");
}
}
3. 基于 ServiceManager 协同管理与响应 JVM 优雅停机
通过 ServiceManager 将上述多个服务整合统一管控,并结合 JVM ShutdownHook 实现系统的优雅停机:
java
import com.google.common.util.concurrent.MoreExecutors;
import com.google.common.util.concurrent.Service;
import com.google.common.util.concurrent.ServiceManager;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.TimeoutException;
import java.util.concurrent.TimeUnit;
public class ApplicationServer {
public static void main(String[] args) {
// 1. 初始化独立服务
Service dbService = new DatabasePoolService();
Service cacheService = new CacheCleanupService();
List<Service> services = Arrays.asList(dbService, cacheService);
// 2. 统一交由 ServiceManager 管理
ServiceManager manager = new ServiceManager(services);
// 3. 为 ServiceManager 绑定全局监听器
manager.addListener(new ServiceManager.Listener() {
@Override
public void healthy() {
System.out.println("\n>>> [系统通知] 所有后台服务已成功启动,系统进入 HEALTHY 状态!<<<\n");
}
@Override
public void stopped() {
System.out.println("\n>>> [系统通知] 所有后台服务已全部安全停止!<<<\n");
}
@Override
public void failure(Service service) {
System.err.println("\n>>> [严重告警] 服务 [" + service.getClass().getSimpleName()
+ "] 启动/运行失败,异常原因: " + service.failureCause() + " <<<\n");
}
}, MoreExecutors.directExecutor());
// 4. 注册 JVM ShutdownHook 实现优雅关闭
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
System.out.println("\n收到 JVM 关闭信号,开始执行优雅停机...");
try {
// 最多等待 5 秒平滑停止所有受管服务
manager.stopAsync().awaitStopped(5, TimeUnit.SECONDS);
} catch (TimeoutException e) {
System.err.println("部分服务停止超时,强制退出");
}
}));
// 5. 一键异步启动所有服务并等待就绪
System.out.println("正在一键启动所有应用服务...");
manager.startAsync();
manager.awaitHealthy(); // 阻塞直到所有服务变为 RUNNING
// 打印服务的启动耗时统计
System.out.println("服务启动耗时汇总: " + manager.startupTimes());
// 模拟应用持续运行 3 秒
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
// 模拟手动触发停止
System.out.println("收到关闭指令,发起系统停止...");
manager.stopAsync();
manager.awaitStopped();
}
}
6.3、注意事项
为什么选择 Service / ServiceManager?
- 统一生命周期抽象,避免重复造轮子:
在没有 Service 前,很多组件通过自定义 init() 和 destroy() 方法管理生命周期,逻辑分散且状态控制容易混乱。Guava Service 将状态变化(STARTING -> RUNNING -> STOPPING -> TERMINATED)进行了标准化约束。 - 告别"死锁式"启动与依赖协同:
ServiceManager 能够并发并行拉起多个独立服务,显著加快应用启动速度。通过 awaitHealthy() 可以保证在所有底座服务就绪前,不暴露网络流量(如不向注册中心注册暴露接口),避免了冷启动期间流量打入导致崩溃。 - 强大的故障隔离与状态监控:
如果有任何一个核心后台服务在运行期间发生崩溃(转换为 FAILED 状态),ServiceManager.Listener.failure(...) 会第一时间感知并广播,方便监控告警或触发全盘保护策略。
最佳实践与建议
- 选择合适的基类:
- AbstractIdleService:用于处理连接建立/断开、静态资源装载/释放。
- AbstractScheduledService:用于替代 ScheduledExecutorService 执行定时轮询任务。
- AbstractExecutionThreadService:用于单线程死循环逻辑(如 while (isRunning()) { consume(); })。
- 在 shutDown() 中释放阻塞与响应中断:
如果服务中有阻塞的线程(如在等待 BlockingQueue.take() 或 Socket 读写),必须在 triggerShutdown() 或 shutDown() 中主动调用中断(thread.interrupt())或关闭底层连接,否则服务无法平滑进入 STOPPING / TERMINATED 状态。