【问题标题】:HastSet<String> thread safeHashSet<String> 线程安全
【发布时间】:2018-02-24 00:06:54
【问题描述】:

===更新====

来自评论

所以我清楚地阅读了文档并且知道它不是线程安全的,我想运行一个小实验来看看它会如何中断。所以文档说结果是不确定的。有谁知道会发生什么?如果我想证明它不是线程安全的,我该如何编写示例代码才能真正看到它不是线程安全的?你们是否真的尝试过并看到不起作用的例子?你有示例代码吗?

如果我有三个线程访问字符串的哈希集。

  • 添加新字符串
  • 第二次删除字符串
  • 第三次全部删除

HashSet 线程安全吗?

public void test()
{
    Set<String> test = new HashSet<>();
    Thread t0= new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                boolean c = test.contains("test");
                System.out.println("checking " + c);
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    });
    Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                test.add("test");
                System.out.println("adding");
                try {
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    });
    Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            while (true) {
                if (!test.isEmpty())
                {
                    test.removeAll(test);
                }
                System.out.println("removing");
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

        }
    });
    t0.start();
    t1.start();
    t2.start();

    while(true) {

    }
}

我有这个测试代码并运行它,它似乎工作。没有抛出异常。我有点困惑,因为 HashSet 不是线程安全的。 我怎么不见了?

【问题讨论】:

  • 不,不是。您是否费心阅读文档,即HashSet 的javadoc?它是这样说的:注意这个实现是不同步的。如果多个线程同时访问一个哈希集,并且至少有一个线程修改了这个集,那么它必须是同步的外部。
  • 我读过。这就是为什么我说它令人困惑。如果你运行代码,它不会破坏任何东西。
  • 为什么期望抛出异常?哪些例外?
  • 我没有看到任何关于 javadoc 所说的内容令人困惑的地方。请解释为什么您认为它令人困惑。否则,基本上,您的问题没有什么可回答的。除了一个字:没有。
  • 所以我清楚地阅读了文档并且知道它不是线程安全的,我想运行一个小实验来看看它会如何破坏。所以文档说结果是不确定的。有谁知道会发生什么?如果我想证明它不是线程,我如何编写示例代码以便我可以真正看到它不是线程安全的?你们是否真的尝试过并看到这个不起作用的例子?有示例代码吗?

标签: java multithreading concurrency thread-safety hashset


【解决方案1】:

来自comment

所以我清楚地阅读了文档并且知道它不是线程安全的,我想运行一个小实验来看看它会如何中断。所以文档说结果是不确定的。有谁知道会发生什么?如果我想证明它不是线程,我如何编写示例代码以便我可以真正看到它不是线程安全的?你们真的尝试过并看到那个不起作用的例子吗?有示例代码吗?

问题是更新Set 可能不是原子操作,尤其是当内部哈希表需要调整大小时。

如果两个线程同时更新,您可能会得到一个简单的结果,即一个线程覆盖了另一个线程的更改,因此您丢失了更改。更严重的是,冲突可能损坏Set 的内部结构。

为了展示这一点,这里有一个小程序,它在添加值期间会导致高度冲突。添加的所有值都是不同的,所以应该全部添加,但是您会在程序完成时看到Set 的大小不正确,证明某些添加的值丢失了。

final int THREAD_COUNT = 10;
final int NUMS_TO_ADD = 100000;
Set<Integer> set = new HashSet<>();
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0; i < THREAD_COUNT; i++) {
    final int threadNo = i;
    threads[i] = new Thread() {
        @Override public void run() {
            for (int j = 0; j < NUMS_TO_ADD; j++)
                set.add(j * THREAD_COUNT + threadNo); // all distinct values
        }
    };
    threads[i].start();
}
for (int i = 0; i < threads.length; i++)
    threads[i].join();
System.out.println("Found " + set.size() + " values, expected " + THREAD_COUNT * NUMS_TO_ADD);

每次运行都会得到不同的结果,例如

Found 898070 values, expected 1000000
Found 825773 values, expected 1000000
Found 731886 values, expected 1000000
Exception in thread "Thread-7" java.lang.ClassCastException: java.base/java.util.HashMap$Node cannot be cast to java.base/java.util.HashMap$TreeNode
    at java.base/java.util.HashMap$TreeNode.moveRootToFront(HashMap.java:1883)
    at java.base/java.util.HashMap$TreeNode.putTreeVal(HashMap.java:2063)
    at java.base/java.util.HashMap.putVal(HashMap.java:638)
    at java.base/java.util.HashMap.put(HashMap.java:612)
    at java.base/java.util.HashSet.add(HashSet.java:220)
    at Test$1.run(Test.java:16)

或者程序只是挂起!

【讨论】:

    【解决方案2】:

    线程安全

    线程不安全并不意味着你不能在多线程中使用它,否则程序会抛出异常。这意味着当程序在多线程中执行时,你不能总是得到你想要的。请参阅this 了解更多信息。

    在计算机编程中,线程安全描述了一个程序部分或 可以从多个编程线程调用的例程,而无需 线程之间不需要的交互。

    而且,即使你得到了预期的实验结果,你也不能说一个对象是线程安全的。因为在不同的环境下结果可能会有所不同。你应该使用JDK提供的同步机制。

    哈希集

    HashSet 不是线程安全的,这意味着:

    • 如果您将一个对象写入其中,该对象可能不可见 其他线程。
    • 如果您同时从不同线程读取集合,它们可能会得到不同的结果。
    • 如果先调用add,再调用removeAll,这个对象 set 不能被删除。
    • ......

    用户安德烈亚斯的例子很清楚。由于HashSet是基于HashMap的key set,你可以参考这个How to prove that HashMap in java is not thread-safe

    解决方案

    JDK提供了一个线程安全的版本set,集合上的所有操作都需要先获取一个内部监视器锁。

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

    【讨论】:

    • 你得到ConcurrentModificationException,因为你在迭代时调用remove(),两者都在同一个线程中,所以它与多线程无关。既然这是你答案的一半,我将不得不投反对票,对不起。
    • 至于解决方案,Java也有两个并发版本,所以部分回答不完整。
    • @Andreas 我在ConcurrentModificationException 上犯了一个错误,我删除了这个例子。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2011-04-07
    • 1970-01-01
    • 1970-01-01
    • 2022-01-22
    • 2023-01-11
    • 2011-07-19
    相关资源
    最近更新 更多