HashMap详解

目录

一、HashMap

二、哈希冲突

三、HashMap的基本属性及构造

3.1 基本属性

3.2 HashMap.Node

3.3 HashMap的构造器

四、HashMap的相对应操作

4.1 HashMap().put方法

4.2 HashMap().remove方法

4.3 HasmMap()的get方法

五、十万个为什么

5.1.为什么重写equals方法需同时重写hashCode方法?

5.2.HashMap 的Java 1.8版本相对于1.7版本进行了哪些优化?

3.HashMap的线程安全

5.4.LinkedHashMap

5.5.ConcurrentHashMap的锁更新


Map用于保存具有映射关系的数据,Map里保存着两组数据:key和value,它们都可以使任何引用类型的数据,但key不能重复。所以通过指定的key就可以取出对应的value。而HashMap是用哈希算法实现Map的具体实现类

一、HashMap

HashMap是基于哈希表的Map接口的非同步实现,继承自AbstractMap,AbstractMap是部分实现Map接口的抽象类。

并且允许使用null键和null值,因为key不允许重复,所以只能有一个就键为null,另外HashMap不能保证放入元素的顺序,所以他是无序的,和放入的顺序并不能完全相同。而且HashMap是线程不安全的(主要体现在Put方法)。

二、哈希冲突

如果两个不同的元素,通过哈希函数得出的实际存储地址相同怎么办

也就是说,当我们对某个元素进行哈希运算,得到一个存储地址,然后要进行插入的时候,发现已经被其他元素占用了,其实这就是所谓的哈希冲突,也叫哈希碰撞

前面我们提到过,哈希函数的设计至关重要,好的哈希函数会尽可能地保证 计算简单和散列地址分布均匀,但是,我们需要清楚的是,数组是一块连续的固定长度的内存空间,再好的哈希函数也不能保证得到的存储地址绝对不发生冲突。那么哈希冲突如何解决呢?哈希冲突的解决方案有多种:开放定址法(发生冲突,继续寻找下一块未被占用的存储地址),再散列函数法,链地址法,而HashMap即是采用了链地址法,也就是数组+链表的方式。

在之前的版本中,HashMap采用数组+链表实现,即使用链表处理冲突,同一hash值的链表都存储在一个链表里。但是当链表中的元素较多,即hash值相等的元素较多时,通过key值依次查找的效率较低。而JDK1.8中,HashMap采用数组+链表+红黑树实现,当链表长度超过阈值(8)时,将链表转换为红黑树,这样大大减少了查找时间。
HashMap数据结构图:

三、HashMap的基本属性及构造

3.1 基本属性
/**
* 默认初始化大小  16(位运算 1<<4 --> 2^4=16)
*/
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

/**
* HashMap最大容量  2^30
*/
static final int MAXIMUM_CAPACITY = 1 << 30;

/**
* 负载因子  当负载等于容量的0.75时,需要进行扩容
*/
static final float DEFAULT_LOAD_FACTOR = 0.75f;

/**
* 链表的最大长度,即需要转换红黑树的边界,当链表长度达到8的时候就需要将链表转换成树
*/
static final int TREEIFY_THRESHOLD = 8;

/**
* 在哈希表扩容时,如果发现链表长度小于 6,则会由树重新退化为链表。
*/
static final int UNTREEIFY_THRESHOLD = 6;

/**
* 在转变成树之前,还会有一次判断,只有键值对数量大于 64 才会发生转换。这是为了避免在哈希表建立初期,多个键值对恰好被放入了同一个链表中而导致不必要的转化。
*/
static final int MIN_TREEIFY_CAPACITY = 64;

为什么负载因子为0.75呢?
通过大量实验统计得出来的,如果过小,比如0.5,那么当存放的元素超过一半时就进行扩容,会造成资源的浪费;如果过大,比如1,那么当元素满的时候才进行扩容,会使get,put操作的碰撞几率增加。
同时hashmap不是无限增大容量的,当达到极限的时候就不再进行扩容:MAXIMUM_CAPACITY

扩容机制

扩容的步骤如下:

🔸 (1) 计算新容量

  • HashMap 新容量 = 旧容量 × 2,即 翻倍增长

  • 例如:16 → 32 → 64 → 128...

  • threshold 也会随之变化,threshold = newCapacity * loadFactor

🔸 (2) 创建新的数组

  • 创建一个新的哈希桶数组,大小是 newCapacity

🔸 (3) 重新计算哈希位置(rehash)

  • HashMap 需要重新分配所有节点的位置,因为扩容后数组长度变了,元素的存储索引 (hash & (length - 1)) 也会变。

  • Java 8 的优化:如果 hash & oldCapacity == 0,则节点位置不变,否则移动到新索引 oldIndex + oldCapacity

🔸 (4) 迁移元素

  • 逐个遍历旧数组中的元素,将其重新分配到新数组的合适位置。

  • 由于数组大小是 2 的幂,Java 8 通过 hash & oldCapacity 判断元素是否需要移动,提高了效率。

在 Java 7 和 Java 8 中,HashMap 的扩容方式有所不同:

版本扩容方式
Java 7重新计算哈希值并重新插入新数组(rehash,可能导致链表顺序变化)
Java 8采用 hash & oldCapacity 方式判断元素是否迁移,减少 rehash

Java 8 迁移优化

在 Java 8 中,HashMap 不会对所有元素重新计算哈希值,而是通过 hash & oldCapacity 来判断元素是否移动

  • hash & oldCapacity == 0,元素 留在原索引
  • hash & oldCapacity != 0,元素 移动到新索引 = 原索引 + oldCapacity

这种方式减少了 hashCode 计算次数,提升了扩容效率。

3.2 HashMap.Node<K, V>

HashMap.Node<K, V>HashMap 内部的一个静态类,它是哈希表的基本存储单元,表示存放在哈希桶(bucket)中的一个键值对(entry)。每个 Node 代表一个键值对,并且支持链地址法(拉链法)处理哈希冲突。


    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;//hash存储当前键的哈希值,加快查找效率,避免重复计算key.hashCode()。
        final K key;//存储键,final 修饰,防止修改。
        V value;//存储值,可以修改。
        Node<K,V> next;//指向链表的下一个节点,实现拉链法(链地址法)处理哈希冲突。

        //Node用于创建 Node 实例,并将 next 作为链表的下一个节点。如果发生哈希冲突,多个 Node 以链表的方式链接在一起。
        Node(int hash, K key, V value, Node<K,V> next) {
            this.hash = hash;
            this.key = key;
            this.value = value;
            this.next = next;
        }
        //获取键值对的 键 和 值,是 Map.Entry 接口的方法。
        public final K getKey() { return key; }
        public final V getValue() { return value; }
        public final String toString() { return key + "=" + value; }
        //计算 Node 的哈希值,^ 是异或运算,避免 key 和 value 哈希碰撞。
        public final int hashCode() {
            return Objects.hashCode(key) ^ Objects.hashCode(value);
        }
        //更新 value 并返回旧值。
        public final V setValue(V newValue) {
            V oldValue = value;
            value = newValue;
            return oldValue;
        }
        //判断两个 Node 是否相等,要求 key 和 value 都相等。
        public final boolean equals(Object o) {
            if (o == this)
                return true;

            return o instanceof Map.Entry<?, ?> e
                    && Objects.equals(key, e.getKey())
                    && Objects.equals(value, e.getValue());
        }
    }
3.3 HashMap的构造器

HashMap有4个构造器,其他构造器如果用户没有传入initialCapacity 和loadFactor这两个参数,会使用默认值,initialCapacity默认为16,loadFactory默认为0.75

1.HashMap(int initialCapacity, float loadFactor)//指定 初始容量 和 负载因子(扩容阈值)。

2.HashMap(int initialCapacity)//只指定初始容量,负载因子默认 0.75

3.HashMap()//默认构造器,初始容量 16,负载因子 0.75

4.public HashMap(Map<? extends K, ? extends V> m) {
    this.loadFactor = DEFAULT_LOAD_FACTOR;
    putMapEntries(m, false);
}

  • 直接使用已有 Map 作为数据源,创建一个新的 HashMap 并拷贝其数据。

  • 容量会根据 m.size() 计算,确保容器大小合理,减少扩容开销。

  • putMapEntries(m, false) 会遍历 m 并插入 HashMap,但不会触发扩容。

一般我们就是 new HashMap<>()直接使用了。

四、HashMap的相对应操作

4.1 HashMap().put方法
public V put(K key, V value) {
        return putVal(hash(key), key, value, false, true);
    }

那么直接看putVal,我加了注释

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;

    // 1. 判断哈希表 table 是否为空,若为空则调用 resize() 初始化
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    // 2. 计算存储索引 i,如果该位置为空,直接插入新节点
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {  // 3. 该位置已有节点,进行冲突处理,说明新的数据和旧的数据哈希冲突,即它们的哈希索引 i = (n - 1) & hash 计算结果相同,
//导致它们存储在 HashMap 的同一个桶(bucket)中。
        Node<K,V> e; K k;

        // 3.1 检查当前位置的第一个节点(桶头)是否与新插入的 key 相等
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p; // 找到相同 key 的节点,记录到 e
        // 3.2 该节点是树节点,调用 `putTreeVal` 进行红黑树插入
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else { // 3.3 该节点是链表,遍历链表查找是否已存在 key
            for (int binCount = 0; ; ++binCount) {
                if ((e = p.next) == null) { // 找到链表尾部
                    p.next = newNode(hash, key, value, null); // 在尾部插入新节点
                    if (binCount >= TREEIFY_THRESHOLD - 1) // 链表长度超过阈值,转为红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break; // 找到相同 key,退出循环
                p = e; // 继续向后遍历
            }
        }

        // 4. 如果 key 已存在,更新 value
        if (e != null) { 
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null) // onlyIfAbsent = true 表示仅在 key 不存在时插入
                e.value = value;
            afterNodeAccess(e); // 允许子类扩展,LinkedHashMap 复写该方法
            return oldValue;
        }
    }

    // 5. 结构修改计数 +1
    ++modCount;

    // 6. 插入后,若 size 超过 threshold,触发扩容
    if (++size > threshold)
        resize();

    afterNodeInsertion(evict); // 允许子类扩展
    return null;
}

核心逻辑解析

  1. 检查 HashMap 是否初始化。如果 table 为空,调用 resize() 进行初始化(默认容量 16)。

  2. 计算索引并插入,计算 i = (n - 1) & hash 确定数组索引位置。如果该位置为空,直接插入 newNode()

  3. 处理哈希冲突

    如果索引位置已有节点
    • 如果是链表,遍历链表:
      • 找到相同 key,则更新 value。
      • 否则,在链表尾部插入新节点。
      • 如果链表长度超过 TREEIFY_THRESHOLD=8,转换为红黑树。
    • 如果是 TreeNode(红黑树),调用 putTreeVal() 处理。
    • 如果 key 相同,直接更新 value。
  4. 更新 HashMap 结构

    • modCount++ 记录修改次数(用于 fail-fast)。
    • 如果 size 超过 threshold,调用 resize() 扩容。
  5. 调用钩子方法

    • afterNodeAccess(e): 允许 LinkedHashMap 复写该方法,用于维护访问顺序。
    • afterNodeInsertion(evict): 允许 LinkedHashMap 复写该方法,可能用于 LRU 逻辑。

扩展知识

  1. (n - 1) & hash 计算索引,n - 1 保证索引在 [0, n-1] 范围内,& 运算代替 % 运算,优化性能。

  2. 链表转红黑树,TREEIFY_THRESHOLD = 8,链表长度 ≥ 8 时,转换为红黑树,提高查找效率。

  3. 为什么 resize() 是 2 倍扩容?HashMap 使用 2^n 容量,扩容后 (n - 1) & hash 保持哈希分布均匀。二进制,是吧!

  4. 哈希冲突,有两种情况:

    1. 完全相同的 key(哈希值相同,key 也相同)

      • 这意味着新数据的 key 和旧数据的 key 是相同的。
      • 此时 putVal() 方法会更新 value,而不是新增节点。
    2. 不同的 key 但哈希值相同或索引相同(哈希冲突)

      • 由于 HashMap 采用 (n - 1) & hash 计算索引,不同的 key 可能映射到同一个索引位置。
      • 此时,HashMap 采用链地址法(链表或红黑树)来存储冲突的元素:
        • 链表:如果冲突的节点较少(< 8 个),采用链表存储。
        • 红黑树:如果冲突的节点 ≥ 8 个,转换为红黑树存储,提高查找效率。
4.2 HashMap().remove方法
 public V remove(Object key) {
        Node<K,V> e;
        return (e = removeNode(hash(key), key, null, false, true)) == null ?
            null : e.value;
 }
/**
 * 移除指定 key 对应的节点(Node)。
 *
 * @param hash       key 的 hash 值
 * @param key        要删除的 key
 * @param value      可选:如果 matchValue 为 true,则仅在值匹配时删除
 * @param matchValue 是否匹配 value,true 表示 key-value 必须匹配才能删除
 * @param movable    是否允许调整数据结构(主要用于红黑树)
 * @return           被移除的节点(如果存在),否则返回 null
 */
final Node<K,V> removeNode(int hash, Object key, Object value,
                           boolean matchValue, boolean movable) {
    Node<K,V>[] tab;   // 哈希表(数组)
    Node<K,V> p;       // 当前遍历的节点
    int n, index;      // 数组长度 & 计算出的索引

    // 1. 确保哈希表非空,并找到 key 可能所在的桶(bucket)
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (p = tab[index = (n - 1) & hash]) != null) { // 计算索引 index = (n - 1) & hash
        Node<K,V> node = null, e; // `node` 代表要删除的节点,e只是个辅助变量,最开始为null,到后面才赋值
        K k;
        V v;

        // 2. 检查桶的头结点是否匹配(hash 值相同且 key 相等)
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            node = p;

        // 3. 如果头结点不匹配,遍历后续节点(链表 或 红黑树)
        else if ((e = p.next) != null) {
            if (p instanceof TreeNode) // 3.1 如果是红黑树,使用树的方式查找节点
                node = ((TreeNode<K,V>)p).getTreeNode(hash, key);
            else { // 3.2 普通链表,逐个查找
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key ||
                         (key != null && key.equals(k)))) {
                        node = e; // 找到要删除的节点
                        break;
                    }
                    p = e; // 继续遍历
                } while ((e = e.next) != null);
            }
        }

        // 4. 如果找到了 key 对应的节点
        if (node != null && (!matchValue || (v = node.value) == value ||
                             (value != null && value.equals(v)))) {
            if (node instanceof TreeNode) // 4.1 红黑树节点,调用树的删除方法
                ((TreeNode<K,V>)node).removeTreeNode(this, tab, movable);
            else if (node == p) // 4.2 链表头结点,直接更新 table[index]
                tab[index] = node.next;
            else // 4.3 链表中的普通节点,修改前驱节点的 next 指针
                p.next = node.next;

            ++modCount; // 结构发生变化,修改计数
            --size; // 元素数量减少
            afterNodeRemoval(node); // 触发子类的扩展方法
            return node; // 返回删除的节点
        }
    }
    return null; // 没找到,返回 null
}
  1. 哈希寻址

    • 计算索引:index = (n - 1) & hash,找到 key 可能所在的桶。
    • 先检查头结点是否匹配,再遍历链表或红黑树查找。
  2. 删除节点

    • 如果是红黑树节点,调用 removeTreeNode()
    • 如果是链表的头结点,直接修改 table[index]
    • 如果是链表的中间节点,调整前驱节点的 next 指针。
  3. 维护 HashMap 结构

    • modCount++:保证并发修改时 fail-fast 机制生效。
    • size--:元素数量减少。
    • afterNodeRemoval():提供给子类扩展,如 LinkedHashMap 可用于维护访问顺序。
4.3 HasmMap()的get方法
public V get(Object key) {
    Node<K, V> e;
    // 调用 getNode 方法查找键对应的节点,如果找到则返回其值,否则返回 null
    return (e = getNode(key)) == null ? null : e.value;
}

/**
 * 获取指定 key 对应的节点。
 * 该方法实现了 Map.get 及相关方法的逻辑。
 *
 * @param key 要查找的键
 * @return 对应的节点,如果不存在则返回 null
 */
final Node<K, V> getNode(Object key) {
    Node<K, V>[] tab; // 哈希表数组
    Node<K, V> first, e; // first:桶中的第一个节点,e:用于遍历链表的辅助变量
    int n, hash; // n:哈希表长度,hash:键的哈希值
    K k; // 用于存储节点的键

    // 检查哈希表是否为空,并获取哈希表长度
    if ((tab = table) != null && (n = tab.length) > 0 &&
        // 计算 key 的哈希值,并定位到对应的桶
        (first = tab[(n - 1) & (hash = hash(key))]) != null) {

        // 如果桶中的第一个节点的哈希值和 key 相同,并且 key 也相等,则直接返回该节点
        if (first.hash == hash && 
            ((k = first.key) == key || (key != null && key.equals(k))))
            return first;

        // 如果该桶存在链表或红黑树结构
        if ((e = first.next) != null) {
            // 如果是红黑树结构,则调用红黑树的 getTreeNode 方法查找
            if (first instanceof TreeNode)
                return ((TreeNode<K, V>) first).getTreeNode(hash, key);
            
            // 遍历链表查找匹配的节点
            do {
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    return e;
            } while ((e = e.next) != null);
        }
    }
    // 如果未找到对应的节点,则返回 null
    return null;
}

五、十万个为什么

5.1.为什么重写equals方法需同时重写hashCode方法?

1. HashMap 的存储规则

HashMap 依赖 hashCode + equals 进行 key 的存储和查找:

  1. 计算 keyhashCode,确定存储的桶索引。

  2. 如果该桶已有元素

    • 遍历桶内的链表(或红黑树),调用 equals() 逐个比较 key。

    • equals() 返回 true,则认为 key 相同,更新 value

2. 如果只重写 equals(),会导致 HashMap 失效

HashMap 依赖 hashCode() 先定位 key 的存储位置,若 hashCode() 计算错误,可能会导致:

  • 同一个 key 被存储到不同的 bucket,导致查找失败

  • HashMap 认为两个相等的 key 是不同的 key,导致数据重复存储

例子

class Person {
    String name;

    Person(String name) {
        this.name = name;
    }

   //@Override 注释掉
   // public int hashCode() {
   //     return name.hashCode(); // 让 hashCode 一致
   // }

    @Override
    public boolean equals(Object obj) {
        if (this == obj) return true;
        if (!(obj instanceof Person)) return false;
        Person other = (Person) obj;
        return this.name.equals(other.name);
    }
}

public class Main {
    public static void main(String[] args) {
        HashMap<Person, String> map = new HashMap<>();
        Person p1 = new Person("Alice");
        Person p2 = new Person("Alice");

        map.put(p1, "Person 1");
        System.out.println(map.get(p2)); // 可能返回 null
    }
}

记住,所有基于哈希的数据结构(如 HashMapHashSetHashtableConcurrentHashMap)都需要同时重写 equals()hashCode(),否则可能会导致存储和查找异常。但对于 TreeSet、TreeMap、ArrayList、LinkedList 等非哈希结构的集合,就 不要求 重写 hashCode(),但可能需要重写 equals()compareTo()(如果它们基于排序)。

5.2.HashMap 的Java 1.8版本相对于1.7版本进行了哪些优化?

1.数组+链表改成了数组+链表或红黑树

  1. HashMap 采用 数组 + 链表 结构时,在哈希冲突较多的情况下,查询效率可能退化为 O(n)。从 JDK 8 开始,当链表长度超过 8(阈值)时,自动转换为 红黑树,使查询效率提升为 O(log n)。

  2. 插入性能优化,传统链表插入是 O(1),而红黑树插入是 O(log n),因此:在链表过长时,转换为 红黑树,提高查询性能。在链表较短时,使用 链表,保持高效插入

  3. 删除效率提升,链表 删除元素需要遍历,最坏 O(n)。红黑树 删除元素 O(log n),提升删除性能。

2.头插法变尾插法

链表的插入方式从头插法改成了尾插法,简单说就是插入时,如果数组位置上已经有元素,1.7将新元素放到数组中,原始节点作为新节点的后继节点,1.8遍历链表,将元素放置到链表的最后;尾插法避免了闭环的发生。因为在 多线程环境 下,多个线程同时扩容 HashMap 时,由于头插法会反转链表的顺序,可能会导致 环形链表,进而引发 死循环

3.扩容机制判断更改

在插入时,1.7先判断是否需要扩容,再插入,1.8先进行插入,插入完成再判断是否需要扩容;扩容的时候1.7需要对原数组中的元素进行重新hash定位在新数组的位置,1.8采用更简单的判断逻辑,位置不变或索引+旧容量大小。(因为二进制,&运算和扩容是2倍数)

3.HashMap的线程安全

首先是HashMap,他是线程不安全的。在多线程环境下,JDK 1.7 会产生死循环(闭环)、数据丢失、数据覆盖的问题,JDK 1.8 中会有数据覆盖的问题。

如何实现线程安全的Map呢? HashTable和ConcurrentHashMap

主要区别对比

特性

HashMap

Hashtable

ConcurrentHashMap

线程安全

非线程安全

线程安全(使用 synchronized

线程安全(使用分段锁)

性能

⭐⭐⭐⭐⭐ 最快(无锁)

⭐⭐ 最慢(全局锁)

⭐⭐⭐⭐ 高效(局部锁)

锁机制

❌ 无

synchronized 整个方法(全局锁)

volatile+CAS + 分段锁(局部锁,提高并发性能)

数据结构

数组 + 链表 + 红黑树(JDK 8+)

数组 + 链表

数组 + 链表 + 红黑树

允许 null 键值

允许 null key 和 null value

都不允许

不允许

适用场景

单线程,非线程安全

多线程(但性能差)

高并发,多线程场景

JDK 版本

JDK 1.2

JDK 1.0(过时,不推荐)

JDK 1.5+

5.4.LinkedHashMap

LinkedHashMap 继承自 HashMap,在 HashMap 的**哈希表(数组 + 链表/红黑树)**基础上,额外维护了一个双向链表,用于记录访问顺序或插入顺序。

LinkedHashMap 继承了 HashMap.Node,并新增了 beforeafter 指针,形成双向链表:

static class Entry<K,V> extends HashMap.Node<K,V> {
    Entry<K,V> before, after; // 指向前后节点的指针(双向链表)
    
    Entry(int hash, K key, V value, Node<K,V> next) {
        super(hash, key, value, next);
    }
}

因为它们存储了对其他节点的引用,用于链接 LinkedHashMap 中的节点,使其形成双向链表。

在 Java 中,尽管没有指针的概念,但对象引用(Reference) 在本质上起到了类似指针的作用。

5.5.ConcurrentHashMap的锁更新

🔹 1. 分段锁(Segment Lock)

分段锁是 ConcurrentHashMap 中的一种实现机制,最早出现在 Java 5 版本的实现中(在 Java 8 中,分段锁被优化为更细粒度的锁,即桶锁)。

工作原理:

  • 分段(Segment) 是一个 HashMap 的子结构,每个分段是一个独立的 HashMap 实例,并且每个分段都有自己独立的锁。(所以内存开销较大)

  • 每个分段 存储一部分数据,每个分段的大小通常与总容量成比例。

  • 操作流程

    • 当一个线程访问某个元素时,首先会计算出该元素所在的分段。

    • 然后,线程只会锁住对应的分段,而不是整个 ConcurrentHashMap,从而避免多个线程对整个 HashMap 进行互斥。

    • 这样就实现了在多个线程并发操作时的并行性。

优点:

  • 减少锁竞争:多个线程可以并发地访问不同的分段,减少了锁竞争。

  • 提高并发性:通过分段锁机制,ConcurrentHashMap 可以在多核处理器上提供更高的并发性能。

缺点:

  • 分段锁导致内存开销:每个分段都需要额外的存储空间,这会导致内存使用的开销。

  • 实现复杂性:分段锁的实现相对复杂,需要管理多个分段和各自的锁。

2. 桶锁(Bucket Lock)

Java 8 中,ConcurrentHashMap 采用了更细粒度的锁,称为 桶锁。这种方式实际上将分段锁替换为对单个桶的锁。

工作原理:

  • ConcurrentHashMap 的底层是一个数组,其中的每个元素称为 ,每个桶用于存储多个键值对,采用链表或者红黑树来组织。

  • 每个桶 都有一个独立的锁,线程在访问某个桶时,只会锁定该桶而不是整个 ConcurrentHashMap

  • 桶的数量 与容量成比例,每个桶都独立存储不同的哈希值对应的数据。

  • 并发读写:多个线程可以并发访问不同的桶,同时锁定不同的桶,避免了大量线程竞争一个锁。

优点:

  • 更细粒度的锁:通过对桶的单独加锁,ConcurrentHashMap 实现了比分段锁更细粒度的并发控制。

  • 提高性能:减少了竞争的范围,尤其是对于大容量的 ConcurrentHashMap,不同线程访问不同的桶时,几乎没有锁竞争,性能更优。

  • 节省内存:相比分段锁,桶锁的实现内存开销较小,因为每个桶只需要锁,而不是整个分段。

缺点:

  • 锁粒度过细:对于一些非常小的映射,桶锁可能会导致资源过度分配,尤其是在桶数较多的情况下,内存可能会浪费。

相关文章:

Java集合类--超详细整理_java集合类(代码手写实现,全面梳理)-CSDN博客文章浏览阅读878次,点赞5次,收藏6次。目录1.什么是Java集合类?1.1 什么是Java集合API?1.2什么是Iterator?2.集合和数组的区别3.Collection集合的方法4.常用集合的分类(总结)4.1List和Set集合详解4.2 Map详解由于近期面试都或多或少提到了集合类,可见其重要性和实用性,于是结合以前的知识,参考了一些博客和贴吧论坛,整理了以下笔记并且优化了以下排版,有一些简单易懂的图片也借鉴了一下,主要讲解的是各个具体实现类的特性,结构优缺点等等。本文用于学习交流,若有不足之.._java集合类(代码手写实现,全面梳理) https://blog.csdn.net/LoveFHM/article/details/106556499?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522921d53f59fe1d96551e0cb1a7ba42d2a%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=921d53f59fe1d96551e0cb1a7ba42d2a&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-1-106556499-null-null.nonecase&utm_term=%E9%9B%86%E5%90%88&spm=1018.2226.3001.4450

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

&岁月不待人&

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

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

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

打赏作者

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

抵扣说明:

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

余额充值