Java并发包中Semaphore的工作原理、源码分析及使用示例

📅 发布时间:2026/8/12 12:27:33
Java并发包中Semaphore的工作原理、源码分析及使用示例 简介在多线程程序设计中有三个重要的同步工具需要我们掌握Semaphore信号量、CountDownLatch倒计数门闸锁和 CyclicBarrier可重用栅栏。本文将重点介绍信号量 Semaphore 的原理、源码分析以及使用示例。1. 信号量 Semaphore 的介绍我们以一个停车场运作为例来说明信号量的作用。假设停车场只有三个车位一开始三个车位都是空的。这时如果同时来了三辆车看门人允许它们进入然后放下车拦。以后来的车必须在入口等待直到停车场中有车辆离开。这时如果有一辆车离开停车场看门人得知后打开车拦放入一辆如果又离开一辆则又可以放入一辆如此往复。在这个停车场系统中车位是公共资源每辆车好比一个线程看门人起的就是信号量的作用。信号量是一个非负整数表示当前公共资源的可用数目在上面的例子中可以用空闲的停车位类比信号量。当一个线程要使用公共资源时在上面的例子中可以用车辆类比线程首先要查看信号量如果信号量的值大于 0则将其减 1然后去占有公共资源。如果信号量的值为 0则线程会将自己阻塞直到有其它线程释放公共资源。在信号量上我们定义两种操作acquire获取和release释放。当一个线程调用 acquire 操作时它要么成功获取信号量信号量减 1要么一直等待直到有线程释放信号量或超时。release 操作会将信号量的值加 1然后唤醒等待的线程。信号量主要用于两个目的用于多个共享资源的互斥使用。用于并发线程数的控制。2. 信号量 Semaphore 的源码分析在 Java 的并发包中Semaphore类表示信号量。Semaphore 内部主要通过 AQSAbstractQueuedSynchronizer实现线程的管理。2.1 构造函数Semaphore 有两个构造函数参数permits表示许可数它最后传递给了 AQS 的state值。// 非公平的构造函数 public Semaphore(int permits) { sync new NonfairSync(permits); } // 通过 fair 参数决定公平性 public Semaphore(int permits, boolean fair) { sync fair ? new FairSync(permits) : new NonfairSync(permits); }线程在运行时首先获取许可如果成功许可数就减 1线程运行当线程运行结束就释放许可许可数就加 1。如果许可数为 0则获取失败线程位于 AQS 的等待队列中它会被其它释放许可的线程唤醒。在创建 Semaphore 对象的时候还可以指定它的公平性非公平信号量在获取许可时先尝试获取许可而不必关心是否已有需要获取许可的线程位于等待队列中如果获取失败才会入列。公平信号量在获取许可时首先要查看等待队列中是否已有线程如果有则入列。2.2 acquire 方法public void acquire() throws InterruptedException { sync.acquireSharedInterruptibly(1); } public final void acquireSharedInterruptibly(int arg) throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); if (tryAcquireShared(arg) 0) doAcquireSharedInterruptibly(arg); } final int nonfairTryAcquireShared(int acquires) { for (;;) { int available getState(); int remaining available - acquires; if (remaining 0 || compareAndSetState(available, remaining)) return remaining; } }可以看出如果remaining 0即获取许可后许可数小于 0则获取失败在doAcquireSharedInterruptibly方法中线程会将自身阻塞然后入列。2.3 release 方法public void release() { sync.releaseShared(1); } public final boolean releaseShared(int arg) { if (tryReleaseShared(arg)) { doReleaseShared(); return true; } return false; } protected final boolean tryReleaseShared(int releases) { for (;;) { int current getState(); int next current releases; if (next current) // overflow throw new Error(Maximum permit count exceeded); if (compareAndSetState(current, next)) return true; } }可以看出释放许可就是将 AQS 中state的值加 1然后通过doReleaseShared唤醒等待队列的第一个节点。Semaphore 使用的是 AQS 的共享模式等待队列中的第一个节点如果成功获取许可又会唤醒下一个节点以此类推。3. 使用示例下面是一个使用 Semaphore 控制并发线程数的示例package javalearning; import java.util.Random; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Semaphore; public class SemaphoreDemo { // 创建一个信号量初始许可数为3表示最多允许3个线程同时访问共享资源 private Semaphore smp new Semaphore(3); private Random rnd new Random(); class TaskDemo implements Runnable { private String id; TaskDemo(String id) { this.id id; } Override public void run() { try { // acquire() 方法尝试获取一个许可 // 1. 如果当前有可用许可state 0则获取成功state减1线程继续执行 // 2. 如果当前没有可用许可state 0则线程会被阻塞进入AQS等待队列 // 3. 该方法会响应中断如果线程在等待时被中断会抛出InterruptedException // 4. 注意acquire() 是阻塞方法会一直等待直到获取到许可或被中断 smp.acquire(); System.out.println(Thread id is working); // 模拟线程执行任务随机休眠0-999毫秒 Thread.sleep(rnd.nextInt(1000)); // release() 方法释放一个许可 // 1. 将信号量的许可数加1state加1 // 2. 如果有线程在等待队列中会唤醒其中一个线程 // 3. 注意release() 方法不会阻塞总是立即返回 // 4. 重要每个acquire()调用都应该有对应的release()调用否则会导致许可泄漏 smp.release(); System.out.println(Thread id is over); } catch (InterruptedException e) { // 异常处理块处理线程中断异常 // 1. 当线程在acquire()等待时被中断会进入这个catch块 // 2. 这里应该进行适当的清理工作比如释放已获取的资源 // 3. 注意在捕获InterruptedException后通常需要恢复中断状态 // Thread.currentThread().interrupt(); // 4. 当前代码只是简单忽略中断实际项目中应根据业务需求处理 // 5. 如果线程在sleep()时被中断也会进入这个catch块 } } } public static void main(String[] args) { SemaphoreDemo semaphoreDemo new SemaphoreDemo(); // 创建缓存线程池会自动管理线程的创建和回收 ExecutorService se Executors.newCachedThreadPool(); // 提交6个任务到线程池但信号量只允许3个线程同时执行 se.submit(semaphoreDemo.new TaskDemo(a)); se.submit(semaphoreDemo.new TaskDemo(b)); se.submit(semaphoreDemo.new TaskDemo(c)); se.submit(semaphoreDemo.new TaskDemo(d)); se.submit(semaphoreDemo.new TaskDemo(e)); se.submit(semaphoreDemo.new TaskDemo(f)); // 关闭线程池不再接受新任务但会执行已提交的任务 se.shutdown(); // 注意这里没有调用se.awaitTermination()等待所有任务完成 // 在实际应用中可能需要等待所有任务完成后再结束程序 } }3.1 运行结果Thread c is working Thread b is working Thread a is working Thread c is over Thread d is working Thread b is over Thread e is working Thread a is over Thread f is working Thread d is over Thread e is over Thread f is over可以看出最多同时有三个线程并发执行也可以认为有三个公共资源比如计算机的三个串口。4. Semaphore 与 CountDownLatch、CyclicBarrier 的对比在 Java 并发编程中Semaphore、CountDownLatch 和 CyclicBarrier 都是重要的同步工具它们各有不同的设计目的和使用场景。下面通过表格从多个维度进行对比对比维度Semaphore信号量CountDownLatch倒计数门闸锁CyclicBarrier可重用栅栏设计目的控制同时访问特定资源的线程数量管理有限资源的并发访问。让一个或多个线程等待其他线程完成操作实现线程间的协调。让一组线程相互等待直到所有线程都到达某个屏障点然后同时继续执行。核心机制基于许可permits的计数器acquire() 获取许可计数器减1release() 释放许可计数器加1。基于倒计数的计数器countDown() 减少计数await() 等待计数归零。基于屏障barrier的等待机制await() 使线程等待直到所有线程都调用了 await()。可重用性可重用许可被释放后可被其他线程获取。不可重用计数归零后门闸打开无法重置除非新建实例。可重用所有线程到达屏障后自动重置可再次使用。主要方法acquire()、release()、tryAcquire()、availablePermits()await()、countDown()、getCount()await()、reset()、getNumberWaiting()、getParties()典型场景数据库连接池限流器Rate Limiter生产者-消费者模型资源池管理主线程等待多个子线程初始化完成并行计算任务等待所有子任务完成启动服务时等待所有组件就绪多阶段并行计算每阶段需要所有线程同步模拟多玩家游戏的回合制同步数据分片处理等待所有分片处理完成线程关系通常用于限制资源访问的线程数量线程之间是竞争关系。一个或多个等待线程与一组工作线程之间的协调关系。一组对等线程之间的相互等待关系。计数器方向许可数可增可减acquire() 减少release() 增加。计数器只减不增从初始值递减到0。无计数器概念基于到达屏障的线程数。异常处理acquire() 可响应中断release() 不会阻塞。await() 可响应中断和超时。await() 可响应中断、超时和屏障破坏异常。总结与适用场景1.Semaphore最适合需要控制并发访问数量的场景。当你有有限数量的资源如数据库连接、文件句柄、API调用配额需要被多个线程共享时Semaphore 可以确保同时访问资源的线程数不超过预设限制。它的核心思想是资源配额管理。2.CountDownLatch最适合一次性协调场景。当你需要让一个或多个线程等待其他一组线程完成特定操作后才能继续执行时CountDownLatch 是最佳选择。例如主线程等待所有服务初始化完成或者测试框架等待所有测试用例执行完毕。它的核心思想是等待完成。3.CyclicBarrier最适合多阶段同步场景。当一组线程需要相互等待在所有线程都到达某个点后才能继续执行下一阶段时CyclicBarrier 非常有用。例如并行计算中每轮迭代需要所有线程同步或者多玩家游戏中每回合开始前等待所有玩家准备就绪。它的核心思想是集体同步。选择建议需要限制资源访问数量 → 选择 Semaphore需要等待其他线程完成 → 选择 CountDownLatch需要线程组相互等待同步 → 选择 CyclicBarrier在实际项目中这三种工具可以结合使用解决复杂的并发同步问题。理解它们的设计哲学和适用场景有助于编写更高效、更安全的并发程序。5. 信号量的典型应用场景信号量在实际开发中有多种应用场景以下是几个典型的例子5.1 数据库连接池数据库连接是有限的资源创建和维护连接需要消耗系统资源。使用信号量可以有效地管理数据库连接池初始化时创建固定数量的连接并将信号量的许可数设置为连接数。当线程需要获取数据库连接时调用acquire()方法获取许可。如果连接池中有可用连接线程立即获得连接并开始操作。如果所有连接都被占用线程会阻塞等待直到有线程释放连接调用release()。线程使用完连接后必须调用release()方法归还许可以便其他线程可以使用。这种方式可以防止过多的线程同时访问数据库避免数据库过载同时确保连接资源被高效复用。5.2 限流器Rate Limiter在高并发系统中为了防止系统被突发流量冲垮需要对请求进行限流设置信号量的许可数为系统能够承受的最大并发请求数。每个请求到达时首先尝试获取许可tryAcquire()或带超时的acquire()。如果获取成功请求被处理如果获取失败许可数为0请求被拒绝或进入等待队列。请求处理完成后释放许可允许新的请求进入。通过调整信号量的许可数可以灵活控制系统的并发处理能力保护后端服务不被过载。5.3 生产者-消费者模型在生产者-消费者模式中信号量可以用于控制缓冲区的访问使用两个信号量emptySlots空槽位信号量和fullSlots满槽位信号量。emptySlots初始值为缓冲区大小表示可用空槽位数量。fullSlots初始值为0表示已填充的槽位数量。生产者线程先获取空槽位许可emptySlots.acquire()生产数据放入缓冲区然后释放满槽位许可fullSlots.release()。消费者线程先获取满槽位许可fullSlots.acquire()从缓冲区取出数据消费然后释放空槽位许可emptySlots.release()。这种实现方式确保了生产者和消费者之间的同步避免了缓冲区溢出或下溢的问题。5.4 资源池管理除了数据库连接池信号量还可以用于管理其他类型的资源池线程池任务队列控制限制同时等待执行的任务数量。文件句柄管理限制同时打开的文件数量防止系统文件描述符耗尽。网络连接限制控制同时建立的网络连接数。硬件设备访问如打印机、扫描仪等共享设备的访问控制。5.5 并发任务控制在某些场景下需要限制同时执行的特定类型任务数量批量数据处理控制同时处理的数据分片数量避免内存溢出。API调用限制遵守第三方API的调用频率限制。下载任务管理限制同时进行的下载任务数量避免网络拥堵。信号量的灵活性和简单性使其成为并发编程中不可或缺的工具。通过合理设置许可数量和选择合适的获取/释放策略可以解决多种并发控制问题。6. 使用 Semaphore 的注意事项与最佳实践虽然 Semaphore 是一个强大的并发控制工具但在实际使用中需要注意一些常见的陷阱和最佳实践以确保程序的正确性和性能。6.1 许可泄漏acquire 后未 release问题描述许可泄漏是最常见的问题之一。当线程调用acquire()获取许可后如果因为异常、逻辑错误或忘记调用release()导致许可没有被释放那么可用的许可数会逐渐减少最终可能导致所有线程都无法获取许可而永久阻塞。最佳实践使用 try-finally 块确保release()一定会被调用。public class SemaphoreSafeDemo { private Semaphore semaphore new Semaphore(3); public void doWork() { try { semaphore.acquire(); // 执行业务逻辑 performTask(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); // 恢复中断状态 // 处理中断逻辑 } finally { semaphore.release(); // 确保释放许可 } } private void performTask() { // 模拟业务逻辑 } }6.2 异常处理不当导致许可未释放问题描述当业务逻辑抛出未捕获的异常时如果release()调用在异常之后许可可能无法被释放。最佳实践将业务逻辑放在 try 块中在 finally 块中释放许可。对于可中断的方法要正确处理 InterruptedException。public class ExceptionSafeDemo { private Semaphore semaphore new Semaphore(2); public void processResource() { semaphore.acquireUninterruptibly(); // 使用不可中断的获取方式 try { // 可能抛出异常的业务逻辑 riskyOperation(); } finally { semaphore.release(); } } public void processWithTimeout() throws InterruptedException { if (semaphore.tryAcquire(1, TimeUnit.SECONDS)) { // 带超时的尝试获取 try { // 业务逻辑 doWork(); } finally { semaphore.release(); } } else { // 超时处理逻辑 handleTimeout(); } } private void riskyOperation() { // 可能抛出 RuntimeException 的操作 } private void doWork() { // 正常业务逻辑 } private void handleTimeout() { // 超时处理 } }6.3 tryAcquire 与 acquire 的选择策略选择建议使用acquire()的场景当线程必须获取到许可才能继续执行时使用阻塞式的acquire()。例如数据库连接池中线程必须等待直到有可用连接。使用tryAcquire()的场景当获取许可是可选的或者需要快速失败时。例如限流器中当系统过载时直接拒绝请求而不是让请求等待。使用带超时的tryAcquire(long timeout, TimeUnit unit)的场景当需要限制等待时间避免线程无限期阻塞时。public class AcquisitionStrategyDemo { private Semaphore semaphore new Semaphore(5); // 场景1必须获取许可 - 使用 acquire() public void mustAcquire() throws InterruptedException { semaphore.acquire(); try { criticalOperation(); } finally { semaphore.release(); } } // 场景2快速失败 - 使用 tryAcquire() public boolean tryAcquireFast() { if (semaphore.tryAcquire()) { try { optionalOperation(); return true; } finally { semaphore.release(); } } return false; // 立即返回不阻塞 } // 场景3有限等待 - 使用带超时的 tryAcquire() public boolean tryAcquireWithTimeout() throws InterruptedException { if (semaphore.tryAcquire(500, TimeUnit.MILLISECONDS)) { try { timeSensitiveOperation(); return true; } finally { semaphore.release(); } } return false; // 超时后返回 } private void criticalOperation() { // 必须执行的操作 } private void optionalOperation() { // 可选的操作 } private void timeSensitiveOperation() { // 对时间敏感的操作 } }6.4 公平与非公平模式对性能的影响公平模式FairSync特点严格按照线程等待的先后顺序分配许可先到先得。优点避免线程饥饿保证公平性。缺点性能较低因为需要维护等待队列上下文切换开销大。适用场景当避免线程饥饿比性能更重要时或者当等待时间可能很长时。非公平模式NonfairSync默认特点允许新请求的线程插队可能比等待队列中的线程先获取许可。优点性能更高减少了线程切换的开销。缺点可能导致线程饥饿某些线程可能长时间无法获取许可。适用场景大多数情况下的默认选择特别是当许可持有时间很短或线程竞争不激烈时。public class FairnessDemo { // 公平信号量 - 性能较低但保证公平 private Semaphore fairSemaphore new Semaphore(3, true); // 非公平信号量默认- 性能较高但可能不公平 private Semaphore nonFairSemaphore new Semaphore(3, false); // 或简写为private Semaphore nonFairSemaphore new Semaphore(3); public void testFairSemaphore() throws InterruptedException { fairSemaphore.acquire(); try { // 公平模式下等待时间最长的线程优先获取许可 System.out.println(Fair semaphore acquired by: Thread.currentThread().getName()); Thread.sleep(100); } finally { fairSemaphore.release(); } } public void testNonFairSemaphore() throws InterruptedException { nonFairSemaphore.acquire(); try { // 非公平模式下新请求的线程可能插队 System.out.println(Non-fair semaphore acquired by: Thread.currentThread().getName()); Thread.sleep(100); } finally { nonFairSemaphore.release(); } } }6.5 其他注意事项1. 避免在持有许可时执行耗时操作// 不推荐在持有许可时执行耗时IO操作 semaphore.acquire(); try { // 耗时操作 - 这会长时间占用许可 processLargeFile(); // 可能执行几分钟 sendNetworkRequest(); // 网络延迟不可控 } finally { semaphore.release(); } // 推荐尽快释放许可 ListString data prepareData(); // 准备数据不持有许可 semaphore.acquire(); try { // 只执行必须同步的核心操作 updateSharedResource(data); } finally { semaphore.release(); // 尽快释放 } // 后续的非同步操作在释放许可后执行 logOperationResult();2. 合理设置许可数量设置过少可能导致性能瓶颈线程频繁等待。设置过多失去并发控制的意义可能耗尽系统资源。建议根据系统资源CPU核心数、内存、IO能力和业务需求动态调整。3. 监控信号量状态public class SemaphoreMonitor { private Semaphore semaphore new Semaphore(10); public void printStatus() { System.out.println(可用许可数: semaphore.availablePermits()); System.out.println(等待队列长度估算: semaphore.getQueueLength()); System.out.println(是否有线程正在等待: semaphore.hasQueuedThreads()); } public void acquireWithLogging() throws InterruptedException { System.out.println(尝试获取许可当前可用: semaphore.availablePermits()); semaphore.acquire(); System.out.println(成功获取许可剩余可用: semaphore.availablePermits()); } }4. 避免嵌套获取// 危险可能导致死锁 semaphore.acquire(); try { // 某些条件下再次获取同一个信号量 if (needMoreResource()) { semaphore.acquire(); // 如果许可数不足这里会永久阻塞 try { // ... } finally { semaphore.release(); } } } finally { semaphore.release(); } // 解决方案使用可重入锁或其他同步机制 private ReentrantLock lock new ReentrantLock(); public void safeNestedAccess() { lock.lock(); try { if (needMoreResource()) { // 使用其他同步机制而不是嵌套获取同一个信号量 handleAdditionalResource(); } } finally { lock.unlock(); } }通过遵循这些最佳实践可以避免 Semaphore 使用中的常见陷阱编写出更健壮、高效的并发程序。7. 总结Semaphore 是 Java 并发编程中重要的同步工具通过控制许可数量来管理对共享资源的访问。本文通过停车场示例介绍了信号量的基本概念分析了 Semaphore 的源码实现并提供了完整的使用示例。掌握 Semaphore 的使用可以帮助我们更好地设计并发程序。