【问题标题】:Is iterating ConcurrentHashMap values thread safe?迭代 ConcurrentHashMap 值线程安全吗?
【发布时间】:2011-04-15 16:13:53
【问题描述】:

ConcurrentHashMap 的 javadoc 中如下:

检索操作(包括 get)一般不会阻塞,因此可能与更新操作(包括 put 和 remove)重叠。检索反映了最近完成的更新操作在其开始时保持的结果。对于 putAll 和 clear 等聚合操作,并发检索可能仅反映插入或删除某些条目。类似地,迭代器和枚举返回反映哈希表在创建迭代器/枚举时或之后的某个时间点的状态的元素。它们不会抛出 ConcurrentModificationException。 但是,迭代器被设计为一次只能由一个线程使用。

这是什么意思?如果我尝试同时用两个线程迭代映射会发生什么?如果我在迭代时从映射中放置或删除一个值会发生什么?

【问题讨论】:

    标签: java multithreading concurrency thread-safety concurrenthashmap


    【解决方案1】:

    什么意思?

    这意味着你不应该尝试在两个线程中使用相同的迭代器。如果您有两个线程需要迭代键、值或条目,那么它们都应该创建并使用自己的迭代器。

    如果我尝试同时使用两个线程迭代映射会发生什么?

    如果您违反此规则会发生什么,目前尚不完全清楚。您可能会遇到令人困惑的行为,就像(例如)两个线程尝试从标准输入读取而不进行同步一样。你也可能得到非线程安全的行为。

    但是如果两个线程使用不同的迭代器,你应该没问题。

    如果我在迭代时从映射中放置或删除一个值会发生什么?

    如果两个线程使用相同的迭代器:见上文。您可能会感到困惑并且可能是非线程安全的行为。

    如果线程使用不同的迭代器,您引用的 javadoc 部分可以充分回答它。基本上,没有定义一个线程/迭代器是否会看到另一个线程/迭代器进行的任何并发插入、更新或删除的影响。但是,插入/更新/删除将根据地图的并发属性进行。

    【讨论】:

      【解决方案2】:

      什么意思?

      这意味着您从ConcurrentHashMap 获得的每个迭代器都设计为由单个线程使用,不应被传递。这包括 for-each 循环提供的语法糖。

      如果我尝试同时使用两个线程迭代映射会发生什么?

      如果每个线程都使用它自己的迭代器,它将按预期工作。

      如果我在迭代时从映射中放置或删除一个值会发生什么?

      如果您这样做,可以保证事情不会中断(这是ConcurrentHashMap 中“并发”的一部分含义)。但是,不能保证一个线程会看到另一个线程执行的对映射的更改(没有从映射中获取新的迭代器)。迭代器保证在创建地图时反映地图的状态。进一步的变化可能会反映在迭代器中,但不一定非得如此。

      总之,这样的陈述

      for (Object o : someConcurrentHashMap.entrySet()) {
          // ...
      }
      

      几乎每次你看到它都会很好(或至少是安全的)。

      【讨论】:

      • 那么如果在迭代过程中,另一个线程从地图中删除了一个对象 o10 会发生什么?即使它已被删除,我还能在迭代中看到 o10 吗? @Waldheinz
      • 如上所述,实际上并没有指定现有迭代器是否会反映以后对地图的更改。所以我不知道,而且按照规范没有人这样做(不查看代码,并且可能会随着运行时的每次更新而改变)。所以你不能依赖它。
      • 但是我在迭代ConcurrentHashMap时仍然得到ConcurrentModificationException,为什么?
      • @KimiChiu 您可能应该发布一个新问题,提供触发该异常的代码,但我高度怀疑它直接源于迭代并发容器。除非 Java 实现有问题。
      【解决方案3】:

      你可以使用这个类来测试两个访问线程和一个改变ConcurrentHashMap的共享实例:

      import java.util.Map;
      import java.util.Random;
      import java.util.UUID;
      import java.util.concurrent.ConcurrentHashMap;
      import java.util.concurrent.ExecutorService;
      import java.util.concurrent.Executors;
      
      public class ConcurrentMapIteration
      {
        private final Map<String, String> map = new ConcurrentHashMap<String, String>();
      
        private final static int MAP_SIZE = 100000;
      
        public static void main(String[] args)
        {
          new ConcurrentMapIteration().run();
        }
      
        public ConcurrentMapIteration()
        {
          for (int i = 0; i < MAP_SIZE; i++)
          {
            map.put("key" + i, UUID.randomUUID().toString());
          }
        }
      
        private final ExecutorService executor = Executors.newCachedThreadPool();
      
        private final class Accessor implements Runnable
        {
          private final Map<String, String> map;
      
          public Accessor(Map<String, String> map)
          {
            this.map = map;
          }
      
          @Override
          public void run()
          {
            for (Map.Entry<String, String> entry : this.map.entrySet())
            {
              System.out.println(
                  Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']'
              );
            }
          }
        }
      
        private final class Mutator implements Runnable
        {
      
          private final Map<String, String> map;
          private final Random random = new Random();
      
          public Mutator(Map<String, String> map)
          {
            this.map = map;
          }
      
          @Override
          public void run()
          {
            for (int i = 0; i < 100; i++)
            {
              this.map.remove("key" + random.nextInt(MAP_SIZE));
              this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString());
              System.out.println(Thread.currentThread().getName() + ": " + i);
            }
          }
        }
      
        private void run()
        {
          Accessor a1 = new Accessor(this.map);
          Accessor a2 = new Accessor(this.map);
          Mutator m = new Mutator(this.map);
      
          executor.execute(a1);
          executor.execute(m);
          executor.execute(a2);
        }
      }
      

      不会抛出异常。

      在访问线程之间共享相同的迭代器会导致死锁:

      import java.util.Iterator;
      import java.util.Map;
      import java.util.Random;
      import java.util.UUID;
      import java.util.concurrent.ConcurrentHashMap;
      import java.util.concurrent.ExecutorService;
      import java.util.concurrent.Executors;
      
      public class ConcurrentMapIteration
      {
        private final Map<String, String> map = new ConcurrentHashMap<String, String>();
        private final Iterator<Map.Entry<String, String>> iterator;
      
        private final static int MAP_SIZE = 100000;
      
        public static void main(String[] args)
        {
          new ConcurrentMapIteration().run();
        }
      
        public ConcurrentMapIteration()
        {
          for (int i = 0; i < MAP_SIZE; i++)
          {
            map.put("key" + i, UUID.randomUUID().toString());
          }
          this.iterator = this.map.entrySet().iterator();
        }
      
        private final ExecutorService executor = Executors.newCachedThreadPool();
      
        private final class Accessor implements Runnable
        {
          private final Iterator<Map.Entry<String, String>> iterator;
      
          public Accessor(Iterator<Map.Entry<String, String>> iterator)
          {
            this.iterator = iterator;
          }
      
          @Override
          public void run()
          {
            while(iterator.hasNext()) {
              Map.Entry<String, String> entry = iterator.next();
              try
              {
                String st = Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']';
              } catch (Exception e)
              {
                e.printStackTrace();
              }
      
            }
          }
        }
      
        private final class Mutator implements Runnable
        {
      
          private final Map<String, String> map;
          private final Random random = new Random();
      
          public Mutator(Map<String, String> map)
          {
            this.map = map;
          }
      
          @Override
          public void run()
          {
            for (int i = 0; i < 100; i++)
            {
              this.map.remove("key" + random.nextInt(MAP_SIZE));
              this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString());
            }
          }
        }
      
        private void run()
        {
          Accessor a1 = new Accessor(this.iterator);
          Accessor a2 = new Accessor(this.iterator);
          Mutator m = new Mutator(this.map);
      
          executor.execute(a1);
          executor.execute(m);
          executor.execute(a2);
        }
      }
      

      一旦您开始在访问器和修改器线程之间共享相同的Iterator&lt;Map.Entry&lt;String, String&gt;&gt;java.lang.IllegalStateExceptions 就会开始弹出。

      import java.util.Iterator;
      import java.util.Map;
      import java.util.Random;
      import java.util.UUID;
      import java.util.concurrent.ConcurrentHashMap;
      import java.util.concurrent.ExecutorService;
      import java.util.concurrent.Executors;
      
      public class ConcurrentMapIteration
      {
        private final Map<String, String> map = new ConcurrentHashMap<String, String>();
        private final Iterator<Map.Entry<String, String>> iterator;
      
        private final static int MAP_SIZE = 100000;
      
        public static void main(String[] args)
        {
          new ConcurrentMapIteration().run();
        }
      
        public ConcurrentMapIteration()
        {
          for (int i = 0; i < MAP_SIZE; i++)
          {
            map.put("key" + i, UUID.randomUUID().toString());
          }
          this.iterator = this.map.entrySet().iterator();
        }
      
        private final ExecutorService executor = Executors.newCachedThreadPool();
      
        private final class Accessor implements Runnable
        {
          private final Iterator<Map.Entry<String, String>> iterator;
      
          public Accessor(Iterator<Map.Entry<String, String>> iterator)
          {
            this.iterator = iterator;
          }
      
          @Override
          public void run()
          {
            while (iterator.hasNext())
            {
              Map.Entry<String, String> entry = iterator.next();
              try
              {
                String st =
                    Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']';
              } catch (Exception e)
              {
                e.printStackTrace();
              }
      
            }
          }
        }
      
        private final class Mutator implements Runnable
        {
      
          private final Random random = new Random();
      
          private final Iterator<Map.Entry<String, String>> iterator;
      
          private final Map<String, String> map;
      
          public Mutator(Map<String, String> map, Iterator<Map.Entry<String, String>> iterator)
          {
            this.map = map;
            this.iterator = iterator;
          }
      
          @Override
          public void run()
          {
            while (iterator.hasNext())
            {
              try
              {
                iterator.remove();
                this.map.put("key" + random.nextInt(MAP_SIZE), UUID.randomUUID().toString());
              } catch (Exception ex)
              {
                ex.printStackTrace();
              }
            }
      
          }
        }
      
        private void run()
        {
          Accessor a1 = new Accessor(this.iterator);
          Accessor a2 = new Accessor(this.iterator);
          Mutator m = new Mutator(map, this.iterator);
      
          executor.execute(a1);
          executor.execute(m);
          executor.execute(a2);
        }
      }
      

      【讨论】:

      • 您确定“在访问线程之间共享相同的迭代器会导致死锁”吗?该文件说读取没有被阻止,我尝试了你的程序,还没有发生死锁。虽然迭代结果会出错。
      【解决方案4】:

      This 可能会给你一个很好的见解

      ConcurrentHashMap 通过稍微放宽它对调用者的承诺来实现更高的并发性。检索操作将返回由最近完成的插入操作插入的值,也可能返回由同时进行的插入操作添加的值(但在任何情况下都不会返回无意义的结果)。 由 ConcurrentHashMap.iterator() 返回的迭代器将最多返回每个元素一次,并且永远不会抛出 ConcurrentModificationException,但可能会或可能不会反映自构造迭代器以来发生的插入或删除。在迭代集合时,不需要(甚至不可能)表范围的锁定来提供线程安全。在任何不依赖锁定整个表以防止更新的能力的应用程序中,ConcurrentHashMap 都可以用作 synchronizedMap 或 Hashtable 的替代品。

      关于这个:

      但是,迭代器被设计为一次只能由一个线程使用。

      这意味着,虽然在两个线程中使用 ConcurrentHashMap 生成的迭代器是安全的,但它可能会导致应用程序中出现意外结果。

      【讨论】:

        【解决方案5】:

        这意味着你不应该在多个线程之间共享一个迭代器对象。创建多个迭代器并在单独的线程中同时使用它们很好。

        【讨论】:

        • 有什么原因你没有在迭代器中将 I 大写?由于它是类的名称,它可能不会那么混乱。
        • @Bill Michell,现在我们进入了发布礼仪的语义。我认为他应该让 Iterator 成为一个指向 Iterator 的 javadoc 的链接,或者至少将它放在内联代码注释 (`) 中。
        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-03-18
        • 2012-08-20
        • 2014-03-04
        • 2012-12-18
        • 2016-07-16
        • 2014-12-12
        • 1970-01-01
        相关资源
        最近更新 更多