仅在添加到 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如果不同步,是否有任何可能的问题?


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


附注:我不认为它与How to lock multiple resources in java multithreading重复 ,尽管对这两个问题的回答可能相似,但这个问题解决了更多的特殊情况。


九州编程
浏览 147回答 3
3回答

小唯快跑啊

我想知道仅在方法上同步是否线程安全add?contains如果不同步,是否有任何可能的问题?简短回答:否和是。有两种解释方式:直观的解释Java 同步(以其各种形式)防止许多事情,包括:两个线程同时更新共享状态。一个线程试图读取状态,而另一个正在更新它。线程看到过时的值,因为内存缓存尚未写入主内存。在您的示例中,同步add就足以确保两个线程不能同时更新HashSet,并且两个调用都将在最新HashSet状态下运行。但是,如果contains也不同步,则contains调用可能会与调用同时发生add。这可能导致contains调用看到 的中间状态HashSet,从而导致不正确的结果,或者更糟。如果调用不是同时发生的,这也会发生,因为更改没有立即刷新到主内存和/或读取线程没有从主内存读取。内存模型解释JLS 指定了 Java 内存模型,它规定了多线程应用程序必须满足的条件,以保证一个线程可以看到另一个线程所做的内存更新。该模型是用数学语言表达的,并不容易理解,但要点是当且仅当从写入到后续读取之间存在一系列 happen&nbsp;before关系时,才能保证可见性。如果写入和读取在不同的线程中,那么线程之间的同步是这些关系的主要来源。例如在&nbsp;// thread one&nbsp;synchronized (sharedLock) {&nbsp; &nbsp; sharedVariable = 42;&nbsp;}&nbsp;// thread two&nbsp;synchronized (sharedLock) {&nbsp; &nbsp; &nbsp;other = sharedVariable;&nbsp;}假设线程一的代码在线程二的代码之前运行,则线程一释放锁和线程二获取锁之间存在happens before关系。有了这个和“程序顺序”的关系,我们就可以建立一个从写入42到赋值到的链条other。这足以保证other将被分配42(或可能是变量的以后值)并且sharedVariable之前没有任何值42被写入它。如果synchronized块不在同一个锁上同步,第二个线程可能会看到一个过时的值sharedVariable;即之前写入的一些值42被分配给它。

森栏

该代码对于该 synchronized (pSet) { }部分是线程安全的:if (!pSet.contains(x)) {&nbsp; synchronized (pSet) {&nbsp;&nbsp; // Here you are sure to have the updated value of pSet&nbsp; &nbsp;&nbsp;&nbsp; if (!pSet.contains(x)) {&nbsp; &nbsp; // do some exclusive work with x.&nbsp; &nbsp; pSet.add(x);&nbsp; }}因为在synchronized对象的声明中pSet:一个且只有一个线程可能在这个块中。在其中,pSet它的更新状态也由与 synchronized 关键字的 happens-before 关系保证。因此,无论等待线程的第一个语句返回的值是什么if (!pSet.contains(x)),当这个被等待的线程醒来并进入语句时 synchronized,它都会设置最后更新的值pSet。因此,即使前一个线程添加了相同的元素,第二个线程 if (!pSet.contains(x))也会返回false。但是这段代码对于if (!pSet.contains(x))在写入Set.根据经验,不应该使用未设计为线程安全的集合来执行并发的写入和读取操作,因为集合的内部状态可能处于正在进行/不一致的状态,以进行同时发生的读取操作一个写操作。虽然一些非线程安全的集合实现在事实中接受了这样的用法,但这根本不能保证它总是正确的。所以你应该使用线程安全的Set实现来保证整个线程安全。例如:Set<String> pSet = ConcurrentHashMap.newKeySet();这在引擎盖下使用 a ConcurrentHashMap,因此没有读取锁和最小的写入锁(仅在要修改的条目上而不是整个结构上)。

临摹微笑

不,您不知道在另一个线程添加期间哈希集可能处于什么状态。可能正在进行根本性的更改,例如存储桶的拆分,因此在另一个线程添加期间包含可能会返回false ,即使该元素将存在于单线程 HashSet 中。在那种情况下,您将尝试第二次添加元素。更糟糕的情况:由于两个线程同时使用的内存中的 HashSet 处于临时无效状态,contains可能会陷入死循环或抛出异常。
打开App,查看更多内容
随时随地看视频慕课网APP

相关分类

Java