线程池以及信号量Semaphore的使用

在 Java 生产环境中,合理使用 线程池信号量(Semaphore) 对保障系统稳定性、防止资源耗尽、控制并发度至关重要。下面分别从 最佳实践配置建议典型应用场景 详细说明。

一、生产环境中的线程池使用

1. 不要直接使用 Executors 工具类

虽然 Executors.newFixedThreadPool() 等方法简单易用,但在生产环境中存在严重隐患:

方法问题
newFixedThreadPool / newSingleThreadExecutor使用无界队列 LinkedBlockingQueue,任务堆积可能导致 OOM
newCachedThreadPool最大线程数为 Integer.MAX_VALUE,高并发下可能创建大量线程,导致系统崩溃

✅ 明确禁止

  • 禁止使用 Executors 创建线程池
    原因:newFixedThreadPool 使用无界队列,newCachedThreadPool 允许无限线程,极易导致 OOM 或线程爆炸。

✅ 强制要求

  1. 必须通过 ThreadPoolExecutor 显式构造
  2. 必须指定有界队列(如 ArrayBlockingQueue 或带容量的 LinkedBlockingQueue
  3. 必须自定义 ThreadFactory,命名规范(如 xxx-pool-%d
  4. 必须设置合理的拒绝策略,并记录日志或告警
  5. 线程池需注册到统一监控平台(如 ARMS、鹰眼)

正确做法:使用 ThreadPoolExecutor 手动创建

ThreadPoolExecutor executor = new ThreadPoolExecutor(
    4,                          // corePoolSize:核心线程数
    8,                          // maximumPoolSize:最大线程数
    60L,                        // keepAliveTime:空闲线程存活时间
    TimeUnit.SECONDS,           // 时间单位
    new LinkedBlockingQueue<>(100), // 有界队列(关键!)
    new ThreadFactory() {       // 自定义线程工厂(便于排查问题)
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        @Override
        public Thread newThread(Runnable r) {
            Thread t = new Thread(r, "biz-pool-" + threadNumber.getAndIncrement());
            t.setDaemon(false);
            return t;
        }
    },
    new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用线程执行任务
);

2. 关键参数说明

参数建议
corePoolSize根据 CPU 核心数和任务类型(CPU 密集型 or IO 密集型)设定。IO 密集型可设为 2 * CPU 核数
maximumPoolSize不宜过大,避免线程过多导致上下文切换开销
workQueue必须使用有界队列(如 ArrayBlockingQueue 或带容量的 LinkedBlockingQueue
RejectedExecutionHandler推荐使用 CallerRunsPolicy(降级执行)或自定义拒绝策略(记录日志、告警等)

3. 监控与优雅关闭

  • 监控:通过 executor.getActiveCount()executor.getQueue().size() 等方法暴露指标(可接入 Prometheus)
  • 关闭
    executor.shutdown(); // 不再接受新任务
    try {
        if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
            executor.shutdownNow(); // 强制关闭
        }
    } catch (InterruptedException e) {
        executor.shutdownNow();
        Thread.currentThread().interrupt();
    }

二、生产环境中的信号量(Semaphore)使用

1. 适用场景

  • 控制并发访问数量(如最多 10 个线程同时调用某外部 API)
  • 资源池访问控制(如数据库连接池、文件句柄等)
  • 限流(轻量级,适用于单机限流)

⚠️ 注意:Semaphore单机限流,分布式场景需用 Redis + Lua 或 Sentinel 等方案。

2. 线程池 + Semaphore 联合使用示例

创建线程池

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicLong;

public class BoundedThreadPool {

    private final ThreadPoolExecutor executor;
    private final Semaphore semaphore;
    private final long acquireTimeoutSeconds;

    public BoundedThreadPool(int maxThreads, int queueCapacity, long acquireTimeoutSeconds) {
        this.acquireTimeoutSeconds = acquireTimeoutSeconds;
        // 总许可数 = 运行中线程 + 队列容量
        this.semaphore = new Semaphore(maxThreads + queueCapacity);
        this.executor = new ThreadPoolExecutor(
                maxThreads,
                maxThreads,
                60L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(queueCapacity),
                new ThreadFactory() {
                    private final AtomicLong count = new AtomicLong(0);
                    @Override
                    public Thread newThread(Runnable r) {
                        Thread t = new Thread(r, "biz-task-pool-" + count.getAndIncrement());
                        t.setDaemon(false);
                        return t;
                    }
                },
                new ThreadPoolExecutor.AbortPolicy() // 实际不会触发
        );
    }

    /**
     * 提交任务,支持返回结果和异常传播
     */
    public <T> CompletableFuture<T> submit(Callable<T> task) {
        CompletableFuture<T> future = new CompletableFuture<>();

        boolean acquired;
        try {
            // ⚠️ 关键:带超时获取许可,防止永久阻塞
            acquired = semaphore.tryAcquire(acquireTimeoutSeconds, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            future.completeExceptionally(new IllegalStateException("任务提交被中断", e));
            return future;
        }

        if (!acquired) {
            // 超时拒绝,返回明确错误
            future.completeExceptionally(
                    new RejectedExecutionException(
                            "系统繁忙,任务提交等待超时(" + acquireTimeoutSeconds + "秒),请稍后重试")
            );
            return future;
        }

        // 许可已获取,提交到线程池
        executor.execute(() -> {
            try {
                T result = task.call();
                future.complete(result);
            } catch (Throwable t) {
                future.completeExceptionally(t);
            } finally {
                // 无论成功/失败/异常,必须释放许可!
                semaphore.release();
            }
        });

        return future;
    }

    public void shutdown() {
        executor.shutdown();
        try {
            if (!executor.awaitTermination(10, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }
}

业务类

public class BusinessTask {

    private final String requestId;

    public BusinessTask(String requestId) {
        this.requestId = requestId;
    }

    /**
     * 按顺序执行 4 个业务场景,任一失败则整个任务终止
     */
    public String execute() throws Exception {
        System.out.println("[" + requestId + "] 开始执行任务");
        try {
            scene1();
            scene2();
            scene3();
            scene4();
        } catch (Exception e) {
            System.err.println("[" + requestId + "] 任务失败: " + e.getMessage());
            throw e; // 向上抛出,由 CompletableFuture 捕获
        }

        System.out.println("[" + requestId + "] 任务成功完成");
        return "SUCCESS";
    }

    private void scene1() throws Exception {
        System.out.println("[" + requestId + "] 执行场景1:数据校验");
        simulateWork(300);
        if (Math.random() < 0.1) throw new RuntimeException("场景1失败:参数非法");
    }

    private void scene2() throws Exception {
        System.out.println("[" + requestId + "] 执行场景2:调用风控服务");
        simulateWork(400);
        if (Math.random() < 0.1) throw new RuntimeException("场景2失败:风控拒绝");
    }

    private void scene3() throws Exception {
        System.out.println("[" + requestId + "] 执行场景3:生成订单");
        simulateWork(500);
        if (Math.random() < 0.1) throw new RuntimeException("场景3失败:库存不足");
    }

    private void scene4() throws Exception {
        System.out.println("[" + requestId + "] 执行场景4:通知用户");
        simulateWork(100);
        if (Math.random() < 0.1) throw new RuntimeException("场景4失败:短信服务异常");
    }

    private void simulateWork(long millis) throws InterruptedException {
        Thread.sleep(millis);
    }
}

测试类

import org.springframework.http.ResponseEntity;

import java.util.Map;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class TaskService {

    // 生产环境建议通过 @Bean 注入
    private static final BoundedThreadPool taskPool =
            new BoundedThreadPool(
                    10,          // 最大并发线程数
                    10,         // 队列容量
                    3            // 提交等待超时:3秒
            );

    public ResponseEntity<?> process(TaskRequest request) {
        String requestId = request.getRequestId();
        System.out.println("[" + requestId + "] 开始处理任务");
        if (requestId == null || requestId.trim().isEmpty()) {
            return ResponseEntity.badRequest().body(Map.of("error", "requestId 不能为空"));
        }

        // 提交任务
        CompletableFuture<String> future = taskPool.submit(() -> {
            BusinessTask task = new BusinessTask(requestId);
            return task.execute(); // 可能抛出业务异常
        });

        try {
            // ⚠️ 总任务执行超时:20秒(含排队+执行)
            String result = future.get(20, TimeUnit.SECONDS);
            return ResponseEntity.ok(Map.of(
                    "code", 200,
                    "message", "处理成功",
                    "result", result,
                    "requestId", requestId
            ));
        } catch (TimeoutException e) {
            return ResponseEntity.status(408).body(Map.of(
                    "code", 408,
                    "error", "任务处理超时(超过20秒)",
                    "requestId", requestId
            ));
        } catch (ExecutionException e) {
            // 获取原始业务异常
            Throwable cause = e.getCause();
            String errorMsg = cause != null ? cause.getMessage() : "未知系统错误";
            System.out.println("[" + requestId + "] 业务处理失败: " + errorMsg);
            return ResponseEntity.status(500).body(Map.of(
                    "code", 500,
                    "error", "业务处理失败: " + errorMsg,
                    "requestId", requestId
            ));
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            System.out.println("[" + requestId + "] 请求处理被中断");
            return ResponseEntity.status(503).body(Map.of(
                    "code", 503,
                    "error", "请求处理被中断",
                    "requestId", requestId
            ));
        }
    }

    // 优雅关闭(Spring Boot 中可用 @PreDestroy)
    // @PreDestroy
    public void destroy() {
        taskPool.shutdown();
    }

    public static void main(String[] args) {
        TaskService taskService = new TaskService();
        TaskRequest taskRequest = new TaskRequest();
        for (int i = 0; i < 1000; i++) {
            String requestId = "task-" + i;
            taskRequest.setRequestId(requestId);
            taskService.process(taskRequest);
        }
        taskService.destroy();
    }
}
public class TaskRequest {
    private String requestId;

    public String getRequestId() { return requestId; }
    public void setRequestId(String requestId) { this.requestId = requestId; }
}

执行结果如下:

三.Semaphore(信号量)和线程池(ThreadPoolExecutor)搭配使用,在 Java 并发编程中是一种精细化控制资源并发访问的高级组合策略。它们各自解决不同层面的问题,配合使用可以实现更安全、更灵活、更可控的并发模型。

一、各自职责回顾

组件职责控制粒度
线程池管理线程生命周期,复用线程,控制任务执行的并发线程数任务调度层面(有多少线程在跑)
Semaphore控制同时访问某类资源或执行某段逻辑的线程数量业务逻辑层面(有多少线程能进某个“门”)

✅ 简单说:

  • 线程池决定“能同时跑多少任务
  • Semaphore 决定“在这些任务中,最多多少个能同时做某件高成本的事

二、典型使用场景(为什么需要搭配?)

场景 1:线程池处理通用任务,但限制其中高成本操作的并发数

💡 例子:一个线程池处理 100 个用户请求,每个请求可能包含“调用第三方 OCR 服务”。OCR 接口有 QPS 限制(如最多 5 QPS),不能让所有线程同时调用。

// 线程池:处理所有请求
ThreadPoolExecutor executor = new ThreadPoolExecutor(10, 20, 60, SECONDS, new LinkedBlockingQueue<>(100));

// 信号量:限制 OCR 并发为 5
Semaphore ocrPermit = new Semaphore(5);

for (int i = 0; i < 100; i++) {
    executor.submit(() -> {
        // 1. 做一些本地处理(无限制)
        preprocess();

        // 2. 调用 OCR(受信号量保护)
        try {
            if (ocrPermit.tryAcquire(2, SECONDS)) {
                try {
                    callOCRApi(); // 受限操作
                } finally {
                    ocrPermit.release();
                }
            } else {
                log.warn("OCR request timeout, fallback...");
                useFallback();
            }
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }

        // 3. 后续处理
        postprocess();
    });
}

优势

  • 线程池可以高效处理大量任务
  • OCR 调用不会超限,避免被第三方限流或封禁
  • 超时未获取许可时可降级(如返回缓存结果)

场景 2:控制对稀缺本地资源的访问

💡 例子:应用需要写入同一个日志文件(或操作同一个硬件设备),但文件 I/O 或设备驱动只支持最多 3 个并发写入。

Semaphore fileWriteSemaphore = new Semaphore(3); // 最多3个线程同时写

executor.submit(() -> {
    try {
        fileWriteSemaphore.acquire();
        writeToFile(data);
    } finally {
        fileWriteSemaphore.release();
    }
});

即使线程池有 50 个线程,也不会导致文件写入冲突或设备过载。


场景 3:实现“分阶段”并发控制

某些任务分为多个阶段,不同阶段有不同的并发限制。

Semaphore phase1 = new Semaphore(10); // 阶段1:最多10并发
Semaphore phase2 = new Semaphore(3);  // 阶段2:最多3并发(如数据库写入)

executor.submit(() -> {
    // 阶段1:数据拉取
    phase1.acquire();
    fetchData();
    phase1.release();

    // 阶段2:数据入库
    phase2.acquire();
    saveToDB();
    phase2.release();
});

场景 4:防止线程池“虚假空闲”导致资源过载

有时线程池看似空闲(线程未满),但下游资源(如数据库连接)已耗尽。此时用 Semaphore 作为“资源许可证”更准确。

// 数据库连接池只有 8 个连接
Semaphore dbPermit = new Semaphore(8);

executor.submit(() -> {
    dbPermit.acquire();
    try (Connection conn = dataSource.getConnection()) { // 实际可能复用,此处仅为示意
        executeSQL(conn);
    } finally {
        dbPermit.release();
    }
});

这样即使线程池有 20 个线程,也不会申请超过 8 个 DB 连接。

三、搭配使用的核心价值

价值说明
解耦调度与资源控制线程池负责“任务怎么跑”,Semaphore 负责“资源怎么用”
精细化限流对特定操作限流,而非整个任务
避免资源过载保护第三方服务、本地文件、硬件设备等
支持降级与超时tryAcquire(timeout) 可实现优雅降级
提升系统稳定性防止单点资源成为系统瓶颈

四、注意事项

  1. release() 必须在 finally 中调用
    防止异常导致许可证泄漏,造成死锁。

  2. 避免 Semaphore 成为性能瓶颈
    如果许可数设置过小,会导致大量线程阻塞,反而降低吞吐。

  3. 不要用 Semaphore 替代线程池
    Semaphore 不管理线程生命周期,大量线程仍会创建(如用 new Thread().start() + Semaphore),浪费资源。

  4. 公平性考虑
    高并发下可考虑 new Semaphore(permits, true) 启用公平模式,避免线程饥饿。


五、总结

线程池 + Semaphore = “广度控制 + 深度控制”

  • 线程池:控制整体并发规模(宏观)
  • Semaphore:控制关键资源/操作的并发深度(微观)

在阿里、腾讯等大厂的高并发系统中,这种组合被广泛用于:

  • 第三方 API 调用限流
  • 数据库/缓存连接保护
  • 文件/设备 I/O 控制
  • 分布式任务中的本地资源协调

合理搭配,既能发挥线程池的高效调度能力,又能精准保护系统中的“脆弱环节”,是构建高可用 Java 应用的重要手段。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值