HashMap和ConcurrentHashMap详解

详细介绍一下java集合容器及hashMap工作原理

一、Collection 、Map

java.util.Collection 是一个集合接口(集合类的一个顶级接口)。它提供了对集合对象进行基本操作的通用接口方法。
在这里插入图片描述
ArrayList:底层数组,排列有序可重复,速度快,增删慢,线程不安全,容量不够时,当前容量1.5倍+1;

Vector:底层数组,排列有序可重复,速度快,增删慢,线程安全,容量不够时,默认扩展一倍容量;

LinkedList:底层双向循环链表结构,排列有序,可重复,线程不安全,查询慢,增删快;

HashSet:底层哈希表,无序不可重复,内部是HashMap,存取速度快;

TreeSet:底层二叉树,排列无序不重复,排序存储,内部是TreeMap的SortedSet。

LinkedHashSet:底层哈希表,并用双向链表记录插入顺序,内部是
LinkedHashMap;

Queue:在两端出入的list,也可以用数组或者链表实现;

在这里插入图片描述
HashMap:底层哈希表,键不可重复,值可以重复,访问速度快,线程不安全,key、value都可以空,对应的线程安全的有ConcurrentHashMap;

HashTable:底层哈希表,键不可重复,值可以重复,线程安全,key、value都不可以空,使用了synchronized进行控制;

TreeMap:底层二叉树,键不可重复,值可以重复;实现了SortMap接口,默认按键值升序排序;也可以指定排序比较器;使用TreeMap时key必须实现Comparable接口或在构造TreeMap传入自定义的Comparator;

LinkedHashMap:是HashMap的一个子类,保存了记录的插入顺序;用Iterator遍历时,先取到的肯定是先插入的;

小结:tree开头的底层是二叉树;hash开头的底层是哈希表;set的内部其实还是map,link开头的底层是双向循环链表结构。

二、常见的问题

1、HashTable、HashMap、ConcurrentHashMap的区别?

HashTable

  • 线程安全的,承自Dictionary类;
  • 使用synchronized加锁,就是对对象加锁;使用一把锁锁住整个链表结构,处理并发容易阻塞;
  • 并发性不如ConcurrentHashMap好,不需要线程安全的场合可以用HashMap替代,所以不建议使用;
  • 默认初始化数组大小默认11,扩容是2倍+1

ConcurrentHashMap:支持多线程

  • jdk1.7中实现:ReentrantLock+Segment+hashEntry数组(分段锁方式,每段一个锁);每个Segment多个哈希表,其中Segment是通过继承ReentrantLock(可重入锁)来进行加锁的,每次锁住一个Segment;
  • 默认初始化数组大小默认16,即并发数、并行级别、Segment数、并行度默认16,也就是说ConcurrentHashMap中有16个Segment;这个值可以在初始化时设置,一旦初始化是不可以扩容的;
  • 实际并发度:2n,实际并发度会使用大于等于该值最小2幂指数作为实际并发度,比如用户设置17,实际并发度32;
  • jdk1.8中实现:CAS(无锁算法)+synchronized+Node+红黑树(锁力度Node首结点);
    在这里插入图片描述

HashMap

  • 线程非安全的;可用Collections的synchronizedMap方法使hashMap具有线程安全的能力,或者使用ConcurrentHashMap;
  • 底层数组+链表+红黑树,而不是二叉树。二叉树特殊情况下会变成一条线性结构,造成树很深的问题,而红黑树是平衡二叉树,可能通过左旋、右旋、变色等使其保持平衡,引入红黑树就是为了查找快;
  • hashMap根据键的hashcode值存储数据,访问速度快,最多允许一条记录的键为null,允许多条记录的值为null,查找时,我们根据hash值快速定位到数组下标,之后顺着链表一个个比较下去,时间复杂度取决于链表长度;为了降低开销;链表长度超过8变成红黑树,低于6变回链表;
  • 数组容量保持2n, 默认初始化数组大小默认16,扩容后大小为原来的2倍;负载因子0.75,所以容量=16*0.75=12,也就是实际大小超过12就需要动态扩展,扩展时调用resize()方法,将table长度变为原来的2倍;

三、ConcurrentHashMap工作原理

在JDK 1.8中,ConcurrentHashMap 的实现进行了显著的优化和改进,主要采用了以下技术:

  • CAS(无锁算法):用于更新特定的共享变量(如计数器和状态标志)。
  • Synchronized:用于控制对特定段(bucket)的竞争访问。
  • Node:用于存储键值对的基本单位。
  • 红黑树:用于优化链表过长时的查找效率,减少查找的时间复杂度。

数据结构:

  • Node:ConcurrentHashMap 的基本存储单元,包含键、值、哈希值以及指向下一个节点的指针。
  • 红黑树:当链表长度超过一定阈值(默认是8)时,链表会转换为红黑树,以提升查找和插入效率。
  • 桶数组:ConcurrentHashMap 的主数据结构是一个数组,每个数组项是一个 Node 链表或红黑树。

以下是 ConcurrentHashMap 中数据插入操作的详细运转说明:
1)初始化和哈希计算
插入数据时,首先计算键的哈希值,并根据哈希值找到对应的桶(数组中的位置)。

final int hash = spread(key.hashCode());
final int binCount = 0;

2)桶初始化
如果桶数组 table 还没有初始化,需要进行初始化:

if (table == null || table.length == 0) {
    initTable();
}

initTable() 使用 CAS 操作保证桶数组只被初始化一次。

3)桶锁定和插入
根据哈希值找到相应的桶,如果该位置为空,则直接使用 CAS 操作插入一个新的节点。

if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
    if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
        break;
}

如果桶位置已经存在节点,则使用 synchronized 锁定该桶位置,进行后续操作。

synchronized (f) {
    if (tabAt(tab, i) == f) {
        if (f.hash == MOVED) {
            tab = helpTransfer(tab, f);
            continue;
        }
        binCount = 1;
        for (Node<K,V> e = f;; ++binCount) {
            K ek;
            if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                V ev = e.val;
                if (!onlyIfAbsent) e.val = value;
                return ev;
            }
            Node<K,V> pred = e;
            if ((e = e.next) == null) {
                pred.next = new Node<K,V>(hash, key, value, null);
                if (binCount >= TREEIFY_THRESHOLD - 1)
                    treeifyBin(tab, i);
                break;
            }
        }
    }
}

4) 链表转换为红黑树
如果链表长度超过一定阈值(默认是8),会将链表转换为红黑树:

if (binCount >= TREEIFY_THRESHOLD - 1)
    treeifyBin(tab, i);

treeifyBin 方法将链表转换为红黑树,从而提升查找和插入效率。

四、hashMap工作原理

1、hashMap基本组成(数组+链表+红黑树)

HashMap的主干是一个Entry数组,链表则是主要为了解决哈希冲突而存在的(链表长度超过8变成红黑树,低于6变回链表;);Entry是HashMap中的一个静态内部类。代码如下,包含4部分主要信息:

static class Entry<K,V> implements Map.Entry<K,V> {
    final K key;
    V value;
    Entry<K,V> next;//存储指向下一个Entry的引用,单链表结构
    int hash;//对key的hashcode值进行hash运算后得到的值,存储在Entry,避免重复计算
    /**
         * Creates new entry.
         */
    Entry(int h, K k, V v, Entry<K,V> n) {
        value = v;
        next = n;
        key = k;
        hash = h;
    } 

在这里插入图片描述

注:hashcode是32位的int类型!

2、HashMap工作原理

1)HashMap通过put和get方法存储和获取;
2)存储对象时将K/V传给put();调用hash()方法计算K的hash值,然后结合数组长度(n-1)& hash计算数组下标;
3)调整数组大小, 容器中元素个数大于capacity*loadfactor时,容器自动扩容resize为2n;
4)如果k的hash值在hashmap中不存在,则插入,若存在,则碰撞;
5)如果k的hash值在hashmap中存在,切equals()返回true,则更新键值对,若返回false,则插入链表的尾部(尾插法)或者红黑树中(树的添加方式),jdk1.7前是头插法;
6)获取对象时,将K传给get(),调用hash(K)找到该键值所在的数组下标, 若为链表,则在链表中通过key.equals(k)查找,O(n) ;若为树,则在树中通过key.equals(k)查找,O(logn);

HashMap数组扩容过程:
创建一个新数组,容量为旧数组的两倍,重新计算旧数组中结点的位置,结点在新数组中的位置只有两种:
①原下标位置;②原下标位置+旧数组大小;

3、Hash函数的设计

将key的hashCode与该hashCode的无符号右移16位,异或起来得到的。

因为当table的size比较小时,能影响到table下标的,只有哈希值几个低位bit,这很可能会加剧哈希碰撞。但这样实现后,哈希值的高16位bit保持不变,低16位则受到高16位的“扰动”而发生改变,这样就使得高位bit也能影响table下标,减少哈希碰撞。

异或是因为:
key的hashCode只要有一个bit发生变化,hash函数的返回值也会跟着变化,用以减少哈希碰撞。

4、为什么计算下标用h & (length-1)?

作用上相当于取模mod或者取余%。
这意味着数组下标相同,并不表示hashCode相同。位操作肯定比取余操作快多了。

5、为什么重写了hashCode方法,也应该重写equals方法?

自定义的类作为key,hashCode方法和equals方法要么都不重写,要么都重写。
如果key只重写了hashCode方法,却没有重写equals方法。那么会造成map里会存在重复的我们认为“相同”的键值对在里面(一般是指,两个对象的成员是相同的)。因为如果添加了相同元素,根据put过程则发生哈希碰撞,本来这个相同元素不应该新增,但由于原始Object的equals方法逻辑使用==判断,所以只要地址值不同就肯定能添加进去。

如果key只重写了equals方法,却没有重写hashCode方法。那么也会造成map里会存在重复的我们认为“相同”的键值对在里面。因为使用了原始的hashCode了,有些该发生的哈希碰撞也就不会发生了,都不在一个哈希桶了,即使我们认为是“相同”的,也不会去调用你重写的equals方法的。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

一只IT攻城狮

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值