HashMap,HashSet,Hashtable,ConcurrentHashMap

1. HashMap:

1.1 Hashmap的底层结构

HahMap的底层通过散列表存储数据,该散列表是一个 Node<K,V>类型的数组,Node即是一个个节点,K和V是节点的键和值。

HashMap中,key和value都允许为null,key为null的键值对永远都放在以 table[0] 为头结点的链表中。

在每个哈希桶中,存放的许多节点以链表结构存储,哈希桶数组中存放的是链表的头节点。在JDK8之后,为了优化链表的查询效率,链表长度达到8之后,链表变为红黑树来存储节点。

Node<K,V>节点:

static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }

1.2 HashMap存储对象的细节

存储对象时,会先调用hashCode()方法计算出 key 的 哈希值(int),对哈希值进行运算得到哈希桶的位置。

观察HashMap中下面这段源码,取对象哈希值的高16位,并与哈希值进行异或后返回,之后再根据这个值计算出哈希桶。

这段源码看起来似乎有些多余,为什么不直接通过 hashCode() 计算得到的哈希值来计算哈希桶的?

static final int hash(Object key) {
        int h;
        return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
    }

再看下面这行源码,是为了计算出数组下标

(n - 1) & hash

如果 n 为16,(n-1)= 15,二进制为1111,&运算后,相当于取 hash 的低四位。如果想让&运算的结果更加随机,就得让 hash 的低四位更加随机。想让低四位更加随机,采用的方法便是与高位异或

总结:

由于和(length-1)运算,length 绝大多数情况小于2的16次方。所以始终是hashcode 的低16位(甚至更低)参与运算。要是高16位也参与运算,会让得到的下标更加散列

1.3 HashMap解决冲突

若哈希值冲突,则在每个哈希桶采用链表/红黑树结构进行存储,通过 equals() 判断 该对象与链表/红黑树的对象是否是同一对象,若是,则覆盖。

原则:如果两个对象相等,那么两个对象的hashcode也应当相等。因此,重写equals,就必须重写hashcode。

若两个对象的hashCode相等,不一定equals,反过来则一定。

发生哈希冲突时,HashMap采用散列表的链表法来解决哈希冲突,除此之外,散列表还有以下两种方法解决冲突:

1. 开放定址法,如果散列表地址发生冲突,就继续向下寻找空的地址。具体方式有线性探测法(F(i)=i)、二次探测法(F(i)=i^2)和双重散列法(使用多个散列函数)。

2. 再散列,当散列表中数据个数太多而引起多次发生冲突的情况,采用再散列的方式,建一个新的散列表,新散列表的容量是旧散列表的两倍,再将旧散列表的数据放进新散列表中。HashMap的扩容机制采用了再散列的方式来减少哈希冲突。

1.4 HashMap的扩容

散列表的初始容量为16,散列因子为0.75,即元素数量达到初始容量的0.75时,散列表便会自动扩容,散列表扩大为原来的两倍。

与Redis的扩容机制不同(Redis中map的扩容机制在以前文章中有提到),HashMap采用的是一“多线程协同式 rehash”,即在需要扩容时,将整个HashMap完全扩容完成后再进行读写。整个过程如下:(如果进行写操作,会有一瞬时的性能抖动)

线程A在扩容把数据从oldTable搬到到newTable,这时其他线程

进行get操作:这个线程知道数据存放在oldTable或是newTable中,直接取即可。

进行写操作:如果要写的桶位,已经被线程A搬运到了newTable。

那么这个线程知道正在扩容,它也一起帮着扩容,扩容完成后才进行put操作

扩容的过程:

通过put添加元素时,会先判断哈希桶已使用的数量是否已达到阈值。如果已达到阈值,便将哈希桶的数量扩容两倍。

对原来的元素重新计算进行哈希桶的位置,放入新的散列表中。

jdk1.7和1.8的区别:

头插法改尾插法,避免扩容时链表逆序造成链表死循环。

1.7是先扩容后插入,1.8是先插入后扩容。至于原因是什么,我也没搞懂,不过我觉得应该是针对红黑树做出的调整。

为什么扩容是扩大两倍?因为哈希桶的数量必须为2的次幂,如果通过构造函数自定义哈希桶的数量,HashMap会将容量自动调整成2的次幂。

为什么容量必须为2的次幂呢?

通过 hashCode() 计算出key的哈希值之后,源码通过 以下一段程序计算出该键值对放在哪个索引下

(n - 1) & hash

n为散列表的长度,只有散列表的长度为 2 的整数次幂时,n-1 的二进制数才全为1。

如果不全为1,会增加碰撞的几率,因为哈希值不同的两个节点也可能会被放在同一个桶里。比如如果容量为15,(n-1)=14,即1110,8和9与1110的结果是相同的,会发生哈希碰撞。

有个数学规律,当 length = 2^n 时,X % length = X & (length - 1),也就是说,模运算可以转换为与运算。而且对于计算机来说,二进制数0,按位与,代表着翻转,降低了运算效率

1.5 为什么HashMap线程不安全?

首先,集合在多线程情况下存元素的通病——元素覆盖。

其次,在JDK1.8之前,HashMap扩容是头插法扩容,头插法会导致链表反向,在多线程情况下扩容容易形成循环链表,造成死循环。JDK1.8之后,扩容改用尾插法,不会造成死循环。

2. HashSet:

HashSet实现了Set接口,不允许集合中出现重复的元素。

HashSet去除重复元素的原理:HashSet基于HashMap的结构进行存储,HashSet是一个 值都相等的但键不同 的HashMap,HashSet在存储键时,会根据equals去除重复的键。

HashSet中相同的 value:

// Dummy value to associate with an Object in the backing Map
private static final Object PRESENT = new Object();

为什么值为object类型,而不为null呢?

是因为 HashSet 的 add() 方法,调用的是 HashMap 的put方法,put方法又调用了putVal方法:

public boolean add(E e) {
    return map.put(e, PRESENT)==null;
}

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

 * @return previous value, or null if none
 */
final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict)

通过 putVal() 方法的@return可知,如果该key是第一次加入,则返回添加的value,如果以前添加过,则返回 null。

HashSet或HashMap在存储对象时,要重写 equals() 和 hashCode() 方法,hashCode()用来确定对象在散列表中的存储地址,equals()则保证了对象不重复。

3. HashTable:

Hashtable同样是基于哈希表实现的,同样每个元素是一个key-value对,其内部也是通过单链表解决冲突问题,容量不足(超过了阀值)时,同样会自动增长。与HashMap不同的是,Hashtable的key和value都不能为null。

HashTable 和 HashMap的区别在于:HashTable是线程安全的,能在多线程环境中使用。

HashTable中的方法是Synchronize的:虽然保证了线程安全,但所有的方法都共用同一把锁,效率极低。

public synchronized V put(K key, V value){}

public synchronized V remove(Object key){}

public synchronized V get(Object key) {}

public synchronized int size(){}

4. ConcurrentHashMap

为了解决多线程下HashMap不安全的情况,JUC提供了ConcurrentHashMap工具类。

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值