JUC系列之《并发流程控制大师:Semaphore》

深入解析Semaphore并发控制

简介: Semaphore是JUC中用于控制并发访问资源数量的工具,通过“许可证”机制限制同时访问特定资源的线程数,适用于数据库连接池、限流等场景,具备公平与非公平模式,是高效管理资源并发的安全利器。(239字)

在多线程的并行世界里,我们常常会遇到这样的需求:控制同时访问某个特定资源的线程数量。比如,只有10个数据库连接,却有100个线程需要访问;或者只有3个停车位,却有10辆车要停。这时,我们就需要一位高效的“交通协管员”来维持秩序,防止资源被过度使用导致系统崩溃。今天的主角——Semaphore,正是JUC包中这位不可或缺的“协管员”。

目录

  • 引言
  • 什么是Semaphore?
  • 核心方法与工作原理
  • 从生活场景到代码实战
  • Semaphore的“公平”与“非公平”
  • 与其他JUC工具的简单对比
  • 总结与展望
  • 互动环节

引言

在Java并发编程中,解决线程安全问题的核心是管理线程对共享资源的访问。除了我们熟知的synchronized和Lock这类互斥锁(一次只允许一个线程访问)之外,Semaphore(信号量)提供了一种更灵活的并发控制模型:允许多个线程同时访问一个资源,但需要限制总数。它是基于AQS(
AbstractQueuedSynchronizer)构建的经典工具,理解它有助于我们更好地掌握JUC的设计思想。

什么是Semaphore?

Semaphore(信号量) 是一个计数器,用于控制访问共享资源的线程数量。它的核心是一个“许可证”(permit)概念。

你可以把它想象成一个:

  • 停车场门卫:停车场一共有N个车位(许可证)。来一辆车(线程),门卫就发放一个许可证,车辆入场。车位满了(许可证为0),新车就得在门口排队等待。走一辆车(线程释放),门卫收回一个许可证,并放行一辆等待的车。
  • 门票售票处:一场音乐会只有固定数量的门票(许可证)。卖光了,后续的观众就得等待有人退票(释放)后才能买到。

在构造Semaphore时,你需要指定许可证的数量。

// 创建一个拥有 10 个许可证的信号量
Semaphore semaphore = new Semaphore(10);

核心方法与工作原理

Semaphore 的核心方法非常直观,主要围绕“获取”和“释放”许可证展开。

方法名

作用描述

是否阻塞

acquire()

获取1个许可证。如果获取不到,则当前线程被阻塞,直到有许可证可用或被中断。

acquire(int permits)

获取指定数量的许可证。

acquireUninterruptibly()

获取1个许可证,但在等待过程中不响应中断

tryAcquire()

尝试获取1个许可证,成功返回true,失败立即返回false,不阻塞

tryAcquire(long timeout, TimeUnit unit)

在指定的超时时间内尝试获取许可证,超时后返回false。

限时阻塞

release()

释放1个许可证,将其返还给信号量,从而可能唤醒一个等待的线程。

-

release(int permits)

释放指定数量的许可证。

-

availablePermits()

返回当前信号量中可用的许可证数量。

-

工作原理(为什么是“交通协管员”?)
当一个线程调用 acquire() 方法时,它是在向信号量申请一个“通行许可”。信号量会检查自己的计数器:

  • 如果计数器 > 0,意味着还有空位,线程成功获取许可证,计数器减1,线程继续执行。
  • 如果计数器 == 0,意味着没有空位,线程会被放入一个FIFO队列中挂起等待(排队),直到有其他线程调用 release() 释放许可证(计数器加1)后,再唤醒队列中的线程来获取。

从生活场景到代码实战

让我们用经典的“停车场”例子来写一段代码。

场景:一个只有5个车位的停车场,模拟10辆汽车试图停入的情况。

import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
public class SemaphoreDemo {
    // 模拟5个停车位
    private static final int PARKING_SPACES = 5;
    // 创建信号量,许可证数=5,且设置为公平模式
    private static final Semaphore PARKING_SEMAPHORE = new Semaphore(PARKING_SPACES, true);
    public static void main(String[] args) {
        // 模拟10辆汽车
        for (int carNum = 1; carNum <= 10; carNum++) {
            new Thread(new CarDriver(carNum), "Car-" + carNum).start();
            // 让车辆稍微错开时间到达
            try { TimeUnit.MILLISECONDS.sleep(100); } catch (InterruptedException e) {}
        }
    }
    static class CarDriver implements Runnable {
        private final int carNum;
        public CarDriver(int carNum) {
            this.carNum = carNum;
        }
        @Override
        public void run() {
            System.out.printf("[%s] 到达停车场,寻找车位...%n", Thread.currentThread().getName());
            try {
                // 尝试在3秒内获取停车许可
                if (PARKING_SEMAPHORE.tryAcquire(3, TimeUnit.SECONDS)) {
                    // 成功获取许可证,停车成功
                    System.out.printf("[%s] ✅ 车辆%d停入车位,剩余车位:%d%n",
                            Thread.currentThread().getName(),
                            carNum,
                            PARKING_SEMAPHORE.availablePermits());
                    // 模拟车辆在车位停放一段随机时间
                    TimeUnit.SECONDS.sleep((long) (Math.random() * 10 + 1));
                    // 车辆开走,释放许可证
                    System.out.printf("[%s]  车辆%d驶离车位!%n", Thread.currentThread().getName(), carNum);
                } else {
                    // 超时未获取到许可证,放弃等待,开走了
                    System.out.printf("[%s] ❌ 车辆%d等了3秒没车位,直接开走了!%n", Thread.currentThread().getName(), carNum);
                    return;
                }
            } catch (InterruptedException e) {
                System.out.printf("[%s] 被中断了%n", Thread.currentThread().getName());
                Thread.currentThread().interrupt();
            } finally {
                // 非常重要:无论以何种方式离开,最终都要释放许可证!
                PARKING_SEMAPHORE.release();
            }
        }
    }
}

代码解读与关键点

  1. tryAcquire 与超时:我们使用了带超时参数的 tryAcquire,这在实际应用中非常有用。它避免了线程无限期阻塞,给了我们做降级处理(比如提示用户“等待超时”)的机会。
  2. finally 块中的 release:这是使用 Semaphore 的最佳实践和铁律!必须将 release() 操作放在 finally 块中,确保无论线程是正常执行完毕还是异常退出,都能释放占用的许可证,防止“许可证泄露”,否则会导致等待的线程永远等不到许可。
  3. 输出结果:运行程序,你会看到类似“Car-1停入,剩余4” -> “Car-6停入,剩余0” -> “Car-7等了3秒没车位,开走了” -> “Car-1驶离,Car-8停入”这样的日志,完美模拟了停车场饱和与调度的过程。

Semaphore的“公平”与“非公平”

在创建 Semaphore 时,第二个构造参数用于指定是否使用公平模式(Fairness)。

  • new Semaphore(5, **true**):公平模式。线程获取许可证的顺序严格遵循FIFO(先来先服务)的原则,即等待时间最长的线程会优先获得许可证。这避免了线程饥饿,但性能开销稍大。
  • new Semaphore(5, **false**):非公平模式(默认)。允许“插队”。当一个线程释放许可证时,信号量会直接尝试将许可证分配给任何一个正在等待的线程,而不是严格按顺序。这可能会提高整体的吞吐量,但可能导致某些线程等待时间过长(饥饿)。

选择哪种模式取决于你的业务场景对公平性和性能的权衡。

与其他JUC工具的简单对比

为了更好地理解 Semaphore,我们简单对比一下其他同步工具:

工具

核心目的

与Semaphore的异同点

CountDownLatch

等待一组事件发生后再继续执行(一次性)

不同。CountDownLatch 是“减法计数器”,不能重置,用于等待;Semaphore 是“可增减的许可证池”,可循环使用,用于控制访问。

CyclicBarrier

让一组线程相互等待,到达一个公共屏障点后再继续执行(可循环)

不同。CyclicBarrier 是“人齐了才开会”,所有线程是对等关系,相互等待;Semaphore 是“有票才能进”,线程与信号量交互,线程间不直接交互。

总结与展望

Semaphore 是JUC包中一个强大而灵活的并发控制工具。它通过“许可证”机制,优雅地解决了限制资源并发访问数的经典问题。掌握它的核心要点:

  1. 初始化:确定资源数量(许可证数)。
  2. 访问前:调用 acquire() 或 tryAcquire() 获取许可。
  3. 访问后务必在 finally 块中调用 release() 释放许可。
  4. 模式选择:根据场景权衡公平与非公平模式。

展望未来,Semaphore 的思想不仅在Java中,在各种分布式系统、限流组件(如Sentinel、Hystrix)中也随处可见。理解了这个单机版的“协管员”,将为你在更复杂的分布式资源管理领域打下坚实的基础。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一枚后端工程狮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值