目录
[二、解决方案:Semaphore + 线程池](#二、解决方案:Semaphore + 线程池)
[1. 为什么用Semaphore而不是synchronized?](#1. 为什么用Semaphore而不是synchronized?)
[2. 为什么用CachedThreadPool?](#2. 为什么用CachedThreadPool?)
[3. 资源清理为什么重要?](#3. 资源清理为什么重要?)
一、问题背景
在物联网终端设备接入服务中,我们经常遇到这样的场景:
-
同一个设备(IMEI)会频繁发送不同类型的消息(登录、位置上报、心跳等)
-
同类型消息必须串行处理(如连续两个登录请求,必须等第一个处理完再处理第二个)
-
不同类型消息可以并行处理(如登录和位置上报可以同时进行)
如果用传统的单线程池方案,会导致所有消息排队,严重影响吞吐量;如果用普通线程池,又会出现并发冲突,导致数据错乱。
二、解决方案:Semaphore + 线程池
经过多次尝试,最终采用**Semaphore(信号量)**强制串行,彻底解决问题。
核心思想
-
为每个
(IMEI, Worker类型)分配一个Semaphore(许可证数量=1) -
任务执行前必须acquire() 获取许可证,执行后**release()**释放
-
同一个Key的任务自然会排队,不同Key的任务互不影响
完整代码实现
java
import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.*;
@Slf4j
public class ExecutorCache {
// IMEI+Worker类型 -> 信号量(保证串行)
private final static ConcurrentHashMap<String, Semaphore> workerLocks = new ConcurrentHashMap<>();
// IMEI+Worker类型 -> 线程池(负责执行任务)
private final static ConcurrentHashMap<String, ExecutorService> deviceExecutors = new ConcurrentHashMap<>();
/**
* 执行任务,保证同一个 IMEI+Worker 串行执行
*
* @param imei 设备ID
* @param workerType Worker类型(如 LoginWorker)
* @param task 需要执行的任务
*/
public static void executeSerial(String imei, String workerType, Runnable task) {
String lockKey = workerType + "-" + imei;
// 获取信号量(最多1个许可 = 互斥锁)
Semaphore semaphore = workerLocks.computeIfAbsent(lockKey, k -> new Semaphore(1));
// 获取线程池(缓存线程池,因为锁已经控制了并发)
ExecutorService executor = deviceExecutors.computeIfAbsent(lockKey,
k -> Executors.newCachedThreadPool(r -> new Thread(r, lockKey)));
// 提交任务到线程池
executor.submit(() -> {
try {
semaphore.acquire(); // 获取锁,如果没有许可则等待
task.run(); // 执行业务逻辑
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("任务被中断:{}", lockKey, e);
} finally {
semaphore.release(); // 释放锁
}
});
}
/**
* 清理设备的所有资源(设备断开时调用)
*/
public static void removeDevice(String imei) {
// 清理信号量
workerLocks.keySet().removeIf(key -> key.endsWith("-" + imei));
// 清理线程池
deviceExecutors.keySet().removeIf(key -> key.endsWith("-" + imei));
}
}
业务层调用
java
private void protocolTerminal(String imei, Channel channel, String cmd, String data) {
switch (cmd) {
case ProtocolConstant.TLOGIN:
TerminalChannelCache.putTempTerminalChannelMap(imei, channel);
ExecutorCache.executeSerial(imei, "LoginWorker",
() -> loginWorker.handle(data));
break;
case ProtocolConstant.TPOS:
ExecutorCache.executeSerial(imei, "locationWorker",
() -> locationWorker.handle(data));
break;
case ProtocolConstant.THEART:
ExecutorCache.executeSerial(imei, "terHeartWorker",
() -> terminalHeartWorker.handle(data));
break;
// ... 其他命令类型
}
}
三、方案优势
| 特性 | 传统方案 | Semaphore方案 |
|---|---|---|
| 同设备同类型串行 | ❌ 依赖线程池复用 | ✅ 强制串行 |
| 同设备不同类型并行 | ✅ 可以 | ✅ 可以 |
| 资源清理 | ⚠️ 容易内存泄漏 | ✅ 显式清理 |
| 调试难度 | 高 | 低 |
| 代码复杂度 | 中 | 低 |
四、关键技术点解析
1. 为什么用Semaphore而不是synchronized?
-
synchronized会阻塞当前线程(这里是Netty的IO线程),影响其他设备 -
Semaphore配合独立线程池,将等待放到业务线程池中,IO线程快速返回
2. 为什么用CachedThreadPool?
-
每个
(IMEI, Worker类型)的任务量不大 -
CachedThreadPool 空闲线程60秒自动回收,不会造成资源浪费
-
锁机制已经保证了串行,线程池本身不需要限制并发数
3. 资源清理为什么重要?
如果不调用 removeDevice(),随着设备不断上下线:
-
workerLocks和deviceExecutors会无限增长 -
导致内存泄漏,最终OOM