HashMap,HashTable,HashSet,ConcurrentHashMap 区别,ConcurrentSkipListMap(跳表)

一. HashSet 与 HashMap 的区别

  1. HashMap 实现了 Map 接口,存储的是键值对,而HashSet 实现的是 Set 接口,底层是基于 HashMap 实现的,存储的是对象。
  2. HashMap 使用 put 方法将元素放入 map 中,HashSet 使用 add 方法将元素放入 Set 中。
HashSet 如何检查重复?

当插入对象时,会先通过对象的 hashcode 值来判断要加入的位置,同时也会与其他加入对象的 hashcode 值作比较,如果没有相同的 hashcode,则加入成功。如果有相同hashcode值的对象,就调用 equals 方法来检查它们的值是否相同。如果相同,则加入失败。

1.1 HashTable 和 HashMap 的区别

  1. HashTable 底层结构和JDK1.8之前的HashMap一样都是数组+链表的结构,JDK1.8 后,HashMap 中如果链表长度大于8,就会转化为 红黑树,而 HashTable 没有这样的机制。
  2. hashTable 是线程安全的,但它实现线程安全的方式是使用 synchronized 对整个 HashTable 加锁,效率较低。HashMap是线程不安全的。
  3. HashMap 的key和value可以为null,HashTable(以及ConcurrentHashMap)的key和value都不能为null,因为在多线程的环境中,通过 get 方法获取到null时,无法判断它是 put 操作存入的 value 为null,还是这个key从来没有做过映射,所以HashTable的 k-v 不能为null。
  4. HashMap 默认初始化容量为 16,每次扩容,容量变为原来的两倍。容量总为2的幂次方,因为这边方便做取模运算(hash & (length-1));HashTable 默认初始容量为 11,每次扩容,容量变为原来的两倍+1。

二. ConcurrentHashMap

JDK1.7 —— 分段锁

  JDK1.7 中 ConcurrentHashMap 底层采用的是数组 + 链表结构,同时还使用了分段锁。它将数组进行分段,每一把锁只锁住一部分数据,然后多线程访问不同数据段上的数据,就不会存在锁竞争,于是提高了并发度。

JDK1.8 —— Synchronized 和 CAS 操作
  • JDK1.8 中 ConcurrentHashMap 摒弃了 segment 的概念,直接通过 node 数组 + 链表 + 红黑树的结构来实现,通过 Synchronized 和 CAS 操作来进行并发控制。Synchronized 只锁定当前链表或红黑树的首节点,相比 JDK1.7 中锁住包含多个数组的 Segment,锁粒度明显降低。
  • put 过程: 写入数据时如果桶为空,则通过 CAS 操作插入元素,如果桶不为空,而且不是处于扩容状态,就通过 synchronized 锁写入数据。
  • get 过程: value 属性用 volatile 关键词修饰,保证了内存可见性,所以每次获取的都是最新值,因此 ConcurrentHashMap 的 get 方法不需要加锁,非常高效。

2.1 ConcurrentHashMap 与 HashMap 的区别

  1. JDK1.8以前底层都是数组+链表结构,链表采用头插法,JDK1.8中都引入了红黑树,链表改为尾插法。(HashTable 是数组+链表,头插法)
  2. HashMap 是线程不安全的,因为它的 get 和 put 方法都没有加锁,在多线程环境下无法保证上一秒 put 的值,在下一秒 get 的时候还是原值,所以线程安全依然无法保证。ConcurrentHashMap 在JDK1.7中把整个数组分成若干个 Segment ,然后通过 ReentrantLock 对每个 Segment 单独加锁;1.8中通过 Synchronized 和 CAS 机制来保证线程安全。(HashTable 通过 Synchronized 来保证线程安全)
  3. ConcurrentHashMap 的键值不能为 null ,hashMap 的可以为 null。因为 ConcurrentHashMap 在多线程的环境中,如果 get 方法得到的是 null,并不能判断是映射的 value 为null,还是因为没有找到对应的 key 而为 null,系统无法判断出这种模糊不清的情况。而用于单线程的 hashMap 可以通过 containsKey 方法来判断到底是否包含这个 null。(HashTable 也不能为 null)
  4. HashMap 和 ConcurrentHashMap 的初始容量都是16,都采取两倍的扩容方式。(HashTable 初始容量是11,采取两倍的原始容量+1 的方式进行扩容)

2.2 hashMap 和 ConcurrentHashMap 的 size 操作

  • HashMap 定义了一个 modCount 变量,每次变动时,无论是 put 还是 remove ,都将 modCount 加1。遍历两次数组,如果得出的 modCount 值一样,就表示未变动了,成功返回 size。否则就表示又变动过了,就继续遍历再次比较 modCount。
  • ConcurrentHashMap 1.7:首先以不加锁的方式,定义了一个 modCount 变量,每次变动时,无论是 put 还是 remove ,都将 modCount 加1。遍历两次数组,如果得出的 modCount 值一样,就表示未变动了,成功返回 size。否则就再遍历一次,如果依然不一致,就对所有的 Segment 加锁,然后一个一个遍历来求出 size。
  • ConcurrentHashMap 1.8:新值了 mappingCount 方法,它的返回值类型是 long 类型,不会因为 size 方法是 int 类型而限制最大值。它里面有一个 volatile 修饰的 baseCount 变量,当没有发生冲突时,使用 baseCount 来计数, 还有一个填充单元 CounterCell 数组,当并发产生冲突时,使用 CounterCell 计数,最后通过 baseCount 和 CounterCell 数组总的计数值来得到 size 大小。其中 CounterCell 添加了 @Contended 注解来防止伪共享,伪共享产生的原因是因为缓存系统是以缓存行为单位进行存储的,缓存行是 2 的幂次方的连续字节,一般为64个字节,当多线程修改同一个缓存行中不同的变量时,由于同时只能有一个线程操作缓存行,所以会影响彼此的性能。JDK1.8 通过添加注解 @Contended 使变量在缓存行中分隔开来解决伪共享问题。(在JDK1.8之前是通过数据填充的方式来解决)(得出的 size 并不准确)
      

三. ConcurrentSkipListMap(跳表)

  • ConcurrentSkipListMap是基于跳表实现的,跳表是一种链表加多层索引的结构,支持快速的插入、删除、查找操作,时间复杂度都是O(logn)。空间复杂度为O(n)。
  • 查找时,从顶级链表开始找。一旦发现被查找的元素大于当前链表中的元素,就会转入下一层链表继续查找。查找过程是跳跃式的。跳表是一种利用空间换时间的算法。
  • 跳表内所有的元素都是排序的。因此在对跳表进行遍历时,可以得到一个有序的结果。(哈希表不会保存元素的顺序)
  • 跳表结构和平衡树类似,它和平衡树的区别是:对平衡树的插入和删除可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,需要局部锁即可,这样在高并发环境下,就可以拥有更好的性能。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值