在JDK1.8里面,ConcurrentHashMap
在put方法里面已经将分段锁移除了,转而是CAS锁和synchronized
ConcurrentHashMap
是Java里面同时兼顾性能和线程安全的一个键值对集合,同属于键值对的集合还有HashTable
以及HashMap
,HashTable
是一个线程安全的类,因为它的所有public
方法都被synchronized
修饰,这样就导致了一个问题,就是效率太低。
虽然HashMap
在JDK1.8
的并发场景下触发扩容时不会出现成环了,但是会出现数据丢失的情况。
所以如果需要在多线程的情况下(多读少写))使用Map集合的话,ConcurrentHashMap
是一个不错的选择。
ConcurrentHashMap
在JDK1.8的时候将put()方法中的分段锁Segment
移除,转而采用一种CAS
锁和synchronized
来实现插入方法的线程安全。
如下代码:
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
//省略相关代码
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) {
//省略相关代码
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);
return null;
}
可以看到在JDK1.8
里面,ConcurrentHashMap
是直接采用数组
+链表
+红黑树
来实现,时间复杂度在O(1)和O(n)之间,如果链表转化为红黑树了,那么就是O(1)到O(nlogn)。
在这里值得一提的是,ConcurrentHashMap
会判断tabAt(tab, i = (n - 1) & hash)
是不是 null,是的话就直接采用CAS
进行插入,而如果不为空的话,则是synchronized
锁住当前Node
的首节点,这是因为当该Node
不为空的时候,证明了此时出现了Hash
碰撞,就会涉及到链表
的尾节点新增或者红黑树
的节点新增以及红黑树
的平衡,这些操作自然都是非原子性的。
从而导致无法使用CAS
,当Node
的当前下标为null的时候,由于只是涉及数组的新增,所以用CAS
即可。
因为CAS是一种基于版本控制的方式来实现,而碰撞之后的操作太多,所以直接用synchronized
比较合适。
ConcurrentHashMap在迭代时和HashMap的区别
当一个集合在迭代的时候如果动态的添加或者删除元素,那么就会抛出Concurrentmodificationexception
,但是在ConcurrentHashMap
里面却不会,例如如下代码:
public static void main(String[] args) {
Map<String,String> map = new ConcurrentHashMap<String, String>();
map.put("a","a1");
map.put("b","b1");
map.put("c","c1");
map.put("d","d1");
map.put("e","e1");
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()){
String it = iterator.next();
if("b".equals(it)){
map.remove("d");
}
System.out.println(it);
}
}
控制台打印如下:
a
b
c
e
而当你把ConcurrentHashMap
换成HashMap
的时候,控制台就会抛出一个异常:
Exception in thread "main" a
b
java.util.ConcurrentModificationException
at java.util.HashMap$HashIterator.nextNode(HashMap.java:1442)
at java.util.HashMap$KeyIterator.next(HashMap.java:1466)
at xyz.somersames.ListTest.main(ListTest.java:22)
原因在于ConcurrentHashMap
的next
方法并不会去检查modCount
和expectedModCount
,但是会检查下一个节点是不是为空
if ((p = next) == null)
throw new NoSuchElementException();
当我们进行remove的时候,ConcurrentHashMap
会直接通过修改指针的方式来进行移除操作,同样的,也会锁住数组
的头节点直至移除结束,所以在同一个时刻,只会有一个线程对当前数组下标的所有节点
进行操作。
但是在HashMap
里面,next
方法会进行一个check,而remove操作会修改modCount
,导致modCount
和expectedModCount
不相等,所以就会导致ConcurrentModificationException
稍微修改下代码:
public static void main(String[] args) {
Map<String,String> map = new ConcurrentHashMap<String, String>();
map.put("a","a1");
map.put("b","b1");
map.put("c","c1");
map.put("d","d1");
map.put("e","e1");
Iterator<String> iterator = map.keySet().iterator();
while (iterator.hasNext()){
if("b".equals(iterator.next())){
map.remove("d");
}
System.out.println(iterator.next());
}
}
控制台打印如下:
b
d
Exception in thread "main" java.util.NoSuchElementException
at java.util.concurrent.ConcurrentHashMap$KeyIterator.next(ConcurrentHashMap.java:3416)
at com.xzh.ssmtest.ListTest.main(ListTest.java:25)
并发下的处理
由于每一个Node
的首节点都会被synchronized
修饰,从而将一个元素的新增转化为一个原子操作,同时Node
的value
和next
都是由volatile
关键字进行修饰,从而可以保证可见性。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。