Java 并发控制:防止重复批次执行的设计与实现
问题分析
需求拆解
- 避免重复推送:确保相同批次任务不会被多次提交。
- 单批次串行:同一时间只有一个实例在执行重复批次。
- 多批次并行:不同批次任务可以并行处理。
关键点
唯一性控制 :需要一个线程安全的机制来记录当前正在处理的批次。
并发控制 :需要确保对批次状态的操作是线程安全的。
任务调度 :线程池负责任务的并发执行。
解决方案
我们可以使用 ConcurrentHashMap 来存储批次的执行状态,并结合 Future 或自定义锁机制来控制任务的执行。以下是具体实现步骤:
- 使用 ConcurrentHashMap 管理批次状态
使用 ConcurrentHashMap<String, Boolean> 来记录批次的状态,其中键是批次 ID,值表示该批次是否正在执行。
当一个批次被推送时,先检查 ConcurrentHashMap 中是否存在该批次。如果不存在,则允许执行;如果存在,则跳过。 - 使用 ReentrantLock 或 synchronized 控制并发
为了确保对批次状态的修改是原子操作,可以使用 ReentrantLock 或 synchronized 来避免竞争条件。 - 线程池调度任务
使用 ExecutorService 提交任务到线程池中执行。
在任务完成后,从 ConcurrentHashMap 中移除该批次的状态。
代码实现
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
public class BatchProcessor {
// 用于存储批次状态的线程安全 Map
private final ConcurrentHashMap<String, Boolean> batchStatusMap = new ConcurrentHashMap<>();
private final ExecutorService executorService;
private final ReentrantLock lock = new ReentrantLock();
public BatchProcessor(int poolSize) {
this.executorService = Executors.newFixedThreadPool(poolSize);
}
/**
* 提交批次任务
*
* @param batchId 批次 ID
* @param task 批次任务
*/
public void submitBatch(String batchId, Runnable task) {
// 尝试获取锁以保证线程安全
lock.lock();
try {
// 检查批次是否已经在处理中
if (batchStatusMap.putIfAbsent(batchId, true) == null) {
// 如果批次不存在,则提交任务到线程池
executorService.submit(() -> {
try {
task.run(); // 执行任务
} finally {
// 任务完成后,移除批次状态
batchStatusMap.remove(batchId);
}
});
} else {
System.out.println("批次 " + batchId + " 已经在处理中,跳过重复推送");
}
} finally {
lock.unlock();
}
}
/**
* 关闭线程池
*/
public void shutdown() {
executorService.shutdown();
}
public static void main(String[] args) {
BatchProcessor processor = new BatchProcessor(5);
// 模拟推送批次任务
for (int i = 0; i < 10; i++) {
String batchId = "batch-" + (i % 3); // 模拟重复批次
processor.submitBatch(batchId, () -> {
System.out.println("处理批次 " + Thread.currentThread().getName());
try {
Thread.sleep(1000); // 模拟任务耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 关闭线程池
processor.shutdown();
}
}
代码解析
ConcurrentHashMap 的作用
:
- putIfAbsent 方法用于检查批次是否已经存在,如果不存在则插入新批次并返回 null,否则返回已存在的值。
这种方式保证了批次状态的唯一性。
ReentrantLock 的作用
:
- 虽然 ConcurrentHashMap 是线程安全的,但在某些情况下(如复杂的业务逻辑)可能需要额外的锁来确保操作的原子性。
任务的生命周期管理
:
- 在任务完成后,通过 finally 块确保从 ConcurrentHashMap 中移除批次状态,避免内存泄漏。
线程池的作用
:
- 使用固定大小的线程池来限制并发任务的数量,避免资源耗尽。
运行结果示例
假设我们推送了 10 个批次任务,其中批次 ID 为 “batch-0
”、“batch-1
” 和 “batch-2
” 循环出现,程序输出可能如下:
处理批次 pool-1-thread-1
处理批次 pool-1-thread-2
处理批次 pool-1-thread-3
批次 batch-0 已经在处理中,跳过重复推送
批次 batch-1 已经在处理中,跳过重复推送
批次 batch-2 已经在处理中,跳过重复推送
...
优化方向
批量处理 :
- 如果批次任务较多,可以考虑批量提交任务以减少锁的竞争。
动态线程池 :
- 根据任务量动态调整线程池大小,避免资源浪费或不足。
持久化批次状态 :
- 如果需要跨 JVM 或重启后保持批次状态,可以将批次状态存储在数据库或分布式缓存中。