HOOOS

Java 并发工具 Semaphore:高并发场景下的限流神器

0 61 并发小能手 Java并发编程限流
Apple

“喂,小王啊,最近系统访问量激增,经常卡顿,你看看能不能想想办法?”

“收到,领导!我这就去排查!”

作为一名 Java 开发者,相信你对上面这段对话一定不陌生。在高并发场景下,系统很容易因为流量过大而出现各种问题,比如响应变慢、CPU 飙升、甚至直接崩溃。为了避免这些问题,限流就成了我们必须掌握的一项技能。

今天,咱们就来聊聊 Java 并发包 java.util.concurrent 中的一个限流利器——Semaphore

什么是 Semaphore?

Semaphore,中文名“信号量”,是一种基于计数的同步工具。你可以把它想象成一个“许可证”管理器。它维护了一组许可证,线程在执行操作前需要先获取许可证,执行完毕后再释放许可证。如果许可证被拿光了,其他线程就只能等着。

Semaphore 有两种主要的构造方法:

  • Semaphore(int permits):创建具有给定数量许可证的 Semaphore,使用非公平的同步策略。
  • Semaphore(int permits, boolean fair):创建具有给定数量许可证的 Semaphore,并可以指定是否使用公平的同步策略。fairtrue 时,表示采用公平策略,即等待时间最长的线程优先获取许可证;fairfalse 时,则采用非公平策略,即线程获取许可证的顺序是不确定的。

Semaphore 最常用的两个方法是:

  • acquire():获取一个许可证,如果许可证不可用,则当前线程会被阻塞,直到有许可证被释放。
  • release():释放一个许可证,将其返回给 Semaphore

Semaphore 如何实现限流?

Semaphore 的核心思想就是控制并发线程的数量。通过限制同时访问特定资源的线程数量,我们可以有效地控制系统的负载,防止系统被过多的请求压垮。

举个例子,假设我们有一个接口,最多只允许 10 个线程同时访问。我们可以这样使用 Semaphore

import java.util.concurrent.Semaphore;

public class RateLimiter {

    private static final Semaphore semaphore = new Semaphore(10); // 允许10个线程同时访问

    public void accessResource() {
        try {
            semaphore.acquire(); // 获取许可证
            // 模拟业务逻辑处理
            System.out.println(Thread.currentThread().getName() + " 正在访问资源...");
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release(); // 释放许可证
            System.out.println(Thread.currentThread().getName() + " 访问结束,释放许可证");
        }
    }

    public static void main(String[] args) {
        RateLimiter rateLimiter = new RateLimiter();
        // 创建20个线程来模拟并发请求
        for (int i = 0; i < 20; i++) {
            Thread thread = new Thread(() -> {
                rateLimiter.accessResource();
            });
            thread.setName("Thread-" + i);
            thread.start();
        }
    }
}

在这个例子中,我们创建了一个 Semaphore,并设置了 10 个许可证。每个线程在访问 accessResource() 方法前都需要先调用 semaphore.acquire() 获取许可证。如果许可证不足,线程就会被阻塞,直到有其他线程释放许可证。在 finally 块中,我们调用 semaphore.release() 释放许可证,以便其他线程可以继续访问。

运行这段代码,你会发现,最多只有 10 个线程能够同时打印“正在访问资源...”,其他线程则需要等待。

如何根据业务需求设置限流策略?

上面的例子只是一个简单的演示。在实际应用中,我们需要根据业务需求来设置更精细的限流策略。

  1. 确定限流的粒度:

    • 接口级别: 对整个接口进行限流,这是最常见的限流方式。
    • 方法级别: 对某个具体的方法进行限流,适用于更细粒度的控制。
    • 用户级别: 对单个用户进行限流,防止某个用户占用过多资源。
    • IP 级别: 对单个 IP 地址进行限流,防止恶意攻击。
  2. 确定限流的指标:

    • QPS (Queries Per Second): 每秒请求数,这是最常用的限流指标。
    • TPS (Transactions Per Second): 每秒事务数,适用于事务性操作的限流。
    • 并发数: 同时处理的请求数,适用于控制系统的并发压力。
  3. 确定许可证的数量:

    许可证的数量需要根据系统的实际承载能力来确定。可以通过压力测试来模拟不同的并发量,观察系统的性能指标,如 CPU 使用率、内存使用率、响应时间等,找到一个合适的阈值。

    一般来说,许可证的数量可以设置为系统能够处理的最大并发数。如果许可证数量设置过小,会导致系统资源利用率不足;如果许可证数量设置过大,则起不到限流的作用。

  4. 选择公平性策略:

    在大多数情况下,非公平策略的性能会更好。因为公平策略需要维护一个等待队列,这会增加额外的开销。但是,如果你的业务场景对公平性有要求,比如需要保证先来的请求先被处理,那么就应该选择公平策略。

Semaphore 与其他限流技术的结合

Semaphore 通常不会单独使用,而是会与其他限流技术结合起来,实现更高级的限流功能。

  1. 与 Guava RateLimiter 结合:

    Guava RateLimiter 是 Google Guava 库提供的一个基于令牌桶算法的限流工具。它可以实现平滑的限流效果,避免突发流量对系统造成冲击。

    我们可以将 SemaphoreRateLimiter 结合起来使用,实现双重限流。Semaphore 用于控制并发数,RateLimiter 用于控制请求速率。

    // 假设 RateLimiter 限制每秒100个请求,Semaphore限制并发数为50
    RateLimiter rateLimiter = RateLimiter.create(100);
    Semaphore semaphore = new Semaphore(50);
    
    public void accessResource() {
        if (rateLimiter.tryAcquire()) { // 先尝试获取令牌
            try {
                semaphore.acquire(); // 再获取许可证
                // ... 执行业务逻辑 ...
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                semaphore.release(); // 释放许可证
            }
        } else {
            // 请求被限流
            System.out.println("请求过于频繁,请稍后再试");
        }
    }
    
  2. 与 Hystrix 结合:

    Hystrix 是 Netflix 开源的一个用于处理分布式系统延迟和容错的库。它提供了熔断、降级、限流等功能。

    Hystrix 内部也使用了 Semaphore 来实现限流。我们可以直接使用 Hystrix 的限流功能,而不需要自己手动创建 Semaphore

     @HystrixCommand(fallbackMethod = "fallback",
            commandProperties = {
                    @HystrixProperty(name = "execution.isolation.strategy", value = "SEMAPHORE"),
                    @HystrixProperty(name = "execution.isolation.semaphore.maxConcurrentRequests", value = "10")
            })
    public String doSomething() {
        // ... 执行业务逻辑 ...
    }
    
    public String fallback() {
        return "服务繁忙,请稍后再试";
    }
    
  3. 与 Sentinel结合:
    Sentinel 是阿里巴巴开源的一款流量控制、熔断降级组件。与Hystrix相比,Sentinel 提供了更全面的限流、熔断、降级、系统负载保护等功能,并且支持可视化配置和动态规则更新。

    Sentinel 同样可以使用信号量进行限流,但更多的是通过配置规则来实现。 你可以在Sentinel的控制台中配置QPS阈值、线程数阈值等,而无需手动编写代码。

注意事项

在使用 Semaphore 时,需要注意以下几点:

  • 避免死锁: 如果多个线程之间存在循环依赖关系,并且都使用了 Semaphore,就可能会发生死锁。例如,线程 A 持有许可证 1,等待许可证 2;线程 B 持有许可证 2,等待许可证 1。这种情况下,两个线程都会无限期地等待下去,导致死锁。

    为了避免死锁,需要仔细设计线程之间的协作关系,避免循环依赖。可以使用 tryAcquire() 方法来尝试获取许可证,如果获取失败,则立即释放已经持有的许可证,避免长时间占用资源。

  • 及时释放许可证: 无论程序是否发生异常,都应该在 finally 块中释放许可证,确保许可证能够被正确地回收。否则,可能会导致许可证泄漏,最终耗尽所有许可证,导致系统无法正常工作。

  • 合理设置超时时间: 在调用 acquire() 方法时,可以设置一个超时时间。如果线程在指定时间内没有获取到许可证,就会放弃等待,避免长时间阻塞。可以使用 acquire(long timeout, TimeUnit unit) 方法来设置超时时间。

总结

“领导,我已经用 Semaphore 对系统进行了限流改造,现在系统稳定多了!”

“干得不错,小王!晚上请你吃饭!”

Semaphore 是 Java 并发编程中一个非常实用的工具,可以帮助我们轻松实现限流功能,保护系统免受高并发流量的冲击。掌握 Semaphore 的使用方法,是每个 Java 开发者进阶的必备技能。希望通过本文的介绍,你能对 Semaphore 有一个更深入的理解,并在实际开发中灵活运用。

记住,限流只是系统保护的一种手段,我们还需要结合其他的技术,如熔断、降级、负载均衡等,才能构建一个真正稳定、可靠的高并发系统。

如果你觉得这篇文章对你有帮助,请点赞、收藏、转发,让更多的朋友看到!

点评评价

captcha
健康