《Java并发编程实战》7.2.5节TrackingExecutor竞态条件时序问题咨询
竞态条件触发时序说明
首先明确两个参与流程的线程:
- 工作线程:线程池中负责执行
CrawlTask的线程 - 控制线程:调用
WebCrawler.stop()方法触发线程池关闭的线程
触发误报的时序步骤
按时间先后顺序的交错执行流程如下:
- 工作线程执行
runnable.run()(即CrawlTask.run())的所有业务逻辑:爬取页面、解析所有子链接并提交新任务,全部代码执行完成,即将进入finally代码块。 - 控制线程在这个极小的时间间隙内发起关闭流程:
- 首先调用
exec.shutdownNow():将线程池状态标记为已关闭,向所有正在运行的工作线程发送中断信号,返回所有未开始执行的排队任务。 - 随后调用
exec.awaitTermination(...),阻塞等待所有工作线程执行结束。
- 首先调用
- 工作线程恢复执行
finally块内的判断逻辑:- 第一个判断
isShutdown():线程池已被标记为关闭,返回true - 第二个判断
Thread.currentThread().isInterrupted():工作线程已经收到shutdownNow()发送的中断信号,中断标记为true,返回true - 两个条件都满足,工作线程执行
tasksCancelledAtShutdown.add(runnable),将已经执行完成的任务误加入取消任务集合。
- 第一个判断
- 工作线程执行结束,线程池标记为完全终止,
awaitTermination返回true。 - 控制线程调用
exec.getCancelledTasks()拿到被误加的已完成任务,调用saveUncrawled将对应URL重新存入待爬取集合,产生误报。
为什么该竞态无法避免
任务最后一条指令执行完成到线程池标记任务为已完成的间隙,可能发生线程池关闭操作。
runnable.run()执行完成后,到finally块的判断逻辑执行前,没有任何同步机制阻止线程池发起关闭和中断操作。这个时间间隙是客观存在的,无法通过加锁等方式完全消除。
配套的兼容设计
WebCrawler中做了幂等处理:seen是线程安全的ConcurrentHashMap,下次误存的URL被提交时,alreadyCrawled()调用putIfAbsent会发现URL已经被爬取过,直接跳过执行,不会产生重复爬取的实际问题。
内容的提问来源于stack exchange,提问作者bimjhi
相关产品推荐
相关产品推荐

