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

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

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

目录

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

引言

在Java并发编程中,解决线程安全问题的核心是管理线程对共享资源的访问。除了我们熟知的synchronizedLock这类互斥锁(一次只允许一个线程访问)之外,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月前
|
Arthas 缓存 监控
深入理解JVM最后一章《常见问题排查思路与调优案例 - 综合实战》
本文系统讲解JVM性能调优的哲学与方法论,强调避免盲目调优。提出三大原则:测量优于猜测、权衡吞吐量/延迟/内存、由上至下排查问题,并结合CPU高、OOM、GC频繁等典型场景,提供标准化排查流程与实战案例,助力科学诊断与优化Java应用性能。
|
10月前
|
Web App开发 安全 Java
并发编程之《彻底搞懂Java线程》
本文系统讲解Java并发编程核心知识,涵盖线程概念、创建方式、线程安全、JUC工具集(线程池、并发集合、同步辅助类)及原子类原理,帮助开发者构建完整的并发知识体系。
|
10月前
|
监控 Java API
JUC系列之《深入剖析LockSupport:Java并发编程的“交警”》
LockSupport是Java并发编程的底层基石,提供park()和unpark()方法实现线程阻塞与精确唤醒。基于“许可证”机制,无需同步块、调用顺序灵活、可精准控制线程,是ReentrantLock、CountDownLatch等高级同步工具的底层支撑,堪称JUC的“手术刀”。
|
10月前
|
安全 Java API
JUC系列之《揭秘Java中的“瑞士军刀”——Unsafe类》
本文深入解析Java中的“后门”类sun.misc.Unsafe,揭秘其如何支撑AtomicInteger、AQS等并发工具的底层实现。涵盖其核心能力如CAS、内存操作、线程调度,剖析为何强大却危险,并介绍Java 9后的安全替代方案VarHandle,助你理解高性能并发背后的原理。
|
10月前
|
存储 算法 搜索推荐
《数据之美》:Java数据结构与算法精要
本系列深入探讨数据结构与算法的核心原理及Java实现,涵盖线性与非线性结构、常用算法分类、复杂度分析及集合框架应用,助你提升程序效率,掌握编程底层逻辑。
|
10月前
|
消息中间件 监控 Java
《聊聊线程池中线程数量》:不多不少,刚刚好的艺术
本文深入探讨Java线程池的核心参数与线程数配置策略,结合CPU密集型与I/O密集型任务特点,提供理论公式与实战示例,帮助开发者科学设定线程数,提升系统性能。
|
10月前
|
存储 缓存 安全
JUC系列之《volatile关键字:穿透Java内存模型的可见性之剑》
本文深入解析Java中的`volatile`关键字,涵盖其核心特性(可见性与有序性)、底层原理(JMM与内存屏障)、典型使用场景(状态标志、单例模式)及局限性(不保证原子性),帮助开发者正确掌握这一轻量级同步工具,避免并发编程误区。
|
10月前
|
缓存 负载均衡 并行计算
JUC系列之《ForkJoinPool:分而治之的并发编程艺术 》
本文深入解析Java并发编程利器ForkJoinPool,涵盖分治思想、工作窃取算法、核心架构及实战应用。通过数组求和与文件处理案例,详解任务拆分与合并技巧,并剖析其高性能背后的双端队列与负载均衡机制,助你掌握并行计算最佳实践。
|
10月前
|
存储 安全 Java
《Java并发编程的“避坑”利器:ThreadLocal深度解析》
ThreadLocal通过“空间换安全”实现线程变量隔离,为每个线程提供独立副本,避免共享冲突。本文深入解析其原理、ThreadLocalMap机制、内存泄漏风险及remove()最佳实践,助你掌握上下文传递与线程封闭核心技术。
|
10月前
|
存储 缓存 Java
【深入浅出】揭秘Java内存模型(JMM):并发编程的基石
本文深入解析Java内存模型(JMM),揭示synchronized与volatile的底层原理,剖析主内存与工作内存、可见性、有序性等核心概念,助你理解并发编程三大难题及Happens-Before、内存屏障等解决方案,掌握多线程编程基石。