创建具有重复值的 HashSet

Create a HashSet with duplicate values

在多个线程向其添加项目的情况下,HashSet 是否可能具有重复值?

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

来自 Javadoc

Note that this implementation is not synchronized. If multiple threads access a hash set concurrently, and at least one of the threads modifies the set, it must be synchronized externally. This is typically accomplished by synchronizing on some object that naturally encapsulates the set. If no such object exists, the set should be "wrapped" using the Collections.synchronizedSet method. This is best done at creation time, to prevent accidental unsynchronized access to the set:

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

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

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

Is there a possibility that HashSet could have duplicate values in case of multiple threads adding items to it?

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

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

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

(不同的备选方案都有与之相关的注意事项。我们无法根据您提供的有限信息推荐特定的备选方案。)

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

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

  2. HashSet in Java 不是线程安全的,因为它默认不同步。如果您在多线程环境中使用 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 很有用。代码块也取自那里。