当前位置: 代码迷 >> 综合 >> Java 集合 (10) -- ConcurrentHashMap 类
  详细解决方案

Java 集合 (10) -- ConcurrentHashMap 类

热度:18   发布时间:2023-12-16 13:20:39.0

文章目录

    • 1. 简介
    • 2. 继承体系
    • 3. 字段
    • 4. 构造器
    • 5. 常用方法

1. 简介

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>implements ConcurrentMap<K,V>, Serializable {
    }

ConcurrentHashMap 是线程安全的 HashMap 版本,并且它采用分段锁的思想实现了线程安全,其效率比通过全局锁实现线程安全的 HashTable 高得多。

JDK 1.7 及以前,ConcurrentHashMap 底层采用 segment 数组 + HsahEntry 数组来存储元素,JDK 1.8 开始采用 数组 + 链表/红黑树 来存储元素;ConcurrentHashMap 采用的锁有 synchronized,CAS,自旋锁,分段锁,volatile 等。

ConcurrentHashMap 中没有 threshold(阈值等于 容量(数组长度) * 加载因子)和 loadFactor 这两个字段,而是采用 sizeCtl 来控制:

  1. sizeCtl = -1,表示有线程正在进行初始化操作;
  2. sizeCtl = 0,默认值,表示后续在真正初始化的时候使用默认容量;
  3. sizeCtl > 0,在初始化之前存储的是传入的容量,在初始化或扩容后存储的是下一次的扩容门槛;
  4. sizeCtl = (resizeStamp << 16) + (1 + nThreads),表示有n个线程正在一起扩容,高位存储扩容邮戳,低位存储扩容线程数加 1

ConcurrentHashMap 与 HashMap 的区别 ?线程安全方面;底层数据结构,分版本。

ConcurrentHashMap 与 HashTable 的区别 ?HashTable 使用全局锁对整张哈希表加锁让线程独占,效率低下,而 ConcurrentHashMap 使用分段锁思想,对数组上的每一个桶都加了锁,各个桶可以并发操作,效率非常高。

2. 继承体系

在这里插入图片描述

3. 字段

4. 构造器

  1. public ConcurrentHashMap() {
          
    }
    
  2. public ConcurrentHashMap(int initialCapacity) {
          if (initialCapacity < 0)throw new IllegalArgumentException();int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?MAXIMUM_CAPACITY :tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));this.sizeCtl = cap;
    }
    
  3. public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
          this.sizeCtl = DEFAULT_CAPACITY;putAll(m);
    }
    
  4. public ConcurrentHashMap(int initialCapacity, float loadFactor) {
          this(initialCapacity, loadFactor, 1);
    }
    
  5. //concurrencyLevel 并发数,默认是 16
    public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) {
          if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)throw new IllegalArgumentException();if (initialCapacity < concurrencyLevel)   // Use at least as many binsinitialCapacity = concurrencyLevel;   // as estimated threadslong size = (long)(1.0 + (long)initialCapacity / loadFactor);int cap = (size >= (long)MAXIMUM_CAPACITY) ?MAXIMUM_CAPACITY : tableSizeFor((int)size);this.sizeCtl = cap;
    }
    

5. 常用方法

  1. public V put(K key, V value):如果桶数组未初始化,则初始化;如果待插入的元素所在的桶为空,则尝试把此元素直接插入到桶的第一个位置;如果正在扩容,则当前线程一起加入到扩容的过程中;如果待插入的元素所在的桶不为空且不在迁移元素,则锁住这个桶(分段锁);如果当前桶中元素以链表方式存储,则在链表中寻找该元素或者插入元素;如果当前桶中元素以红黑树方式存储,则在红黑树中寻找该元素或者插入元素;如果元素存在,则返回旧值 (并没有替换旧值);如果元素不存在,整个Map的元素个数加1,并检查是否需要扩容。添加元素操作中使用的锁主要有(自旋锁 + CAS + synchronized + 分段锁)
  2. public V remove(Object key):删除元素跟添加元素一样,都是先找到元素所在的桶,然后采用分段锁的思想锁住整个桶,再进行操作:计算hash;如果所在的桶不存在,表示没有找到目标元素,返回;如果正在扩容,则协助扩容完成后再进行删除操作;如果是以链表形式存储的,则遍历整个链表查找元素,找到之后再删除;如果是以树形式存储的,则遍历树查找元素,找到之后再删除,删除元素之后树较小,则退化成链表;如果确实删除了元素,则整个map元素个数减1,并返回旧值;如果没有删除元素,则返回null。
  3. public V get(Object key):获取元素,根据目标key所在桶的第一个元素的不同采用不同的方式获取元素,关键点在于find()方法的重写:hash到元素所在的桶;如果桶中第一个元素就是该找的元素,直接返回;如果是树或者正在迁移元素,则调用各自Node子类的find()方法寻找元素;如果是链表,遍历整个链表寻找元素。获取元素的过程没有加锁
  4. public int size():元素个数的存储也是采用分段的思想,获取元素个数时需要把所有段加起来:元素的个数依据不同的线程存在在不同的段里;计算CounterCell所有段及baseCount的数量之和。获取元素个数的过程也没有加锁
  相关解决方案