总结 HashTable, HashMap, ConcurrentHashMap 之间的区别

1. 多线程环境使用哈希表

HashMap 本身不是线程安全的.

在多线程环境下使用哈希表可以使用:

  • Hashtable(不推荐使用)
  • ConcurrentHashMap(推荐使用)

1)HashMap

  • 首先HashMap本身线程不安全
  • 其次HashMap的key值可以为空

下面是HashMap的hash方法,可以看出如果key值为空,hash方法的返回值为0
在这里插入图片描述

  • HashMap 扩容是等某一次 put 后发现负载因子不达标后开始扩容, 新建一个表, 重新将元素哈希(具体细节有机会再讲)

2)Hashtable

  1. Hashtable源码中的put方法中的key如果为空,hashCode方法会抛出空指针异常

在这里插入图片描述

比如:

在这里插入图片描述

  1. **HashTable的线程安全只是简单的把关键方法加上了 synchronized 关键字. **

在这里插入图片描述

相当于直接针对 Hashtable 对象本身加锁.

  • 如果多线程访问同一个 Hashtable 就会直接造成锁冲突.

  • size 属性也是通过 synchronized 来控制同步, 也是比较慢的.

  • 一旦触发扩容, 就由该线程完成整个扩容过程. 这个过程会涉及到大量的元素拷贝, 效率会非常低.

在这里插入图片描述

一个Hashtable只有一把锁,两个线程访问Hashtable中的任意数据都会出现锁竞争,就如上图,有两个线程要操作这两个元素,由于是一把大锁,就会产生竞争,但是仔细分析,这两操作在不同的哈希桶上,不牵扯修改同一个变量,因此就不会发生线程安全,所以上面的锁竞争是没有必要的。

如果两个修改落到同一个哈希桶上,有线程安全风险

3)ConcurrentHashMap

相比之下ConcurrentHashMap就做出了重大的改进,把锁的粒度细化了

  • 读操作没有加锁(但是使用了 volatile 保证从内存读取结果), 只对写操作进行加锁. 加锁的方式仍然 是是用 synchronized, 但是不是锁整个对象, 而是 “锁桶” (用每个链表的头结点作为锁对象), 大大降 低了锁冲突的概率.

  • 充分利用 CAS 特性. 比如 size 属性通过 CAS 来更新. 避免出现重量级锁的情况.

  • 优化了扩容方式: 化整为零

    • 发现需要扩容的线程, 只需要创建一个新的数组, 同时只搬几个元素过去.
    • 扩容期间, 新老数组同时存在.
    • 后续每个来操作 ConcurrentHashMap 的线程, 都会参与搬家的过程. 每个操作负责搬运一小 部分元素.
    • 搬完最后一个元素再把老数组删掉.
    • 这个期间, 插入只往新数组加.
    • 这个期间, 查找需要同时查新数组和老数组

在这里插入图片描述

此时一个哈希表上面的桶的个数可能会非常多,导致出现锁冲突的概率,大大降低了

扩容=申请更大的内存+搬运元素(搬运元素过程肯定要重新哈希)

上面讲的都是基于Java 8里的;在Java1.7里,ConcurrentHashMap不是每个桶一个锁,而是“分段锁”一个锁管若干个桶

在这里插入图片描述

  • ConcurrenHashMap的key也不可以为空,看下面的源码中的put方法,如果key==null则会抛出空指针异常

在这里插入图片描述

总结HashTable, HashMap, ConcurrentHashMap 之间的区别

HashMap: 线程不安全. key 允许为 null

Hashtable: 线程安全. 使用 synchronized 锁 Hashtable 对象, 就是一把大锁,锁冲突极高,效率较低. key 不允许为 null.

ConcurrentHashMap: 线程安全. 使用 synchronized 锁每个链表头结点, 锁冲突概率低, 充分利用 CAS 机制. 优化了扩容方式. key 不允许为 null

猜你喜欢

转载自blog.csdn.net/HBINen/article/details/126613985