研究背景:
在项目中遇到,由于HashMap的线程不安全,导致程序报错
如果多个线程同时访问一个哈希映射,并且至少有一个线程从结构上修改了该映射,则必须 保持同步。(结构上的修改是指添加或删除一个或多个映射关系的任何操作;仅改变与实例已经包含的键关联的值不是结构上的修改。)一般通过对自然封装该映射的对象进行同步操作来完成。如果不存在这样的对象,则应该使用 Collections.synchronizedMap 方法来“包装”该映射。最好在创建时完成这一操作,避免对映射进行意外的非同步访问,如下所示:
Map m = Collections.synchronizedMap(new HashMap(…));
由所有此类的“collection 视图方法”所返回的迭代器都是快速失败 的:在迭代器创建之后,如果从结构上对映射进行修改,除非通过迭代器本身的 remove 方法,其他任何时间任何方式的修改,迭代器都将抛出ConcurrentModificationException。因此,面对并发的修改,迭代器很快就会完全失败,而不冒在将来不确定的时间发生任意不确定行为的风险。
注意,迭代器的快速失败行为不能得到保证,一般来说,存在非同步的并发修改时,不可能作出任何坚决的保证。快速失败迭代器尽最大努力抛出 ConcurrentModificationException。因此,编写依赖于此异常的程序的做法是错误的,正确做法是:迭代器的快速失败行为应该仅用于检测程序错误。
解决方案:ConcurrentHashMap
了解ConcurrentHashMap
ConcurrentHashMap中涉及的概念值,sizeCtl含义解释
注意:以上这些构造方法中,都涉及到一个变量sizeCtl,这个变量是一个非常重要的变量,而且具有非常丰富的含义,它的值不同,对应的含义也不一样,这里我们先对这个变量不同的值的含义做一下说明,后续源码分析过程中,进一步解释
sizeCtl为0,代表数组未初始化, 且数组的初始容量为16
sizeCtl为正数,如果数组未初始化,那么其记录的是数组的初始容量,如果数组已经初始化,那么其记录的是数组的扩容阈值
sizeCtl为-1,表示数组正在进行初始化
sizeCtl小于0,并且不是-1,表示数组正在扩容, -(1+n),表示此时有n个线程正在共同完成数组的扩容操作
ConcurrentHashMap.put() 源码解读
链表长度大于/等于8,将链表转成红黑树
扩容
…
addCount(1L, binCount)
添加完后,size+1
addCount(1L, binCount);
线程A通过cas 将size加一
U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)
线程B 无法通过CAS将size加一,通过CounterCell数组实现map 的size加一,即map(size) = BASECOUNT + CounterCell(value),以此提升高并发效能
① CounterCell数组不为空,优先利用数组中的CounterCell记录数量
② 如果数组为空,尝试对baseCount进行累加,失败后,会执行fullAddCount逻辑
③ 如果是添加元素操作,会继续判断是否需要扩容
initTable()
while自旋加cas初始化map,只有一个线程能够初始化,其他线程,放弃cpu调度
简单总结:
java8 中的ConcurrentHashMap利用了大量的自旋加cas,加上synchronized保证了Map的线程安全