HashMap

Fast-Fail(遍历时写入操作异常)

在使用迭代器的过程中如果HashMap被修改,那么ConcurrentModificationException将被抛出,也即Fast-fail策略。

当HashMap的iterator()方法被调用时,会构造并返回一个新的EntryIterator对象,并将EntryIterator的expectedModCount设置为HashMap的modCount(该变量记录了HashMap被修改的次数)。

HashIterator() {
expectedModCount = modCount;
if (size > 0) { // advance to first entry
Entry[] t = table;
while (index < t.length && (next = t[index++]) == null)
;
}
}

在通过该Iterator的next方法访问下一个Entry时,它会先检查自己的expectedModCount与HashMap的modCount是否相等,如果不相等,说明HashMap被修改,直接抛出ConcurrentModificationException。该Iterator的remove方法也会做类似的检查。该异常的抛出意在提醒用户及早意识到线程安全问题。

tableSizeFor方法

tableSizeFor的功能是返回大于输入参数且最近的2的整数次幂的数。比如10,则返回16。

   static final int tableSizeFor(int cap) {
int n = cap - 1;
n |= n >>> 1;
n |= n >>> 2;
n |= n >>> 4;
n |= n >>> 8;
n |= n >>> 16;
return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
    static final int MAXIMUM_CAPACITY = 1 << 30;

再次分析问什么减一

  int n = cap - 1;

让cap-1再赋值给n的目的是另找到的目标值大于或等于原值。例如二进制1000,十进制数值为8。如果不对它减1而直接操作,将得到答案10000,即16。显然不是结果。减1后二进制为111,再进行操作则会得到原来的数值1000,即8。

HashMap里的MAXIMUM_CAPACITY是230。我结合tableSizeFor()的实现,猜测设置原因如下:

int的正数最大可达231-1,而没办法取到231。所以容量也无法达到231。又需要让容量满足2的幂次。所以设置为230

hash方法

    static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

从上面的代码可以看到key的hash值的计算方法。key的hash值高16位不变,低16位与高16位异或作为key的最终hash值。(h >>> 16,表示无符号右移16位,高位补0,任何数跟0异或都是其本身,因此key的hash值高16位不变。)

为什么要这么干呢?

这个与HashMap中table下标的计算有关。

n = table.length;
index = (n-1) & hash;

因为,table的长度都是2的幂,因此index仅与hash值的低n位有关(此n非table.leng,而是2的幂指数),hash值的高位都被与操作置为0了。

假设table.length=24=16。

由上图可以看到,只有hash值的低4位参与了运算。

这样做很容易产生碰撞。设计者权衡了speed, utility, and quality,将高16位与低16位异或来减少这种影响。设计者考虑到现在的hashCode分布的已经很不错了,而且当发生较大碰撞时也用树形存储降低了冲突。仅仅异或一下,既减少了系统的开销,也不会造成的因为高位没有参与下标的计算(table长度比较小时),从而引起的碰撞。

ConcurrentHashMap

JDK7中处理

Segment继承自ReentrantLock,所以我们可以很方便的对每一个Segment上锁。

读操作(get)

对于读操作,获取Key所在的Segment时,需要保证可见性(请参考如何保证多线程条件下的可见性)。具体实现上可以使用volatile关键字,也可使用锁。但使用锁开销太大,而使用volatile时每次写操作都会让所有CPU内缓存无效,也有一定开销。ConcurrentHashMap使用如下方法保证可见性,取得最新的Segment。

Segment<K,V> s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)

获取Segment中的HashEntry时也使用了类似方法

HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE)

写操作(put,remove)

写操作,并不要求同时获取所有Segment的锁,因为那样相当于锁住了整个Map。它会先获取该Key-Value对所在的Segment的锁,获取成功后就可以像操作一个普通的HashMap一样操作该Segment,并保证该Segment的安全性。

同时由于其它Segment的锁并未被获取,因此理论上可支持concurrencyLevel(等于Segment的个数)个线程安全的并发读写。

获取锁时,并不直接使用lock来获取,因为该方法获取锁失败时会挂起(参考可重入锁)。事实上,它使用了自旋锁,如果tryLock获取锁失败,说明锁被其它线程占用,此时通过循环再次以tryLock的方式申请锁。如果在循环过程中该Key所对应的链表头被修改,则重置retry次数。如果retry次数超过一定值,则使用lock方法申请锁。

这里使用自旋锁是因为自旋锁的效率比较高,但是它消耗CPU资源比较多,因此在自旋次数超过阈值时切换为互斥锁。

JDK8处理

可见性,查看node源码:

static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}

读操作,由于数组被volatile关键字修饰,因此不用担心数组的可见性问题。同时每个元素是一个Node实例(Java 7中每个元素是一个HashEntry),它的Key值和hash值都由final修饰,不可变更,无须关心它们被修改后的可见性问题。而其Value及对下一个元素的引用由volatile修饰,可见性也有保障。

对于Key对应的数组元素的可见性,由Unsafe的getObjectVolatile方法保证。

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

总结

HashMap在JDK8与JDK7中的区别

  • 插入数据时,hash冲突,jdk7总是把数据插入到链表的头部,jdk8要先判断node是红黑树,还是链表,如果是链表,长度超过8也要转换成红黑树,链表的话,插入到链表尾部,如果是remove数据,红黑树长度小于6也会转换成链表。
  • 扩容resize时,jdk7的扩容,按旧链表正序遍历,在新链表的头部依次插入,在多线程的情况下,有一定概率会出现链表环,出现死锁。jdk8扩容,按旧链表正序遍历,在新链表尾部依次插入,不会出现jdk7中的链表环,但在多线程的情况下有一定概率出现脏数据,数据丢失问题。

ConcurrentHashMap在JDK7与JDK8中的区别

  • ConcurrentHashMap在jdk8中初始化采用了延迟初始化策略,他会在第一次执行put的时候初始化table。

  • JDK7采用segment的分段锁机制实现线程安全,其中segment继承自ReentrantLock。JDK8采用CAS(读)+Synchronized(写)保证线程安全。

  • 锁的粒度:原来是对需要进行数据操作的Segment加锁,JDK8调整为对每个数组元素加锁(Node)。

  • 链表转化为红黑树:定位结点的hash算法简化会带来弊端,Hash冲突加剧,因此在链表节点数量大于8时,会将链表转化为红黑树进行存储。

  • 查询时间复杂度:从原来的遍历链表O(n),变成遍历红黑树O(logN)。

  • JDK8推荐使用mappingCount方法而不是size方法获取当前map表的大小,因为这个方法的返回值是long类型,size方法是返回值类型是int

ConcurrentHashMap与HashMap相比,有以下不同点

  • ConcurrentHashMap线程安全,而HashMap非线程安全
  • HashMap允许Key和Value为null,而ConcurrentHashMap不允许
  • HashMap迭代器是强一致性,ConcurrentHashMap迭代器是弱一致性,HashMap不允许通过Iterator遍历的同时通过HashMap修改,而ConcurrentHashMap允许该行为,并且该更新对后续的遍历可见。

参考:

Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析

Java进阶(六)从ConcurrentHashMap的演进看Java多线程核心技术

Java8的HashMap详解(存储结构,功能实现,扩容优化,线程安全,遍历方法)

Java8 HashMap源码阅读

Java进阶(六)从ConcurrentHashMap的演进看Java多线程核心技术

Java集合类框架学习 5.3—— ConcurrentHashMap(JDK1.8)

JAVA HashMap与ConcurrentHashMap的更多相关文章

  1. 轻松理解 Java HashMap 和 ConcurrentHashMap

    前言 Map 这样的 Key Value 在软件开发中是非常经典的结构,常用于在内存中存放数据. 本篇主要想讨论 ConcurrentHashMap 这样一个并发容器,在正式开始之前我觉得有必要谈谈 ...

  2. [Java集合] 彻底搞懂HashMap,HashTable,ConcurrentHashMap之关联.

    注: 今天看到的一篇讲hashMap,hashTable,concurrentHashMap很透彻的一篇文章, 感谢原作者的分享. 原文地址: http://blog.csdn.net/zhanger ...

  3. java基础知识再学习--HashMap与ConcurrentHashMap的区别

    引用:http://blog.csdn.net/xuefeng0707/article/details/40834595 从JDK1.2起,就有了HashMap,正如前一篇文章所说,HashMap不是 ...

  4. Java中关于Map的使用(HashMap、ConcurrentHashMap)

    在日常开发中Map可能是Java集合框架中最常用的一个类了,当我们常规使用HashMap时可能会经常看到以下这种代码: Map<Integer, String> hashMap = new ...

  5. 沉淀再出发:java中的HashMap、ConcurrentHashMap和Hashtable的认识

    沉淀再出发:java中的HashMap.ConcurrentHashMap和Hashtable的认识 一.前言 很多知识在学习或者使用了之后总是会忘记的,但是如果把这些只是背后的原理理解了,并且记忆下 ...

  6. Java并发指南13:Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析

    Java7/8 中的 HashMap 和 ConcurrentHashMap 全解析 转自https://www.javadoop.com/post/hashmap#toc7 部分内容转自 http: ...

  7. java并发编程笔记(十)——HashMap与ConcurrentHashMap

    java并发编程笔记(十)--HashMap与ConcurrentHashMap HashMap参数 有两个参数影响他的性能 初始容量(默认为16) 加载因子(默认是0.75) HashMap寻址方式 ...

  8. Java中HashMap与ConcurrentHashMap的区别

    从JDK1.2起,就有了HashMap,正如前一篇文章所说,HashMap不是线程安全的,因此多线程操作时需要格外小心. 在JDK1.5中,伟大的Doug Lea给我们带来了concurrent包,从 ...

  9. JAVA学习:HashMap 和 ConcurrentHashMap

     一.最基本的HashMap 和 ConcurrentHashMap 1.HashMap的结构和底层原理:由数组和链表组成,数组里面每个地方都存了Key-Value这样的实例,在Java7叫Entry ...

随机推荐

  1. Python进阶-XVIV 类的内置方法:__str__ 、__repr__、析构函数(__del__)、双下的call,eq,new,hash 以及item相关的三个方法

    类的内置方法 它与内置函数有紧密的联系,有的内置函数就是调用的内置方法. 在顶级父类obj中有: 两个双下方法 obj.__str__ str(obj) obj.__repr__ repr(obj) ...

  2. C语言第2

    今个那老哥替我刷题,然后遇到一个打印中文编码得问题,我多嘴一问,为啥 用 unsiged 然后他让我换成char试试:然后出现了以下结果 #include <stdio.h> #inclu ...

  3. Codeforces Rating System

    来翻译一下官方文档,但是建议看英文原文,本文可能会出现一些错误,虽然不是为了方便自己查阅用的. 首先,对于人 \(i\),定义 \(r_i\) 是他的 rating,对于人 \(i,j\),定义 \( ...

  4. vue_day05

    目录 vue前后端交互: vue 分离前后端交互: vue前端发送请求: vue请求插件--axios: main.js配置: 前端朝后端请求传参方式: django后端返回数据样式: vue配置El ...

  5. PyCharm2019.3.1专业版激活

    PyCharm2019.3.1专业版激活 Python的IDE非常多,但个人感觉PyCharm最好用.JetBrains 致力于为开发者打造最高效智能的开发工具,与2019.12.19带来新的版本20 ...

  6. R语言- 实验报告 - 利用R语言脚本与Java相互调用

    一. 实训内容 利用R语言对Java项目程序进行调用,本实验包括利用R语言对java的.java文件进行编译和执行输出. 在Java中调用R语言程序.本实验通过eclipse编写Java程序的方式,调 ...

  7. python中count和index

    str = [1,2,3,4,5] #定义一个列表 str = 3 #列表3 str [1, 2, 3, 4, 5, 1, 2, 3, 4, 5, 1, 2, 3, 4, 5] str.count(1 ...

  8. Gitlab 备份还原/迁移

    Gitlab 备份还原 备份数据:通过命令进行备份操作 gitlab-rake gitlab:backup:create ... [DISABLED] Creating backup archive: ...

  9. 改变src图片不更新

    var url = response + "?r" + Math.random();[完美解决修改src不更新图片]$('#loginimage').attr("src& ...

  10. Java之路---Day16(泛型)

    2019-11-02-23:25:26 目录 1.泛型的概念: 2.泛型的定义和使用: 2.1定义和使用含有泛型的类: 2.2定义和使用含有泛型的方法: 2.3定义和使用含有泛型的接口: 泛型的概念: ...