使用ExecutorService存WebPage到Set却为空,求排查问题
多线程下HashSet添加元素为空的问题分析与解决
兄弟,你的问题主要出在两个核心点,再加上一个容易忽略的细节,我给你逐一拆解:
1. HashSet本身不是线程安全的集合
HashSet的底层是基于HashMap实现的,而HashMap和HashSet都没有任何同步机制。当多个线程同时调用webPages.add()的时候,会出现竞态条件:
- 比如两个线程同时检查某个元素是否存在(基于hashCode和equals),然后都尝试添加,可能导致元素被覆盖或者根本没被正确插入;
- 更严重的是,并发修改HashSet内部的哈希表结构时,可能会导致链表成环、扩容异常等问题,直接让集合处于不一致状态,元素无法正常存储。
2. 主线程没有等待线程池任务全部完成
你调用了executor.shutdown(),但这个方法只是告诉线程池“不再接受新任务”,已经提交的任务可能还在后台执行。如果主线程在shutdown()之后直接结束,那么这些子线程可能还没来得及执行webPages.add()操作,你去查看集合的时候自然是空的。
3. 容易忽略的细节:WebPage类的equals和hashCode实现
HashSet是通过元素的hashCode()和equals()来判断是否重复的。如果你的WebPage类没有正确重写这两个方法,就算元素被添加进去,也可能因为HashSet认为是重复元素(或者错误地认为是不同元素但存储异常),导致最终集合不符合预期。
对应的解决办法
(1)替换为线程安全的集合
把HashSet换成以下两种线程安全的集合之一:
Collections.synchronizedSet()包装的HashSet:给HashSet加上同步锁,保证每次只有一个线程能操作集合:Set<WebPage> webPages = Collections.synchronizedSet(new HashSet<>());CopyOnWriteArraySet:适合读多写少的场景,每次写入都会复制一个新的数组,避免并发修改问题:Set<WebPage> webPages = new CopyOnWriteArraySet<>();
(2)等待线程池任务执行完毕
在executor.shutdown()之后,调用awaitTermination()等待所有任务完成,确保主线程会等到所有子线程都执行完添加操作:
executor.shutdown(); try { // 设置一个合理的超时时间,比如5分钟 if (!executor.awaitTermination(5, TimeUnit.MINUTES)) { // 如果超时就强制关闭线程池 executor.shutdownNow(); } } catch (InterruptedException e) { executor.shutdownNow(); // 恢复中断状态 Thread.currentThread().interrupt(); }
(3)确保WebPage正确重写equals和hashCode
比如根据title、link这些核心字段来实现:
import java.util.Objects; class WebPage { private String title; private String link; private String bodyText; public WebPage(String title, String link, String bodyText) { this.title = title; this.link = link; this.bodyText = bodyText; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; WebPage webPage = (WebPage) o; return Objects.equals(title, webPage.title) && Objects.equals(link, webPage.link) && Objects.equals(bodyText, webPage.bodyText); } @Override public int hashCode() { return Objects.hash(title, link, bodyText); } // 按需添加getter、setter方法 }
修改后的完整示例代码
public void spinOffThreadsToGetPages() { // 使用线程安全的集合 Set<WebPage> webPages = Collections.synchronizedSet(new HashSet<>()); ExecutorService executor = Executors.newFixedThreadPool(10); for (String link : links) { executor.submit(() -> addWebPagesToSet(webPages, link)); } executor.shutdown(); // 等待所有任务完成 try { if (!executor.awaitTermination(5, TimeUnit.MINUTES)) { executor.shutdownNow(); } } catch (InterruptedException e) { executor.shutdownNow(); Thread.currentThread().interrupt(); } // 此时webPages应该包含所有成功获取的WebPage对象 } private void addWebPagesToSet(Set<WebPage> webPages, String link) { Document document = getDocument(link); if (document == null) { return; } WebPage webPage = new WebPage(document.title(), link, document.body().text()); webPages.add(webPage); } // 正确实现equals和hashCode的WebPage类 class WebPage { private String title; private String link; private String bodyText; public WebPage(String title, String link, String bodyText) { this.title = title; this.link = link; this.bodyText = bodyText; } @Override public boolean equals(Object o) { if (this == o) return true; if (o == null || getClass() != o.getClass()) return false; WebPage webPage = (WebPage) o; return Objects.equals(title, webPage.title) && Objects.equals(link, webPage.link) && Objects.equals(bodyText, webPage.bodyText); } @Override public int hashCode() { return Objects.hash(title, link, bodyText); } }
内容的提问来源于stack exchange,提问作者Lukas
相关产品推荐
相关产品推荐

