首页 > 解决方案 > 仅在添加到 HashSet 时同步是否是线程安全的?

问题描述

想象一下,有一个主线程创建一个 HashSet 并启动许多工作线程,将 HashSet 传递给它们。

就像下面的代码一样:

void main() {
  final Set<String> set = new HashSet<>();
  final ExecutorService threadExecutor = 
  Executors.newFixedThreadPool(10);

  threadExecutor.submit(() -> doJob(set));
} 

void doJob(final Set<String> pSet) {
  // do some stuff
  final String x = ... // doesn't matter how we received the value.
  if (!pSet.contains(x)) {
    synchronized (pSet) {
      // double check to prevent multiple adds within different threads
      if (!pSet.contains(x)) {
        // do some exclusive work with x.
        pSet.add(x);
      }
    }
  }
  // do some stuff
}

我想知道仅在 add 方法上同步是否是线程安全的?contains如果不同步,是否有任何可能的问题?

我的直觉告诉我这很好,在离开同步块更改后对所有线程都应该可见,但 JMM 有时可能违反直觉。

PS我不认为它是How to lock multiple resources in java multithreading的重复 即使两者的答案可能相似,这个问题解决了更特殊的情况。

标签: javamultithreadingconcurrency

解决方案


我想知道仅在add方法上同步是否是线程安全的?contains如果不同步,是否有任何可能的问题?

简短的回答:不,是。

有两种解释方式:

直观的解释

Java 同步(以各种形式)可以防止许多事情,包括:

  • 两个线程同时更新共享状态。
  • 一个线程试图读取状态,而另一个线程正在更新它。
  • 由于内存缓存尚未写入主内存,线程看到过时的值。

在您的示例中,同步add足以确保两个线程不能同时更新HashSet,并且两个调用都将在最新HashSet状态下运行。

但是,如果contains不同步,则contains呼叫可能与add呼叫同时发生。这可能会导致contains调用看到 的中间状态HashSet,从而导致不正确的结果,或者更糟。如果调用不是同时发生的,也可能发生这种情况,因为更改没有立即刷新到主内存和/或读取线程没有从主内存读取。

记忆模型解释

JLS 指定了 Java 内存模型,它规定了多线程应用程序必须满足的条件,以保证一个线程看到另一个线程进行的内存更新。该模型用数学语言表示,不易理解,但要点是当且仅当在从写入到后续读取的关系之前存在一连串发生时,才能保证可见性。如果写入和读取在不同的线程中,那么线程之间的同步是这些关系的主要来源。例如在

 // thread one
 synchronized (sharedLock) {
    sharedVariable = 42;
 }

 // thread two
 synchronized (sharedLock) {
     other = sharedVariable;
 }

假设线程一的代码在线程二的代码之前运行,那么线程一释放锁和线程二获取锁之间存在着先于关系。有了这个和“程序顺序”的关系,我们可以建立一个从 write of42到 assignment to的链other。这足以保证other将被分配42(或可能是变量的后续值),而不是sharedVariable之前42写入的任何值。

如果没有在synchronized同一个锁上同步块,第二个线程可能会看到一个过时的值sharedVariable; 即之前写入它的一些值42被分配给它。


推荐阅读