HashMap的并发问题
Java7
在Java7上,HashMap扩容Rehash的过程中,可能出现循环链表导致死循环的情况:
Java7在Rehash时采用了倒序链表,因为Java7在Rehash中向链表插入Entry时,是向head前插入,所以形成了倒序。
这在并发时可能导致链表死循环,比如:
1.原始链表为3–>7–>null
2.线程1rehash,拿到了3,然后被挂起
3.线程2rehash,把链表重排,变成7–>3–>null
4.线程1恢复执行,继续把3插到7前面,变成了7–>3–>7,循环链表,在查找时会变成死循环。
Java8
在Java8上,HashMap修改了Rehash的链表顺序,不再使用倒序,不会再出现死循环。
但是,在并发读写的场景下,get和put的可能同时发生,不能保证数据正确,所以,Java8的HashMap仍然不是线程安全的。
HashTable的并发问题
HashTable和HashMap没有继承关系,HashTable继承了Dictionary抽象类和Map接口。
HashTable在所有的方法上都加了方法锁,所以在单个方法的执行上是线程安全的。
但是,HashTable的并发效率很差,同一时刻只能有一个线程操作。
而且,HashTable是个遗留类,主要用来兼容遗留代码,如果要使用并发地HashMap,推荐使用的是ConcurrentHashMap,EventBus中就使用了ConcurrentHashMap来管理粘滞事件。
ConcurrentHashMap
Java7
在Java7中,ConcurrentHashMap维持了16个segment(继承自ReentrantLock),每个segment存放一个HashMap,各segment之间可以并行。
所以segment的hash散列会关系到并行效率,对key取hash后,利用hash的高位来计算属于哪个segment。
获取size时,为了提高效率,会先不锁表,先对segment的值求和,连续三次,如果值不变就认为没有冲突,如果值变化了,再去锁表。
Java8
Java8的ConcurrentHashMap和Java8里的HashMap都使用红黑树(如果链表长度超过8,就使用红黑树管理)。
在Java8中,ConcurrentHashMap做了进一步优化,不再划分segment,而是直接针对数组挂的每条链表都可以并行,同时,为了保持可见性,数组、Node的value,和Node的next都是volatile变量。
在获取size时,不再做运算,而是在平时,put和remove时,维护map的size。
参考
Java8系列之重新认识HashMap
Java进阶(六)从ConcurrentHashMap的演进看Java多线程核心技术