为了账号安全,请及时绑定邮箱和手机立即绑定

仅在添加到 HashSet 时同步是线程安全的吗?

仅在添加到 HashSet 时同步是线程安全的吗?

九州编程 2023-05-17 16:56:16
想象一下,有一个主线程创建一个 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重复 ,尽管对这两个问题的回答可能相似,但这个问题解决了更多的特殊情况。
查看完整描述

3 回答

?
小唯快跑啊

TA贡献1863条经验 获得超2个赞

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

简短回答:否和是。

有两种解释方式:

直观的解释

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

  • 两个线程同时更新共享状态。

  • 一个线程试图读取状态,而另一个正在更新它。

  • 线程看到过时的值,因为内存缓存尚未写入主内存。

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

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

内存模型解释

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

 // thread one

 synchronized (sharedLock) {

    sharedVariable = 42;

 }


 // thread two

 synchronized (sharedLock) {

     other = sharedVariable;

 }

假设线程一的代码在线程二的代码之前运行,则线程一释放锁和线程二获取锁之间存在happens before关系。有了这个和“程序顺序”的关系,我们就可以建立一个从写入42到赋值到的链条other。这足以保证other将被分配42(或可能是变量的以后值)并且sharedVariable之前没有任何值42被写入它。


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


查看完整回答
反对 回复 2023-05-17
?
森栏

TA贡献1810条经验 获得超5个赞

该代码对于该 synchronized (pSet) { }部分是线程安全的:


if (!pSet.contains(x)) {

  synchronized (pSet) { 

  // Here you are sure to have the updated value of pSet    

  if (!pSet.contains(x)) {

    // do some exclusive work with x.

    pSet.add(x);

  }

}

因为在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,因此没有读取锁和最小的写入锁(仅在要修改的条目上而不是整个结构上)。


查看完整回答
反对 回复 2023-05-17
?
临摹微笑

TA贡献1982条经验 获得超2个赞

您不知道在另一个线程添加期间哈希集可能处于什么状态。可能正在进行根本性的更改,例如存储桶的拆分,因此在另一个线程添加期间包含可能会返回false ,即使该元素将存在于单线程 HashSet 中。在那种情况下,您将尝试第二次添加元素。

更糟糕的情况:由于两个线程同时使用的内存中的 HashSet 处于临时无效状态,contains可能会陷入死循环或抛出异常。


查看完整回答
反对 回复 2023-05-17
  • 3 回答
  • 0 关注
  • 146 浏览

添加回答

举报

0/150
提交
取消
意见反馈 帮助中心 APP下载
官方微信