目录
一. HashSet 与 HashMap 的区别
- HashMap 实现了 Map 接口,存储的是键值对,而HashSet 实现的是 Set 接口,底层是基于 HashMap 实现的,存储的是对象。
- HashMap 使用 put 方法将元素放入 map 中,HashSet 使用 add 方法将元素放入 Set 中。
HashSet 如何检查重复?
当插入对象时,会先通过对象的 hashcode 值来判断要加入的位置,同时也会与其他加入对象的 hashcode 值作比较,如果没有相同的 hashcode,则加入成功。如果有相同hashcode值的对象,就调用 equals 方法来检查它们的值是否相同。如果相同,则加入失败。
1.1 HashTable 和 HashMap 的区别
- HashTable 底层结构和JDK1.8之前的HashMap一样都是数组+链表的结构,JDK1.8 后,HashMap 中如果链表长度大于8,就会转化为 红黑树,而 HashTable 没有这样的机制。
- hashTable 是线程安全的,但它实现线程安全的方式是使用 synchronized 对整个 HashTable 加锁,效率较低。HashMap是线程不安全的。
- HashMap 的key和value可以为null,HashTable(以及ConcurrentHashMap)的key和value都不能为null,因为在多线程的环境中,通过 get 方法获取到null时,无法判断它是 put 操作存入的 value 为null,还是这个key从来没有做过映射,所以HashTable的 k-v 不能为null。
- 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 的区别
- JDK1.8以前底层都是数组+链表结构,链表采用头插法,JDK1.8中都引入了红黑树,链表改为尾插法。(HashTable 是数组+链表,头插法)
- HashMap 是线程不安全的,因为它的 get 和 put 方法都没有加锁,在多线程环境下无法保证上一秒 put 的值,在下一秒 get 的时候还是原值,所以线程安全依然无法保证。ConcurrentHashMap 在JDK1.7中把整个数组分成若干个 Segment ,然后通过 ReentrantLock 对每个 Segment 单独加锁;1.8中通过 Synchronized 和 CAS 机制来保证线程安全。(HashTable 通过 Synchronized 来保证线程安全)
- ConcurrentHashMap 的键值不能为 null ,hashMap 的可以为 null。因为 ConcurrentHashMap 在多线程的环境中,如果 get 方法得到的是 null,并不能判断是映射的 value 为null,还是因为没有找到对应的 key 而为 null,系统无法判断出这种模糊不清的情况。而用于单线程的 hashMap 可以通过 containsKey 方法来判断到底是否包含这个 null。(HashTable 也不能为 null)
- 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)。
- 查找时,从顶级链表开始找。一旦发现被查找的元素大于当前链表中的元素,就会转入下一层链表继续查找。查找过程是跳跃式的。跳表是一种利用空间换时间的算法。
- 跳表内所有的元素都是排序的。因此在对跳表进行遍历时,可以得到一个有序的结果。(哈希表不会保存元素的顺序)
- 跳表结构和平衡树类似,它和平衡树的区别是:对平衡树的插入和删除可能导致平衡树进行一次全局的调整。而对跳表的插入和删除只需要对数据结构的局部进行操作即可。这样带来的好处是:在高并发的情况下,需要一个全局锁来保证整个平衡树的线程安全。而对于跳表,需要局部锁即可,这样在高并发环境下,就可以拥有更好的性能。