Java1.5 引入了 java.util.concurrent 包,其中 Collection 类的实现允许在运行过程中修改集合对象。实际上, Java 的集合框架是[迭代器设计模式]的一个很好的实现。

为什么需要使用 ConcurrentHashMap ?

HashMap 不是线程安全的,因此多线程操作需要注意,通常使用 HashTable 或者 Collections.synchronizedMap() 来返回线程安全的 HashMap ,但是这两种方法都是对所有方法实现同步,导致读写性能比较低,而 ConcurrentHashMap 引入“分段锁”的概念,可以理解为把一个大的 Map 差分成小的 HashTable ,根据 key.hashCode() 来决定把 key 放到哪个 HashTable 中去。

就是把 Map 分成了N个 Segment , put 和 get 的时候,都是现根据 key.hashCode() 算出放到哪个Segment中:

图片.png

图片.png

图片.png

图片.png

对比:

ConcurrentHashMap 与 HashMap 很相似,但是它支持在运行时修改集合对象。

例子:

ConcurrentHashMapExample.java

import java.util.HashMap;

import java.util.Iterator;

import java.util.Map;

import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {

public static void main(String[] args) {

//ConcurrentHashMap

Map myMap = new ConcurrentHashMap();

myMap.put("1", "1");

myMap.put("2", "1");

myMap.put("3", "1");

myMap.put("4", "1");

myMap.put("5", "1");

myMap.put("6", "1");

System.out.println("ConcurrentHashMap before iterator: "+myMap);

Iterator it = myMap.keySet().iterator();

while(it.hasNext()){

String key = it.next();

if(key.equals("3")) myMap.put(key+"new", "new3");

}

System.out.println("ConcurrentHashMap after iterator: "+myMap);

//HashMap

myMap = new HashMap();

myMap.put("1", "1");

myMap.put("2", "1");

myMap.put("3", "1");

myMap.put("4", "1");

myMap.put("5", "1");

myMap.put("6", "1");

System.out.println("HashMap before iterator: "+myMap);

Iterator it1 = myMap.keySet().iterator();

while(it1.hasNext()){

String key = it1.next();

if(key.equals("3")) myMap.put(key+"new", "new3");

}

System.out.println("HashMap after iterator: "+myMap);

}

}

输出如下:

ConcurrentHashMap before iterator: {1=1, 5=1, 6=1, 3=1, 4=1, 2=1}

ConcurrentHashMap after iterator: {1=1, 3new=new3, 5=1, 6=1, 3=1, 4=1, 2=1}

HashMap before iterator: {3=1, 2=1, 1=1, 6=1, 5=1, 4=1}

Exception in thread "main" java.util.ConcurrentModificationException

at java.util.HashMap$HashIterator.nextEntry(HashMap.java:793)

at java.util.HashMap$KeyIterator.next(HashMap.java:828)

at com.test.ConcurrentHashMapExample.main(ConcurrentHashMapExample.java:44)

明显 ConcurrentHashMap 可以支持向 map 中添加新元素,而 HashMap 则抛出了 ConcurrentModificationException。

介绍 ConcurrentHashMap:

ConcurrentHashMap (简称 CHM )是在 Java 1.5作为 Hashtable 的替代选择新引入的,是 concurrent 包的重要成员。在 Java 1.5之前,如果想要实现一个可以在多线程和并发的程序中安全使用的 Map ,只能在 HashTable 和 synchronized Map 中选择,因为 HashMap 并不是线程安全的。但再引入了 CHM 之后,我们有了更好的选择。 CHM 不但是线程安全的,而且比 HashTable 和 synchronizedMap 的性能要好。相对于 HashTable 和 synchronizedMap 锁住了整个 Map , CHM 只锁住部分 Map 。 CHM 允许并发的读操作,同时通过同步锁在写操作时保持数据完整性。

Java 中 ConcurrentHashMap 的实现:

CHM 引入了分割,并提供了 HashTable 支持的所有的功能。在 CHM 中,支持多线程对 Map 做读操作,并且不需要任何的 blocking --因为 CHM 将 Map 分割成了不同的部分,在执行风行操作时只锁住一部分。根据默认的并发级别, Map 被分割成16个部分,并且由不同的锁控制。这意味着,同时最多可以有16个写线程操作 Map 。

另外一个重点是在迭代遍历 CHM 时, keySet 返回的 iterator 是弱一直和 fail-safe 的,可能不会返回某些最近的改变,并且在遍历中,如果已经遍历的数组上的内容发生了变化,是不会抛出 ConcurrentModificationException 的异常。

什么时候使用 ConcurrentHashMap ?(待求证)

CHM 适用于读者数量超过写者时,当写者数量大于等于读者时, CHM 的性能是低于 Hashtable 和 synchronized Map 的。这是因为当锁住了整个Map时,读操作要等待对同一部分执行写操作的线程结束。 CHM 适用于做 cache ,在程序启动时初始化,之后可以被多个请求线程访问。正如 Javadoc 说明的那样, CHM 是 HashTable 一个很好的替代,但要记住, CHM 的比 HashTable 的同步性稍弱。

ConcurrentHashMap 小总结:

*** CHM 允许并发的读和线程安全的更新操作

***在执行写操作时, CHM 只锁住部分的 Map

***并发的更新是通过内部根据并发级别将 Map 分割成小部分实现的

***高的并发级别会造成时间和空间的浪费,低的并发级别在写线程多时会引起线程间的竞争

*** CHM 的所有操作都是线程安全

*** CHM 返回的迭代器是弱一致性, fail-safe 并且不会抛出 ConcurrentModificationException 异常

*** CHM 不允许 null 的键值

***可以使用 CHM 代替 HashTable,但要记住 CHM 不会锁住整个 Map

HashMap 与 ConcurrentHashMap对比:

HashMap 的结构:

image

简单来说, HashMap 是一个Entry对象的数组。数组中的每一个 Entry 元素,又是一个链表的头节点。

Hashmap 不是线程安全的。在高并发环境下做插入操作,有可能出现下面的环形链表:

image

Segment 是什么呢? Segment 本身就相当于一个 HashMap 对象。

同 HashMap 一样, Segment 包含一个 HashEntry 数组,数组中的每一个 HashEntry 既是一个键值对,也是一个链表的头节点。

单一的 Segment 结构如下:

image

像这样的 Segment 对象,在 ConcurrentHashMap 集合中有多少个呢?有2的N次方个,共同保存在一个名为 segments 的数组当中。

因此整个ConcurrentHashMap的结构如下:

image

可以说,ConcurrentHashMap 是一个二级哈希表。在一个总的哈希表下面,有若干个子哈希表。

这样的二级结构,和数据库的水平拆分有些相似。

附上 ConcurrentHashMap 遍历方式:

import java.util.Iterator;

import java.util.Map;

import java.util.concurrent.ConcurrentHashMap;

/**

Java 中遍历 Map 的四种方式,这里使用的是 ConcurrentHashMap,

可以替换为 HashMap

*/

public class IteratorMap {

public static void main(String[] args) {

Map map = new ConcurrentHashMap();

init(map);

//方式一:在 for-each 循环中使用 entries 来遍历

System.out.println("方式一:在 for-each 循环中使用 entries 来遍历");

for(Map.Entry entry: map.entrySet()) {

System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());

}

//方法二:在 for-each 循环中遍历 keys 或 values ,这种方式适用于需要值或者键的情况,方法二比方法一快了10%

System.out.println("方法二:在 for-each 循环中遍历 keys 或 values ,这种方式适用于需要值或者键的情况");

//遍历键

for(String key : map.keySet()) {

System.out.println("key = " + key);

}

//遍历值

for(String value : map.values()) {

System.out.println("value = " + value);

}

//方法三:使用 Iterator 遍历,使用并发集合不会报异常,性能类似于方法二

//使用泛型

Iterator> entries = map.entrySet().iterator();

System.out.println("使用 Iterator 遍历,并且使用泛型:");

while (entries.hasNext()) {

Map.Entry entry = entries.next();

System.out.println("Key = " + entry.getKey() + ", Value = " + entry.getValue());

//注意这里操作了集合,下面的的遍历不会再打印0

if("0".equals(entry.getKey())) {

map.remove(entry.getKey());

}

}

//不使用泛型

Iterator entrys = map.entrySet().iterator();

System.out.println("使用 Iterator 遍历,并且不使用泛型");

while (entrys.hasNext()) {

Map.Entry entry = (Map.Entry) entrys.next();

String key = (String)entry.getKey();

String value = (String)entry.getValue();

System.out.println("Key = " + key + ", Value = " + value);

}

//方式四:通过键找值遍历,该方法效率相当低,不建议使用

System.out.println("方式四:通过键找值遍历");

for (String key : map.keySet()) {

String value = map.get(key);

System.out.println("Key = " + key + ", Value = " + value);

}

}

/**

初始化 Map

@param map

*/

private static void init(Map map) {

if(map == null) {

throw new RuntimeException("参数为空,无法执行初始化");

}

for(int i = 0; i < 10; i ++) {

map.put(String.valueOf(i), String.valueOf(i));

}

}

}

运行结果:

方式一:在 for-each 循环中使用 entries 来遍历

Key = 0, Value = 0

Key = 1, Value = 1

Key = 2, Value = 2

Key = 3, Value = 3

Key = 4, Value = 4

Key = 5, Value = 5

Key = 6, Value = 6

Key = 7, Value = 7

Key = 8, Value = 8

Key = 9, Value = 9

方法二:在 for-each 循环中遍历 keys 或 values ,这种方式适用于需要值或者键的情况

key = 0

key = 1

key = 2

key = 3

key = 4

key = 5

key = 6

key = 7

key = 8

key = 9

value = 0

value = 1

value = 2

value = 3

value = 4

value = 5

value = 6

value = 7

value = 8

value = 9

使用Iterator遍历,并且使用泛型:

Key = 0, Value = 0

Key = 1, Value = 1

Key = 2, Value = 2

Key = 3, Value = 3

Key = 4, Value = 4

Key = 5, Value = 5

Key = 6, Value = 6

Key = 7, Value = 7

Key = 8, Value = 8

Key = 9, Value = 9

使用Iterator遍历,并且不使用泛型

Key = 1, Value = 1

Key = 2, Value = 2

Key = 3, Value = 3

Key = 4, Value = 4

Key = 5, Value = 5

Key = 6, Value = 6

Key = 7, Value = 7

Key = 8, Value = 8

Key = 9, Value = 9

方式四:通过键找值遍历

Key = 1, Value = 1

Key = 2, Value = 2

Key = 3, Value = 3

Key = 4, Value = 4

Key = 5, Value = 5

Key = 6, Value = 6

Key = 7, Value = 7

Key = 8, Value = 8

Key = 9, Value = 9