Java ConcurrentHashMap与示例
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> 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中的迭代器设计为一次只能由一个线程使用。