Kotlin多线程爬虫中Queue.poll()返回null但队列非空问题
多线程网页爬虫Queue.poll()返回null但队列非空的问题分析
以下是几个可能导致该异常的核心原因及排查方向:
1. 队列实现非线程安全
如果你的toDownload队列用的是普通非线程安全实现(比如LinkedList),多线程并发读写时会出现数据结构不一致的情况:
- 当一个线程正在执行入队操作(比如修改链表指针),另一个线程同时调用
poll(),此时队列内部结构还没更新完成,poll()就可能返回null,但实际队列已经有元素待处理。 - 必须替换为线程安全的队列实现,比如
LinkedBlockingQueue、ArrayBlockingQueue或ConcurrentLinkedQueue,这些类本身就处理了并发读写的同步逻辑。
2. 重复过滤逻辑的并发冲突
虽然你重写了URL和WebPage的equals与hashCode,但如果用于去重的集合是非线程安全的(比如HashSet),会出现竞态条件:
- 线程A检查某链接不在去重集合中,准备添加到队列;同时线程B也完成了相同的检查,导致两个线程都认为可以添加,但实际队列中可能只成功入队一次,或者去重集合的状态与队列状态完全脱节,让你误以为队列有大量元素,实际已被过滤。
- 替换去重集合为线程安全实现,比如用
ConcurrentHashMap(以链接为key,标记是否已处理)或ConcurrentSkipListSet。
3. 队列操作的边界处理错误
- 如果使用有界队列,当队列满时,若入队逻辑没有正确处理:比如用
add()方法抛出异常后直接忽略,会导致元素没有真正入队,但你统计的“待下载数量”却累加了,出现计数与实际队列元素不符的情况。应该用offer()并检查返回值,或用阻塞方法put()确保元素成功入队。 - 另外,若你在
poll()返回null后直接进入无限循环,没有正确处理“队列暂时为空但后续会有元素”的场景,也会触发问题——但结合你说的队列实际非空,更可能是前面的并发同步问题。
4. 队列size()统计的误导性
你提到的“存在28035个元素”如果是通过队列的size()方法获取的,要注意:
- 对于
ConcurrentLinkedQueue这类无界线程安全队列,size()是近似值——它需要遍历整个队列来计数,遍历过程中可能有元素被添加或移除,导致统计值与实际可poll的元素数量不一致。你看到的“非空”可能只是过时的统计结果,实际队列已经为空;或者反过来,队列有元素但size()还没更新。
排查建议
- 立即替换队列和去重集合为标准线程安全实现,验证问题是否消失。
- 在
poll()返回null时,打印当前线程ID、队列size(),并尝试用迭代器遍历队列(注意部分线程安全队列的迭代器是弱一致性的),确认队列实际是否有元素。 - 检查入队逻辑:是否正确处理了
offer()的返回值,是否捕获了入队时的异常并做了正确处理(比如重试或标记失败)。
内容的提问来源于stack exchange,提问作者Bartlomiej Dlugosz
相关产品推荐
相关产品推荐

