Hashtable、HashMap和ConcurrentHashMap

2020/02/15 Java

在使用 HashMap 时在多线程情况下扩容会出现 CPU 接近 100%的情况,因为 hashmap 并不是线程安全的,通常我们可以使用在 java 体系中古老的 hashtable 类,该类基本上所有的方法都采用 synchronized 进行线程安全的控制,可想而知,在高并发的情况下,每次只有一个线程能够获取对象监视器锁,这样的并发性能的确不令人满意。ConccurentHashMap是如何做的呢?参考博客Java的ConcurrentHashMap

HashMap、Hashtable、ConccurentHashMap三者的区别

HashMap线程不安全,数组+链表+红黑树

Hashtable线程安全,锁住整个对象,数组+链表

ConccurentHashMap线程安全,CAS+同步锁,数组+链表+红黑树

HashMap的key,value均可为null,其他两个不行。

ConcurrentHashMap

ConcurrentHashMap在JDK8里结构,其实和 1.8 HashMap 结构类似,当链表节点数超过指定阈值的话,也是会转换成红黑树的,大体结构也是一样的。那么它到底是如何实现线程安全的?答案:其中抛弃了原有的Segment 分段锁,而采用了 CAS + synchronized 来保证并发安全性。

put方法的逻辑:

  1. 判断Node[]数组是否初始化,没有则进行初始化操作
  2. 通过hash定位数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头节点),添加失败则进入下次循环。
  3. 检查到内部正在扩容,就帮助它一块扩容。
  4. 如果f!=null,则使用synchronized锁住f元素(链表/红黑树的头元素)。如果是Node(链表结构)则执行链表的添加操作;如果是TreeNode(树型结构)则执行树添加操作。
  5. 判断链表长度已经达到临界值8(默认值),当节点超过这个值就需要把链表转换为树结构。
  6. 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容

在JDK1.7和JDK1.8中的区别,在JDK1.8主要设计上的改进有以下几点:

  1. 不采用segment而采用node,锁住node来实现减小锁粒度。
  2. 设计了MOVED状态 当resize的中过程中 线程2还在put数据,线程2会帮助resize。
  3. 使用3个CAS操作来确保node的一些操作的原子性,这种方式代替了锁。
  4. sizeCtl的不同值来代表不同含义,起到了控制的作用。
  5. 采用synchronized而不是ReentrantLock

Search

    Table of Contents