需求背景:
有一个监听任务,需要确保重不间断的去监听某一个事件(需求发起网络请求到服务端来触发监听),它可能执行完成的时间是0~15秒(当监听到结果时,会响应返回,这时就会结束监听),获取到事件响应之后会有1~2秒的处理时间。
请设计一个监听程序。
问题分析:
你希望通过多线程监听任务实现对事件的无缝监听,避免在某个任务进行1~2秒的处理时间内出现监听中断。
为了解决这个问题,你可以创建 两个线程交替监听事件。当一个线程正在处理事件时,另一个线程可以继续监听,从而实现事件监听的 覆盖和无缝交接。
设计方案:线程交替监听任务
实现思路一:
1. 两个线程轮流监听,确保即使一个线程在处理事件时,另一个线程依旧能够监听。
2. 使用 信号量(Semaphore) 或 锁机制 来保证任务之间的同步,避免资源冲突。
3. 当一个线程捕获到事件并开始处理时,另一个线程继续监听,保证不遗漏任何事件。
方案分析:
• 优点:【无缝监听】两个线程交替监听事件,避免了监听的空档期、【线程安全】通过信号量实现同步,保证只有一个线程在处理事件
• 问题1:为了实现真正的无缝监听,应该在开始处理事件之前就释放信号量
• 问题2:将事件处理任务也提交到线程池中进行处理是一个更优雅的解决方案。这样,监听线程不会被阻塞,可以继续监听新的事件,同时事件处理可以在后台异步执行
• 问题3:发现启动监听的过程是网络请求,有一个小于1秒的请求时间,那么其实这个方案不是无缝监听,中间存在可能是1秒的网络请求时间的空隙
实现思路二:
实现两个线程的监听过程有至少 1 秒的重叠,我们需要改进当前的信号量机制,使得两个线程的监听能够重叠一部分时间,避免在交替监听期间出现空档。
要实现两个线程的监听过程有至少 1 秒的重叠,我们需要改进当前的信号量机制,使得两个线程的监听能够重叠一部分时间,避免在交替监听期间出现空档。
优化思路:
1. 提前启动下一个线程的监听任务:在当前线程监听即将结束时,提前启动另一个线程。
2. 不再使用纯信号量控制交替监听:可以通过 倒计时锁(CountDownLatch) 或 定时器机制 来启动下一个线程监听。
3. 解决网络请求的潜在延迟:保证两个监听任务在请求重叠期间,至少有一个线程始终在监听。
优化方案:提前启动线程监听,实现监听任务重叠
方案实现:
• 在线程 A 即将结束监听的 1 秒前,启动线程 B 的监听任务。
• CountDownLatch 或自定义的计时机制用于提前通知线程 B 开始监听。
• 保证每个线程的监听任务有 重叠时间,避免网络请求造成的监听空档。
优化后的完整代码:
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.Random;
public class OverlappedEventListener {
private final Random random = new Random(); // 用于模拟随机时间
private final ExecutorService executorService = Executors.newFixedThreadPool(2); // 创建线程池
public static void main(String[] args) {
OverlappedEventListener listener = new OverlappedEventListener();
listener.startListening(); // 启动监听任务
}
// 启动两个线程交替监听
public void startListening() {
// 启动两个监听线程
executorService.submit(() -> listenTask("Listener-1", "Listener-2"));
executorService.submit(() -> listenTask("Listener-2", "Listener-1"));
}
// 定义监听任务
private void listenTask(String currentListener, String nextListener) {
while (true) {
try {
// 启动当前线程的监听任务(模拟1~15秒的监听时间)
int listenTime = random.nextInt(15) + 1;
System.out.println(currentListener + " 正在监听事件,耗时:" + listenTime + " 秒");
// 创建 CountDownLatch,用于在监听快结束时提前通知下一个线程
CountDownLatch latch = new CountDownLatch(1);
// 提前1秒启动下一个监听任务
executorService.submit(() -> {
try {
Thread.sleep((listenTime - 1) * 1000); // 等待监听时间的剩余秒数
System.out.println(currentListener + " 通知 " + nextListener + " 开始监听");
latch.countDown(); // 通知下一个线程
} catch (InterruptedException e) {
e.printStackTrace();
}
});
// 等待任务结束,并确保至少有1秒重叠时间
latch.await(1, TimeUnit.SECONDS);
// 模拟当前线程的事件处理(1~2秒)
int processTime = random.nextInt(2) + 1;
System.out.println(currentListener + " 正在处理事件,耗时:" + processTime + " 秒");
Thread.sleep(processTime * 1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
示例输出:
Listener-1 正在监听事件,耗时:10 秒
Listener-1 通知 Listener-2 开始监听
Listener-2 正在监听事件,耗时:8 秒
Listener-2 正在处理事件,耗时:1 秒
Listener-2 通知 Listener-1 开始监听
Listener-1 正在监听事件,耗时:12 秒
Listener-1 正在处理事件,耗时:2 秒
代码解析:
1. 提前 1 秒启动下一个线程的监听任务:
• 每个监听线程在接近监听结束时,提前 1 秒通知另一个线程开始监听。
• 使用 CountDownLatch 来确保下一个线程在指定时间内被唤醒,避免监听空档。
2. 线程的监听任务有重叠时间:
• 当前线程还在处理任务时,另一个线程已经开始监听,确保没有任何时间空档。
3. CountDownLatch 控制重叠监听:
• CountDownLatch 保证下一个线程的监听任务在当前任务结束前提前启动,确保至少 1 秒重叠。
优化后的效果:
• 监听无缝衔接:每次监听任务有至少 1 秒的重叠时间,保证不会因为网络请求的延迟导致监听空档。
• 并行监听与处理:监听任务和事件处理解耦,通过线程池并发执行。
• 避免竞态问题:使用 CountDownLatch 控制任务启动,保证线程之间的同步。
总结:
这个优化方案通过 提前 1 秒启动另一个线程的监听任务,确保两个线程的监听任务有重叠时间,彻底解决了网络请求时间带来的监听空档问题。这种设计不仅提高了系统的可靠性,还保证了监听的实时性。