文章目录
相关文章:
《ConcurrentHashMap原理探究》
1. 前言
我们之前写过关于ArrayList的线程安全相关的文章《java.util.ConcurrentModificationException异常详解&ArrayList&CopyOnWriteArrayList原理探究》,那么对于HashMap里面的线程安全相关知识是什么样的呢,今天就来汇总下。
2. 回顾下ArrayList
从之前的文章,我们得知:
-
ArrayList是非线程安全的
原因是未加锁 -
ArrayList在迭代期间,可能引起ConcurrentModificationException异常
原因是如果数组发生删除操作,Iterator迭代器在迭代期间会进行modCount != expectedModCount
比较,导致抛出异常java.util.ConcurrentModificationException,作用是提醒用户发生了数据不一致的情况,请用户捕获处理遵守fail-fast 原则,详情参考《java中快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?》
-
可以使用CopyOnWriteArrayList替代ArrayList
作用是正在迭代的数组不会产生异常ConcurrentModificationException,原因是读操作是个备份机制,别的操作不影响当前的快照;因为快照机制,产生了弱一致性问题,即数组发生变化,不会立即体现在快照数组中 -
CopyOnWriteArrayList也有缺陷,不能在迭代期间调用Iterator.remove(),Iterator.add()操作
原因是读操作是对快照进行的,如果在迭代期间调用Iterator.remove(),只会删除快照里面的数据,而不会影响原数组,故而直接禁止调用相关的方法。
我们下面仿照ArrayList的顺序,解说HashMap,以便进行对比,方便记忆
3. HashMap
3.1 为什么HashMap是非线程安全的?
简单来说,就是在添加、删除、扩容时,会导致脏数据,详情可以参考:
《HashMap为什么是线程不安全的?》
3.2 HashMap迭代期间,可能引起ConcurrentModificationException异常
抛出ConcurrentModificationException异常和ArrayList抛出该异常原理一致,都是在迭代期间发现数据不一致的情况,请用户捕获处理。
源码如下:
private abstract class HashIterator{
final Entry<K,V> nextEntry() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException(); //抛出异常的根本原因
Entry<K,V> e = next;
...
return e;
}
}
可以看到在迭代过程中出现modCount != expectedModCount
判断条件
3.3 使用ConcurrentHashMap替代HashMap
ConcurrentHashMap
可以做到读取数据不加锁,并且其内部的结构可以让其在进行写操作的时候能够将锁的粒度保持地尽量地小,允许多个修改操作并发进行,其关键在于使用了锁分离技术。它使用了多个锁来控制对hash
表的不同部分进行的修改。ConcurrentHashMap
内部使用段(Segment
)来表示这些不同的部分,每个段其实就是一个小的Hashtable
,它们有自己的锁。只要多个修改操作发生在不同的段上,它们就可以并发进行。
简单来说,就是通过写操作加锁(锁粒度很小,尽可能的减少冲突),保证了写操作的一致性,因此读操作在迭代期间不会抛出ConcurrentModificationException异常,确实解决了我们的难题;并且通过Segment段的技术(JDK1.7),提升了加锁效率;并且读不加锁,提升读的效率。
从底层原理来看,ConcurrentHashMap与CopyOnWriteArrayList实现原理是不同的
ConcurrentHashMap也存在弱一致性问题
虽然加锁机制保证了写操作的一致性,但是读操作未加锁,故而如果存在读写操作,读操作可能读取到写操作的中间态,有关弱一致性,可以参考《concurrent包下并发容器背后的设计理念 - 锁分段、写时复制和弱一致性》
3.3 ConcurrentHashMap迭代期间能执行Iterator.remove()等操作吗
首先给出答案:是可以的,给个例子验证下,把entrySet()
方法转为为Iterator:
import java.util.Iterator;
import java.util.Map.Entry;
import java.util.concurrent.ConcurrentHashMap;
public class HashMapTest {
public static void main(String[] args) {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>();
map.put("a", "a1");
map.put("b", "b1");
Iterator<Entry<String, String>> it = map.entrySet().iterator(); //entrySet
while (it.hasNext()) {
Entry<String, String> o = it.next();
it.remove(); //调用Iterator.remove()
System.out.println(o.getValue());
}
System.out.println(map.size()); //输出0
}
}
以上代码能正确编译执行,并且打印的结果也说明符合预期,map中的元素都被删除了。
那么为什么此时可以执行Iterator.remove()
呢,原因是Iterator.remove()
复用了ConcurrentHashMap.remove()
通过断点,发现ConcurrentHashMap的entrySet()
方法的迭代器是HashIterator
,我们看下源码:
public final void remove() {
if (lastReturned == null)
throw new IllegalStateException();
ConcurrentHashMap.this.remove(lastReturned.key);
lastReturned = null;
}
如果是通过keySet()转为为Iterator呢?答案也是可以的,而且发现最终也是调用HashIterator
的remove()
,与entrySet()
一模一样:
public static void main(String[] args) {
ConcurrentHashMap<String, String> map = new ConcurrentHashMap<String, String>();
map.put("a", "a1");
map.put("b", "b1");
Iterator<String> it = map.keySet().iterator(); //keySet
while (it.hasNext()) {
String key = it.next();
it.remove();
System.out.println(key);
}
System.out.println(map.size()); //输出0
}
4.汇总
-
HashMap是非线程安全的
原因是未加锁 -
HashMap在迭代期间,可能引起ConcurrentModificationException异常
原因是如果数组发生删除操作,Iterator迭代器在迭代期间会进行modCount != expectedModCount
比较,导致抛出异常java.util.ConcurrentModificationException,作用是提醒用户发生了数据不一致的情况,请用户捕获处理遵守fail-fast 原则,详情参考《java中快速失败(fail-fast)和安全失败(fail-safe)的区别是什么?》
-
可以使用ConcurrentHashMap替代HashMap
作用是正在迭代的数组不会产生异常ConcurrentModificationException,原因是写操作分段加锁,一是加锁避免数组污染,二是通过分段加锁,减少实际加锁次数,提升并发效率;读操作不加锁,但会产生了弱一致性问题 -
ConcurrentHashMap迭代期间调用Iterator.remove(),Iterator.add()操作
原因是读操作在迭代期间调用Iterator.remove(),底层会调用ConcurrentHashMap.remove(),由于会加锁,不会产生冲突。
有关ConcurrentHashMap更详细的知识,可以参考《ConcurrentHashMap原理探究》