超话Java----HashMap和ConcurrentHashMap

超话Java----HashMap和ConcurrentHashMap

HashMap和ConcurrentHashMap在不同版本之间的差别

JDK1.7
  • 数据结构
    HashMap采用Entry数组 + 链表的结构
    ConcurrentHashMap采用segment数组+Entry数组+链表的结构
    在这里插入图片描述
  • 查询复杂度:O(n)
  • 插入数据的方式:头插法(先将原数据向后移动在插入当前数据)
  • 扩容的时机:插入数据之前扩容
  • 扩容的方式:全部按照原来的方法重新计算
JDK1.8
  • 数据结构
    HashMap采用Entry数组 + 链表 / 红黑树的结构
    ConcurrentHashMap采用Node数组 + 链表 / 红黑树的结构在这里插入图片描述
  • 查询复杂度:O(n) 或 O(logN)
  • 插入数据的方式:尾插法(直接插入到链表尾部/红黑树)
  • 扩容的时机:插入数据成功后扩容
  • 扩容的方法:没有重Hash计算,直接高低位取模

没有重hash,而是采用4个指针(高低位头尾指针)直接放到index(原位置)和(index + oldlength)的位置,(为什么没有rehash?因为每个节点的数据直接&oldlength做与运算可以得到两个不同的值一个index和一个(index + oldlength)就可以直接获取到位置。)

HashMap的各个参数

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4;  // 默认初始容量

    static final int MAXIMUM_CAPACITY = 1 << 30; // 最大容量
 
    static final float DEFAULT_LOAD_FACTOR = 0.75f; // 默认加载因子

    static final int TREEIFY_THRESHOLD = 8; // 树化阈值(链表长度达到8时转换成红黑树)
 
    static final int UNTREEIFY_THRESHOLD = 6; // 链化阈值(红黑树中的长度减少到6时转换成链表)

    static final int MIN_TREEIFY_CAPACITY = 64; // 最小的树化Node数组的长度(即:Node数组的长度要达到64才会转化成红黑树)
为什么树化阈值是8?
  • 遵循泊松分布概率,8个数据定位到同一个数组节点概率最小。(HashMap 源码中的解释)
加载因子为什么是0.75?
  • 加载因子是数据储存达到数组长度的0.75就扩容
  • 减少hash碰撞 时间与空间之间做平衡,选择0.75
  • 加载因子为1 最大化利用空间
  • 加载因子为0.5 查询效率高 时间复杂度快 但是空间浪费多

HashMap的put逻辑

  • 如果HashMap未被初始化过,则初始化(延迟创建)
  • 对Key进行与运算求Hash值,然后在计算下标
  • 如果没有Hash碰撞,直接放入Node桶中
  • 如果碰撞了,以链表的方式连接到后面
  • 如果链表长度超过阈值(8),就把链表转成红黑树
  • 如果链表长度低于6,就把红黑树转回链表
  • 如果节点已经存在就替换旧值
  • 如果桶满了(容量16*加载因子0.75 == 12),就需要resize(扩容2倍后重排)

HashMap如何有效减少碰撞?

  1. 扰动函数:促使元素位置分布均匀,减少碰撞几率
  2. 使用final对象,并采用合适的equals()和hashCode()方法
    不可变性使得能够缓存不同键的HashCode,这将提高获取对象的速度(String,Integer)

为什么HashMap是线程不安全的?

1. 同时put碰撞导致数据丢失

两个线程同时put到同一个桶(Node[]的一个节点)中(put()方法的逻辑是先将桶拿出来插入新值在放回去),两个线程同时拿到先前的桶,这样就导致一个数据必定丢失

2. 同时put扩容到时数据丢失

两个线程同时发现需要扩容,那么最后也是会导致一个线程要put进去的数据丢失。(与上面的类似)

3. 死循环造成的CPU100%

在并发的情况,发生扩容时,可能会产生循环链表,在执行get的时候,会触发死循环,引起CPU的100%问题,所以一定要避免在并发环境下使用HashMap。

HashMap关于并发的特点

  • 非线程安全
  • 迭代时不允许修改内容
  • 只读的并发是线程安全的
  • 如果一定要把HashMap用在并发环境中就用Collections.synchronizedMap(new HashMap<>());

ConcurrentHashMap在JDK1.7和JDK1.8中的锁机制

JDK1.7

图片来自玩转java并发工具,精通JUC课程图片

  • 1.7的ConcurrentHashMap是采用segment数组 + Entry数组 + 链表的结构
  • 每个segment数组会有一个锁,形成分段锁(segment类是继承了ReentrantLock自带锁的功能)每个segment之间互不影响,提高了并发效率
  • 每个segment的底层数据结构与HashMap类似,仍然是数组和链表组成的拉链法
  • ConcurrentHashMap默认有16个Segments,所以最多同时支持16个线程并发写(操作分别分布在不同的segment上)。这个默认值可以在初始化的时候设置为其他值,但是一旦初始化以后,是不可以扩容的
JDK1.8

图片来自玩转java并发工具,精通JUC课程图片

  • 1.8时 ConcurrentHashMap采用Node数组 + 链表或红黑树的结构
  • 采用CAS + synchronized保证并发安全
  • synchronized锁的对象是Node数组里面的根节点
  • 1.8 将锁的粒度更加细化,且支持的并发数更多,可以扩容
  • 插入数据时,先判断Node数组是否存在,如果不存在则采用CAS插入头结点,失败则循环重试;如果存在则尝试获取头结点的同步锁,再进行操作。
ConcurrentHashMap 在 JDK 1.8 中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?
  1. 粒度降低了
  2. JVM 开发团队没有放弃 synchronized,而且基于 JVM 的 synchronized 优化空间更大,更加自然。
  3. 在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开销更多的内存。

HashMap、Hashtable、ConcurrentHashMap 三者区别

  1. HashMap线程不安全,数组+链表+红黑树
  2. Hashtable线程安全,锁住整个对象,数组+链表
  3. ConcurrentHashMap线程安全。CAS+同步锁synchronized,数组+链表+红黑树
  4. HashMap的key、value均可为null,而其他两个类不支持
  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值