【问题标题】:Create a HashSet with duplicate values创建具有重复值的 HashSet
【发布时间】:2023-04-10 22:09:01
【问题描述】:

如果多个线程向其中添加项目,HashSet 是否有可能具有重复值?

我不是从修改 equals 或 hashcode 方法的角度来看,而是从多线程环境来看。

【问题讨论】:

  • HashSet 不是线程安全的,因此不应该在不使用某种锁的情况下同时从多个线程访问它;这是您想知道的,还是您从理论上对并发更新是否会导致重复值而不是其他类型的故障特别感兴趣?
  • 您能告诉我们更多有关情况吗?你看到这种情况发生了吗?如果您不应该允许多个线程同时访问该集合,这是否会发生真的重要吗?
  • @kaya3 这正是我的疑问......知道(理论上)并发更新是否会导致重复。您可能遇到的示例程序/屏幕截图会有所帮助,因为我多次尝试使用复杂的对象但没有运气体验这种情况。
  • @Steve 出于好奇,我想看看由于并发添加而导致哈希集中重复的用例。

标签: java hashset


【解决方案1】:

如果多个线程向其中添加项目,HashSet 是否有可能具有重复值?

HashSet 不是线程安全类。如果您在没有正确同步的情况下从多个线程更新HashSet,则行为是未指定的,并且难以预测。 (并且取决于 Java 版本,因为 HashSet 的实现在 Java SE 的生命周期中已经更改了很多次。)

未指定的行为可能包括通过集合的迭代器观察到的出现在集合中的重复项。

如果您想在多个线程之间共享(可变)集合,请使用ConcurrentHashSetCollections.synchronizedSet 包装器或显式Lock 或互斥锁来同步操作。

(不同的替代品都有相关的注意事项。根据您提供的有限信息,我们无法推荐特定的替代品。)

【讨论】:

  • 这是有道理的,我只想以某种方式重现它。
  • 很难重现。而且我不明白为什么你会需要来重现它。真正的收获是,如果您在没有正确同步的情况下使用HashSet,可能会发生奇怪的事情......而且您不应该这样做!
  • 我想复制来验证这个重复理论。根据下面 hfontanez 的建议,我尝试编写一个测试程序,但看到了一个奇怪的行为。我看到尺寸大于 1,但是当我尝试打印该项目时,它只打印一个。我已经上传了示例文件here
  • 好的。任何。我只是不明白为什么你需要验证这个理论。 Java 语言规范中明确规定了在这种情况下出现未指定行为的可能性。这应该足够了。
【解决方案2】:

我们可以逐个讨论这个答案:

  1. HashSet 不允许重复的elements,这意味着您不能在 HashSet 中存储重复的值。它的替代 Hashmap 不允许 duplicate keys,但是,它允许 duplicate values

  2. Java 中的HashSet 不是线程安全的,因为它默认不同步。如果您在多线程环境中使用 HashSet,其中它由多个线程同时访问并且即使是单个线程也对其结构进行修改,那么它必须在外部同步。结构修改定义为添加或删除一个或多个元素或显式调整后备数组大小的任何操作;仅仅设置元素的值不是结构修改。

    因此,当您在没有外部同步的情况下从多个线程更新HashSet 时,其行为将无法预测。

  3. 为了避免这种不可预知的行为,我们可以使用 Collections.synchronizedSet() 方法来同步 HashSet。

例子:

  • 首先,我们将看一个例子,如果 HashSet 用于多线程环境而不同步它会发生什么。

    在 Java 代码中创建了四个线程,每个线程向 Set 中添加 5 个元素。在所有线程完成后Set size should be 20

public class SetSynchro implements Runnable{
  private Set<String> numSet;
  public SetSynchro(Set<String> numSet){
    this.numSet = numSet;
  }
  
  public static void main(String[] args) {
    Set<String> numSet = new HashSet<String>();
    /// 4 threads
    Thread t1 = new Thread(new SetSynchro(numSet));
    Thread t2 = new Thread(new SetSynchro(numSet));
    Thread t3 = new Thread(new SetSynchro(numSet));
    Thread t4 = new Thread(new SetSynchro(numSet));
        
    t1.start();
    t2.start();
    t3.start();
    t4.start();
        
    try {
      t1.join();
      t2.join();
      t3.join();
      t4.join();
    } catch (InterruptedException e) {    
      e.printStackTrace();
    }
    System.out.println("Size of Set is " + numSet.size());
  }

  @Override
  public void run() {
    System.out.println("in run method" + Thread.currentThread().getName());
    String str = Thread.currentThread().getName();
    for(int i = 0; i < 5; i++){
      // adding thread name to make element unique
      numSet.add(i + str);
      try {
        // delay to verify thread interference
        Thread.sleep(500);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}

输出:

in run methodThread-2
in run methodThread-0
in run methodThread-3
in run methodThread-1
Size of Set is 19
//In one of the run size was 19, in another run 18 and sometimes even 20, so you can see that thread interference is making the behavior unpredictable.
  • 因此您可以看到线程干扰使行为变得不可预测。因此,我们将使用相同的示例同步 HashSet。
public class SetSynchro implements Runnable{
  private Set<String> numSet;

  public SetSynchro(Set<String> numSet){
    this.numSet = numSet;
  }

  public static void main(String[] args) {
    // Synchronized Set
    Set<String> numSet = Collections.synchronizedSet(new HashSet<String>());
    /// 4 threads
    Thread t1 = new Thread(new SetSynchro(numSet));
    Thread t2 = new Thread(new SetSynchro(numSet));
    Thread t3 = new Thread(new SetSynchro(numSet));
    Thread t4 = new Thread(new SetSynchro(numSet));
    
    t1.start();
    t2.start();
    t3.start();
    t4.start();
        
    try {
      t1.join();
      t2.join();
      t3.join();
      t4.join();
    } catch (InterruptedException e) {    
      e.printStackTrace();
    }
    System.out.println("Size of Set is " + numSet.size());
  }

  @Override
  public void run() {
    System.out.println("in run method" + Thread.currentThread().getName());
    String str = Thread.currentThread().getName();
    for(int i = 0; i < 5; i++){
      // adding thread name to make element unique
      numSet.add(i + str);
      try {
        // delay to verify thread interference
        Thread.sleep(500);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}

输出:

in run methodThread-3
in run methodThread-2
in run methodThread-1
in run methodThread-0
Size of Set is 20
//Now every time size of HashSet is 20.

更多详情,link 很有用。代码块也取自那里。

【讨论】:

  • 感谢您提供详细信息,但我对您遇到的代码更感兴趣,特别是在类似的多线程条件下重复的用例。
  • 这种情况是不可预测的。它可能会发生,也可能不会。所以,很难说:)
【解决方案3】:

来自 Javadoc

请注意,此实现不同步。如果多个线程 同时访问一个哈希集,以及至少一个线程 修改集合,它必须在外部同步。这是 通常通过同步一些自然而然的对象来完成 封装集合。如果不存在这样的对象,则该集合应该是 使用Collections.synchronizedSet 方法“包装”。这是最好的 在创建时完成,以防止意外的不同步访问 集合:

Set s = Collections.synchronizedSet(new HashSet(...));

基本上,我对此的解释是,在正确(或错误)的条件下,发生这种情况的可能性很小。但是,由于假设您知道多个线程将访问该集合,因此您需要在外部同步访问(因为HashSet 不是线程安全的)。或者,在没有这种外部机制的情况下,您需要按照上图所示包装这个集合。

如果您真的想找出答案,请创建一个包含许多尝试设置相同值的线程的应用程序。插入后,如果集合的大小大于 1,则向控制台打印一些消息。这应该告诉你答案。也许这种情况发生的可能性很小。但是类文档告诉你,如果你希望在多线程进程中使用,你应该同步它。

【讨论】:

  • 非常感谢。我一直在寻找类似的方式来重现这一点。我可以试试这个。
  • 我尝试了您建议的示例程序,但我看到大小大于 1,但是当我尝试打印该项目时,它只打印一个。我已经上传了示例文件here。您能否建议我为什么会看到这种行为?
  • @PratikShekhar 这就是您问题的答案。即使在多线程应用程序中,也不可能将相同的值添加到 HashSet
猜你喜欢
  • 2019-04-15
  • 1970-01-01
  • 1970-01-01
  • 2015-05-12
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2013-08-13
  • 2021-02-21
相关资源
最近更新 更多