迭代ConcurrentHashMap的值是否线程安全?
迭代ConcurrentHashMap的值是否线程安全?
在ConcurrentHashMap的javadoc中,有以下内容:\n
\n检索操作(包括get)通常不会阻塞,因此可能与更新操作(包括put和remove)重叠。检索反映了在其开始时保持的最近完成的更新操作的结果。对于像putAll和clear这样的聚合操作,同时进行的检索可能仅反映了某些条目的插入或删除。类似地,迭代器和枚举器返回在迭代器/枚举器创建时或自其创建以来的某一点的哈希表状态反映的元素。它们不会抛出ConcurrentModificationException异常。然而,迭代器只设计为一次由一个线程使用。\n
\n这是什么意思?如果我尝试同时使用两个线程迭代映射会发生什么?如果我在迭代映射时放入或删除一个值会发生什么?
从上述内容中可以整理出(Is iterating ConcurrentHashMap values thread safe?)这个问题的出现的原因以及解决方法。
问题的出现原因是由于在多个线程之间共享了一个迭代器对象,而在ConcurrentHashMap中,迭代器对象是不具备线程安全性的。所以,如果多个线程同时使用同一个迭代器对象进行迭代操作,就会导致线程安全问题。
解决这个问题的方法是,应该为每个线程创建一个独立的迭代器对象,并在各个线程中分别使用这些独立的迭代器对象进行迭代操作。这样可以避免多个线程之间对同一个迭代器对象进行并发操作,从而保证线程安全。
代码示例:
ConcurrentHashMapmap = new ConcurrentHashMap<>(); // 添加元素到ConcurrentHashMap中 // 创建一个独立的迭代器对象 Iterator iterator = map.values().iterator(); // 在各个线程中使用独立的迭代器对象进行迭代操作 while (iterator.hasNext()) { Integer value = iterator.next(); // 对value进行处理 }
通过以上的解决方法,我们可以确保在多线程环境下迭代ConcurrentHashMap的values是线程安全的。
从上面的内容中可以看出,问题的原因是由于多个线程共享了同一个迭代器(iterator)导致的。在第一个示例中,使用了ConcurrentHashMap的entrySet方法返回的迭代器进行遍历操作,由于ConcurrentHashMap是线程安全的,所以不会抛出异常。但是在第二个示例中,将迭代器作为参数传递给了多个Accessor线程和一个Mutator线程,这样就会导致线程之间相互干扰,进而引发并发问题。具体来说,当Accessor线程和Mutator线程同时对迭代器进行操作时,会发生java.lang.IllegalStateException异常。
解决这个问题的方法是,每个线程都使用自己的迭代器进行遍历操作,而不是共享同一个迭代器。可以在每个线程中创建一个新的迭代器对象,并将其作为参数传递给相应的线程。这样就可以避免多个线程之间的干扰,确保线程安全。
以下是整理后的文章:
使用ConcurrentHashMap的values方法进行迭代操作是否线程安全?
在使用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 Mapmap = new ConcurrentHashMap (); 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 map; public Accessor(Map map) { this.map = map; } public void run() { for (Map.Entry entry : this.map.entrySet()) { System.out.println( Thread.currentThread().getName() + " - [" + entry.getKey() + ", " + entry.getValue() + ']' ); } } } private final class Mutator implements Runnable { private final Map map; private final Random random = new Random(); public Mutator(Map map) { this.map = map; } 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); } }
在上述示例中,我们使用了ConcurrentHashMap的values方法返回的迭代器进行遍历操作,并没有抛出任何异常。这是因为ConcurrentHashMap是线程安全的,可以在多个线程中同时进行读取操作。
但是,当我们将迭代器作为参数传递给多个Accessor线程和一个Mutator线程时,就会出现线程安全的问题。具体来说,当Accessor线程和Mutator线程同时对迭代器进行操作时,会发生java.lang.IllegalStateException异常。
为了解决这个问题,我们可以为每个线程创建一个新的迭代器对象,并将其作为参数传递给相应的线程。这样就可以避免多个线程之间的干扰,确保线程安全。
以下是修改后的示例代码:
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 Mapmap = new ConcurrentHashMap (); private final Iterator > 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 > iterator; public Accessor(Iterator > iterator) { this.iterator = iterator; } public void run() { while (iterator.hasNext()) { Map.Entry 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 map; private final Random random = new Random(); private final Iterator > iterator; public Mutator(Map map, Iterator > iterator) { this.map = map; this.iterator = iterator; } 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); } }
通过为每个线程创建一个新的迭代器对象,我们可以避免迭代器的共享和线程安全问题。
在使用ConcurrentHashMap进行迭代时,可能会出现线程安全的问题。原因是每个从ConcurrentHashMap获得的迭代器都是为单个线程设计的,不应该传递给其他线程使用。这包括for-each循环提供的语法糖。
如果尝试在两个线程同时迭代该映射,只要每个线程使用自己的迭代器,就可以正常工作。
如果在迭代映射时放入或移除值,保证不会出现问题(这是ConcurrentHashMap中“concurrent”一词的一部分含义)。然而,并不能保证一个线程将看到另一个线程对映射所做的更改(除非从映射中获取一个新的迭代器)。迭代器保证反映其创建时映射的状态。后续的更改可能会反映在迭代器中,但不一定会如此。
总之,像下面的语句:
for (Object o : someConcurrentHashMap.entrySet()) { // ... }
几乎每次看到它时都是可行的(或者至少是安全的)。但是,如果在迭代过程中,另一个线程从映射中移除了一个对象o10,会发生什么呢?即使已经被移除,我还能在迭代中看到o10吗?
如上所述,没有明确规定现有的迭代器是否会反映对映射的后续更改。因此,我不知道,根据规范,没有人知道(除非查看代码,而且每次运行时更新可能会改变)。因此,您不能依赖它。
但是,如果在迭代ConcurrentHashMap时仍然遇到ConcurrentModificationException异常,那么应该提供触发该异常的代码,并发布一个新问题,但我非常怀疑这是直接从迭代并发容器中引起的问题,除非Java实现有错误。