Java ConcurrentHashMap与示例

时间:2020-01-09 10:35:09  来源:igfitidea点击:

Java中的ConcurrentHashMap是线程安全的Map实现,除了HashTable或者显式同步HashMap之外,它还提供了另一种在多线程环境中使用的替代方法。 ConcurrentHashMap是java.util.concurrent包的一部分。

ConcurrentHashMap如何是更好的选择

其他线程安全的实现,例如HashTable或者HashMap的显式同步,将所有方法同步到一个锁上,并且所有方法都被同步,即使这些方法用于检索元素。这使得这些选项非常慢

  • 由于整个集合都被锁定,因此在给定的时间只有一个线程可以访问它。

  • 由于所有方法都是同步的,因此读取操作也很慢。

Java中的ConcurrentHashMap试图解决这些问题

  • 通过不锁定诸如get()之类的检索操作的集合。仅允许同步写入操作,并发读取操作才被允许。

  • 即使对于写操作,整个集合也不会被锁定,而是仅根据计算出的哈希码将表中必须放置元素的部分锁定。

Java中ConcurrentHashMap的内部实现

为了存储其元素,ConcurrentHashMap在内部使用名为Node类型的表的数组。

transient volatile Node<K,V>[] table;

在此节点类代表的键值项,在ConcurrentHashMap实现定义为静态类。节点类具有用于存储键和值的字段,还具有用于保存对下一节点的引用的下一字段。

static class Node<K,V> implements Map.Entry<K,V> {
  final int hash;
  final K key;
  volatile V val;
  volatile Node<K,V> next; 
  .....

此处请注意,在Java 5中的ConcurrentHashMap的初始实现中,使用了数组Segment,默认情况下提供的并发级别为16,即16个线程可以访问存储在数组的不同索引中的16个元素,因为每个段可以独立锁定。但是,从Java 8开始,对ConcurrentHashMap的内部实现进行了修改,现在使用了名为table的数组,并且在写操作期间,它主要使用Compare-And-Swap(CAS)操作并发。

通过同步特定存储区的第一个节点,仍然可以独立锁定表中的每个数组索引。

Java ConcurrentHashMap构造函数

  • ConcurrentHashMap()–使用默认的初始表大小(16)创建一个新的空地图。

  • ConcurrentHashMap(int initialCapacity)–创建一个新的空映射,其初始表大小可容纳指定数量的元素,而无需动态调整大小。

  • ConcurrentHashMap(int initialCapacity,float loadFactor)–根据给定的元素数量(initialCapacity)和初始表密度(loadFactor)创建一个具有初始表大小的新的空地图。

  • ConcurrentHashMap(int initialCapacity,float loadFactor,int concurrencyLevel)–根据给定的元素数(initialCapacity),表密度(loadFactor)和并发更新线程数(concurrencyLevel)创建一个初始表大小为空的新映射。

  • ConcurrentHashMap(Map <?扩展K ,?扩展V> m)–创建一个与给定映射具有相同映射的新映射。

Java示例,创建一个ConcurrentHashMap

在此示例中,创建了ConcurrentHashMap并向其添加了(键,值)对,随后显示。

import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class CHMExample {
  public static void main(String[] args) {
    // Creating ConcurrentHashMap
    Map<String, String> carMap = new ConcurrentHashMap<String, String>();
    // Storing elements
    carMap.put("1", "Audi");
    carMap.put("2", "BMW");
    carMap.put("3", "Jaguar");
    carMap.put("4", "Mini Cooper");
    
    for (Map.Entry<String, String> entry : carMap.entrySet()) {
      System.out.println("key- " + entry.getKey() + " value- " + entry.getValue());
    }
  }
}

输出:

key- 1 value- Audi
key- 2 value- BMW
key- 3 value- Jaguar
key- 4 value- Mini Cooper

Java ConcurrentHashMap中不允许为Null

ConcurrentHashMap不允许插入null作为键或者值。因此,以下两个语句均导致NullPointerException。

carMap.put(null, "Audi");

Exception in thread "main" java.lang.NullPointerException
carMap.put("1", null);

Exception in thread "main" java.lang.NullPointerException

Java中的ConcurrentHashMap是线程安全的

Java中的ConcurrentHashMap在多线程环境中可以安全使用。让我们看一个例子,我们首先尝试使用4个线程在HashMap中插入400个元素(这不是线程安全的),每个线程都插入100个元素。执行后,HashMap的预期大小为400。

import java.util.HashMap;
import java.util.Map;

public class MapSynchro implements Runnable{
  private Map<String, String> testMap;
  public MapSynchro(Map<String, String> testMap){
    this.testMap = testMap;
  }

  public static void main(String[] args) {
    Map<String, String> testMap = new HashMap<String, String>();
    /// 4 threads
    Thread t1 = new Thread(new MapSynchro(testMap));
    Thread t2 = new Thread(new MapSynchro(testMap));
    Thread t3 = new Thread(new MapSynchro(testMap));
    Thread t4 = new Thread(new MapSynchro(testMap));
        
    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 Map is " + testMap.size());
  }
  @Override
  public void run() {
    System.out.println("in run method" + Thread.currentThread().getName());
    String str = Thread.currentThread().getName();
    for(int i = 0; i < 100; i++){
      // adding thread name to make element unique
      testMap.put(str+i, str+i);
      try {
        // delay to verify thread interference
        Thread.sleep(100);
      } catch (InterruptedException e) {
        e.printStackTrace();
      }
    }
  }
}

输出:

in run methodThread-3
in run methodThread-0
in run methodThread-1
in run methodThread-2
Size of Map is 394

如我们所见,由于线程干扰,一次运行的大小为394.

使用ConcurrentHashMap可以消除这种不一致。我们只需要更改代码中的以下行。

Map<String, String> testMap = new ConcurrentHashMap<String, String>();

现在大小始终为400。

Java ConcurretHashMap返回故障保护迭代器

由ConcurrentHashMap返回的迭代器是故障安全的,并且如果在创建迭代器之后的任何时间都对结构进行了修改,则不会引发ConcurrentModificationException。

public class FailSafeDemo {
  public static void main(String[] args) {
    Map<String, String> carMap = new ConcurrentHashMap<String, String>();
    carMap.put("1", "Audi");
    carMap.put("2", "BMW");
    carMap.put("3", "Jaguar");
    carMap.put("4", "Mini Cooper");
    // iterating map
    Iterator<Map.Entry<String, String>> itr = carMap.entrySet().iterator();
    while(itr.hasNext()) {
      Map.Entry<String, String&gt; entry = itr.next();
      System.out.println("Key is " + entry.getKey() + " Value is " + entry.getValue());
      carMap.put("5", "Mercedes");
    }
    System.out.println("Size- " + carMap.size());
  }
}

输出:

Key is 1 Value is Audi
Key is 2 Value is BMW
Key is 3 Value is Jaguar
Key is 4 Value is Mini Cooper
Key is 5 Value is Mercedes
Size- 5

在代码中,在迭代ConcurrentHashMap时,向其中添加了一个新元素,这不会导致抛出ConcurrentModificationException。

ConcurrentHashMap中的原子操作

即使Java ConcurrentHashMap是线程安全的,但原子操作仍然可能在多线程环境中产生不一致的结果。例如,以下情况。

Integer oldVal = CHMMap.get(key); 
Integer newVal = (oldVal== null) ? 1 : oldVal + 1;
// newValue stored by another thread
CHMMap.put(key, newValue);

在这里,如果在执行此line之后执行线程被另一个线程预占了,则整数Integer newVal =(oldVal == null) 1:oldVal +1;那么放回ConcurrentHashMap中的值可能不正确。在这种情况下,最好使用原子操作。 ConcurrentHashMap类中的一些原子操作是

  • putIfAbsent(K键,V值)–如果指定的键尚未与值关联,则将其与给定的值关联。

  • remove(Object key,Object value)–仅在当前映射到给定值时才删除键的条目。

  • computeIfAbsent(K键,Function <?super K ,?扩展V> mappingFunction)–如果指定的键尚未与值关联,则尝试使用给定的映射函数计算其值,除非该值为null,否则将其输入到此映射中。

  • computeIfPresent(K键,BiFunction <?super K,?super V ,?扩展V> remappingFunction)–如果存在指定键的值,则尝试计算给定键及其当前映射值的新映射。

  • compute(K键,BiFunction <?super K,?super V ,?扩展V> remappingFunction)–尝试计算指定键及其当前映射值的映射(如果没有当前映射,则为null)。

  • merge(K键,V值,BiFunction <?super V,?super V ,?扩展V> remappingFunction)–如果指定的键尚未与(非null)值关联,请将其与给定值关联。

使用原子操作计算,上面列出的方案可以写成如下:

CHMMap.compute(key, (k,v)-> v == null ? 1 : v + 1);

ConcurrentHashMap的优缺点

  • 如果在多线程环境中允许并发读取操作,则读取次数多于写入次数,则Java中的ConcurrentHashMap的性能会更好。由于检索操作是非阻塞的,因此可能与更新操作(包括放置和删除)重叠。因此,并发检索可能会或者可能不会反映某些条目的插入或者删除。

  • 如果ConcurrentHashMap中的写入和更新次数更多,并且HashCode实现不正确,则许多元素可能具有相同的哈希码。在这种情况下,大多数线程将需要访问要存储具有相同哈希码的元素的相同表索引,从而导致性能下降。

  • ConcurrentHashMap中的迭代器设计为一次只能由一个线程使用。