ConcurrentHashMap和HashMap的区别

HashMap

  • HashMap 与 HashSet 一样,不保证存储的顺序,因为底层是以 hash 表的方式存储的;
  • HashMap 底层存储结构为 数组 + 链表+红黑树 (Java 8);
  • HashMap 存储的 key-value 数据类型为 HashMap N o d e 类型,该类型实现了 M a p Node 类型,该类型实现了 Map Node类型,该类型实现了MapEntry 接口;
  • HashMap 没有实现同步,因此是线程不安全的;
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 默认的 table 数组容量 aka 16
static final float DEFAULT_LOAD_FACTOR = 0.75f;  // 默认加载因子为 0.75
static final int MAXIMUM_CAPACITY = 1 << 30; // 集合最大容量的上限是:2的30次幂
static final int TREEIFY_THRESHOLD = 8; // 链表树化临界值
static final int UNTREEIFY_THRESHOLD = 6; // 树转成链表的临界值
static final int MIN_TREEIFY_CAPACITY = 64; // 树化时数组的最小长度

transient Node<K,V>[] table;  // 存放元素的数组
transient Set<Map.Entry<K,V>> entrySet;  // 存放元素的缓存
transient int size;  // HashMap 中实际元素个数
transient int modCount;  // HashMap 修改次数,每个扩容和更改map结构的计数器
int threshold;  // table 扩容临界值 数组长度 * 加载因子
final float loadFactor;  // table 加载因子

HashMap初始化和扩容机制

transient Node<K,V>[] table;
  • 创建 HashMap 对象时,数组 table 默认为 null,加载因子 loadfactor 默认为 0.75;
  • 创建 HashMap 对象时,数组 table 默认为 null,加载因子 loadfactor 默认为 0.75;
public HashMap() {
    this.loadFactor = DEFAULT_LOAD_FACTOR; // all other fields defaulted
}
  • 第一次添加元素时,判断 table 为空数组,默认将 table 扩容到 16,阈值 threshold 为数组长度与加载因子的积 12(resize() 方法);
  • 当添加元素 key-value 时,通过 key 的 hash 值计算该元素在 table 中的索引位置,判断该索引位置是否存在 Node 结点,没有则直接添加;
  • 如果索引位置有结点,则判断要添加的元素的 key 与该节点的 key 是否相同,如果相同,则将 value 替换到当前结点,如果不相同,则判断当前结点是否是树结构,是树结构则执行添加树结点操作;
  • 如果不是,判断为链表结构,且要添加的元素和链表第一个结点不同,遍历链表判断 key 是否存在,不存在则向链表插入元素,并判断当前链表是否满足树化条件(链表长度大于等于8),如果存在执行替换操作;
  • 当 table 长度大于阈值 threshold 时,则执行 resize() 方法给 table 进行扩容,扩容大小为原来的 2 倍,同时阈值更新为当前数组长度乘以加载因子;
final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table; // 获取旧的哈希表数组
    int oldCap = (oldTab == null) ? 0 : oldTab.length; // 获取旧的容量,如果旧哈希表为空,则容量为0
    int oldThr = threshold; // 获取旧的阈值
    int newCap, newThr = 0; // 定义新的容量和阈值,初始值设为0
    if (oldCap > 0) { // 如果旧的容量大于0
        if (oldCap >= MAXIMUM_CAPACITY) { // 如果旧容量超过了最大容量
            threshold = Integer.MAX_VALUE; // 将阈值设为Integer.MAX_VALUE
            return oldTab; // 返回旧的哈希表数组
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && // 计算新的容量,不能超过最大容量
                 oldCap >= DEFAULT_INITIAL_CAPACITY) // 如果旧容量大于等于默认容量16
            newThr = oldThr << 1; // 计算新的阈值,即旧阈值的2倍
    }
    else if (oldThr > 0) // 如果旧的哈希表数组为空,但旧的阈值不为0
        newCap = oldThr; // 直接使用旧的阈值作为新的容量
    else { // 如果旧的哈希表数组和阈值都为空
        newCap = DEFAULT_INITIAL_CAPACITY; // 使用默认容量16作为新的容量
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 计算新的阈值,即默认容量16乘以加载因子0.75
    }
    if (newThr == 0) { // 如果新的阈值仍然为0
        float ft = (float)newCap * loadFactor; // 计算新阈值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE); // 如果新容量小于最大容量且新阈值小于最大整数,则使用新阈值;否则使用最大整数
    }
    threshold = newThr; // 将阈值设置为新的阈值
    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 创建新的哈希表数组
    table = newTab; // 将哈希表数组引用指向新的哈希表数组
    if (oldTab != null) { // 如果旧的哈希表数组不为空
        for (int j = 0; j < oldCap; ++j) { // 遍历旧的哈希表数组
            Node<K,V> e; // 定义节点e
            if ((e = oldTab[j]) != null) { // 如果节点e不为空
                oldTab[j] = null; // 将旧的哈希表数组对应位置置为空
                if (e.next == null) // 如果节点e没有冲突
                    newTab[e.hash & (newCap - 1)] = e; // 直接将节点e放入新的哈希表数组中
                else if (e instanceof TreeNode) // 如果节点e是树节点
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 将树节点分裂成两个链表
                else { // 如果节点e为普通链表节点
                    Node<K,V> loHead = null, loTail = null; // 定义低位链表的头节点和尾节点
                    Node<K,V> hiHead = null, hiTail = null; // 定义高位链表的头节点和尾节点
                    Node<K,V> next; // 定义下一个节点
                    do {
                        next = e.next; // 获取下一个节点
                        if ((e.hash & oldCap) == 0) { // 如果节点e的hash值与旧容量的按位与结果为0
                            if (loTail == null) // 如果低位链表尾节点为空
                                loHead = e; // 将节点e作为低位链表的头节点
                            else
                                loTail.next = e; // 将节点e连接到低位链表的尾部
                            loTail = e; // 更新低位链表的尾节点
                        }
                        else { // 如果节点e的hash值与旧容量的按位与结果为1
                            if (hiTail == null) // 如果高位链表尾节点为空
                                hiHead = e; // 将节点e作为高位链表的头节点
                            else
                                hiTail.next = e; // 将节点e连接到高位链表的尾部
                            hiTail = e; // 更新高位链表的尾节点
                        }
                    } while ((e = next) != null); // 遍历链表
                    if (loTail != null) { // 如果低位链表不为空
                        loTail.next = null; // 将低位链表的尾节点置为空
                        newTab[j] = loHead; // 将低位链表连接到新的哈希表数组中对应位置
                    }
                    if (hiTail != null) { // 如果高位链表不为空
                        hiTail.next = null; // 将高位链表的尾节点置为空
                        newTab[j + oldCap] = hiHead; // 将高位链表连接到新的哈希表数组中对应位置
                    }
                }
            }
        }
    }
    return newTab; // 返回新的哈希表数组
}

ConcurrentHashMap



private static final int MAXIMUM_CAPACITY = 1 << 30;
//        定义最大容量:根据需求定义哈希表的最大容量,例如1 << 30(2^30)。
//
//        检查容量:在添加或调整哈希表大小之前,始终检查当前容量是否已达到最大容量。如果达到最大容量,则停止添加新元素或进行扩容操作。
//
//        调整容量:如果当前容量未达到最大容量且需要扩容时,可以执行相应的扩容操作。这可能涉及创建一个更大的数组,并将原始数据重新散列到新的数组中。
//
//        控制阈值:可以定义负载因子和树化阈值来控制哈希表的大小和性能。负载因子表示哈希表的使用程度,当达到一定程度时,可以触发扩容操作。树化阈值表示将链表转换为树结构的节点数量阈值,以提高查找效率。
      

  



    private static final int DEFAULT_CAPACITY = 16;
 //哈希表的默认初始容量,必须是2的幂(即至少为1)且不超过最大容量。




    static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
//最大可能的(非2的幂)数组大小。toArray和相关方法需要使用该值



    private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
//哈希表的默认并发级别。此值未使用,但定义是为了与此类的先前版本兼容。



    private static final float LOAD_FACTOR = 0.75f;
//哈希表的负载因子。构造函数中对此值的覆盖仅影响初始表容量。实际的浮点值通常不被使用——更简单地使用表达式,如{@code n - (n >>> 2)}用于相关的重新调整大小阈值。



    static final int TREEIFY_THRESHOLD = 8;
//链表转换为树的节点数量阈值。当向具有至少这么多节点的桶中添加元素时,桶会被转换为树。该值必须大于2,并且应至少为8,以便与关于在收缩时转换回普通桶的假设相吻合。
//当链表的结点大于8的时候,链表会转换为红黑树



    static final int UNTREEIFY_THRESHOLD = 6;

//在调整大小操作期间取消将(拆分的)桶转换为树的节点数量阈值。应小于TREEIFY_THRESHOLD,并且最多为6,以适应删除下的收缩检测
//当树的结点小于6的时候会转换为链表


    static final int MIN_TREEIFY_CAPACITY = 64;
//可以将桶转换为树的最小表容量。否则,如果一个桶中的节点太多,将调整表的大小。该值应至少为4 * TREEIFY_THRESHOLD,以避免调整大小和树化阈值之间的冲突。

//当数组的元素大于64,转化为红黑树



    private static final int MIN_TRANSFER_STRIDE = 16;
//每个传输步骤的最小重分配次数。范围被细分以允许多个调整大小线程。此值作为下限,以避免调整大小器遇到过多的内存争用。该值应至少是DEFAULT_CAPACITY。
//并行处理的最小值



    private static int RESIZE_STAMP_BITS = 16;
//在sizeCtl中用于记录大小戳的位数。对于32位数组,必须至少为6。



    private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;
//可以帮助调整大小的最大线程数。必须适应32 - RESIZE_STAMP_BITS位。



    private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;
//在sizeCtl中记录大小戳的位移量。



    static final int MOVED     = -1; // 转发节点的哈希值
    static final int TREEBIN   = -2; // 树的根节点的哈希值
    static final int RESERVED  = -3; //临时预留的哈希值
    static final int HASH_BITS = 0x7fffffff; // 普通节点哈希值的可用位。

  
    static final int NCPU = Runtime.getRuntime().availableProcessors();//CPU的数量,用于限制某些大小。


put

final V putVal(K key, V value, boolean onlyIfAbsent) {
    // 检查key和value是否为null,如果是null则抛出NullPointerException异常
    if (key == null || value == null) throw new NullPointerException();
    
    // 计算哈希值,使用spread()方法对key.hashCode()进行处理,以减少哈希冲突
    int hash = spread(key.hashCode());
    
    // 用于记录链表长度的计数器
    int binCount = 0;
    
    // 使用无限循环对ConcurrentHashMap的table进行操作
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        
        // 如果table为null或长度为0,则调用initTable()方法进行初始化
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        
        // 如果当前位置tab[i]为空,则使用CAS操作将新节点加入到该位置,这里不需要加锁
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null)))
                break; // no lock when adding to empty bin
        }
        
        // 如果当前位置tab[i]的哈希值为MOVED,表示正在进行扩容操作,调用helpTransfer()方法来帮助进行扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        
        else {
            V oldVal = null;
            
            // 在synchronized块中,检查当前位置tab[i]是否仍然等于f,确保其它线程未修改
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        // 如果当前位置tab[i]是普通的链表节点
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            
                            // 查找与key相等的节点
                            if (e.hash == hash && ((ek = e.key) == key ||
                                (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                
                                // 如果onlyIfAbsent为false,则更新节点的值为value
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            
                            Node<K,V> pred = e;
                            
                            // 遍历到链表的末尾仍未找到与key相等的节点,则在链表末尾添加新节点
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key, value, null);
                                break;
                            }
                        }
                    }
                    
                    // 如果当前位置tab[i]是一个红黑树节点(TreeBin)
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        
                        // 调用putTreeVal()方法将节点添加到红黑树中
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
                            oldVal = p.val;
                            
                            // 如果onlyIfAbsent为false,则更新节点的值为value
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            
            // 如果binCount不为0,则判断是否需要将链表转化为红黑树,如果超过了阈值,则调用treeifyBin()方法进行转化
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    
    // 通过调用addCount()方法来更新ConcurrentHashMap的计数器
    addCount(1L, binCount);
    
    // 返回null,表示没有旧值
    return null;
}

get

public V get(Object key) {
    Node<K,V>[] tab; // 声明哈希表数组
    Node<K,V> e, p; // 当前节点和找到的节点
    int n, eh; // 数组长度和当前节点的哈希值
    K ek; // 当前节点的键

    int h = spread(key.hashCode()); // 计算目标键的哈希值,并进行处理以减少哈希冲突

    // 检查哈希表数组是否不为空,并且数组长度大于0,同时获取哈希值对应位置的第一个节点e
    if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) {
        // 判断第一个节点的哈希值是否与目标哈希值相等
        if ((eh = e.hash) == h) {
            // 如果当前节点的键与目标键相等,则返回当前节点的值
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        } else if (eh < 0) {
            // 如果当前节点的哈希值小于0,表示当前节点是一个树节点(TreeBin),调用find()方法在红黑树中查找指定键的节点
            return (p = e.find(h, key)) != null ? p.val : null;
        }
        
        // 遍历链表中的节点,直到链表末尾
        while ((e = e.next) != null) {
            // 如果当前节点的哈希值与目标哈希值相等,并且键与目标键相等,则返回当前节点的值
            if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    // 如果未找到匹配的节点,则返回null,表示没有找到对应的值
    return null;
}

remove

public final void remove() {
    Node<K,V> p = current; // 当前节点
    if (p == null)
        throw new IllegalStateException(); // 如果当前节点为null,则抛出异常
    if (modCount != expectedModCount)
        throw new ConcurrentModificationException(); // 如果修改计数与预期的修改计数不相等,则抛出异常,表示集合已被修改
    current = null; // 将当前节点置为null
    K key = p.key; // 获取当前节点的键
    removeNode(hash(key), key, null, false, false); // 调用removeNode()方法移除具有指定哈希值和键的节点
    expectedModCount = modCount; // 更新预期的修改计数为当前的修改计数
}

resize

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table; // 保存旧的table数组
    int oldCap = (oldTab == null) ? 0 : oldTab.length; // 获取旧的table数组的长度,如果为null则长度为0
    int oldThr = threshold; // 保存旧的阈值
    int newCap, newThr = 0; // 新的容量和阈值

    if (oldCap > 0) { // 如果旧的容量大于0
        if (oldCap >= MAXIMUM_CAPACITY) { // 如果旧的容量大于等于最大容量
            threshold = Integer.MAX_VALUE; // 设置阈值为最大整数值
            return oldTab; // 返回旧的table数组
        }
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY && oldCap >= DEFAULT_INITIAL_CAPACITY) {
            newThr = oldThr << 1; // 新的阈值为旧的阈值左移一位(即乘以2)
        }
    }
    else if (oldThr > 0) { // 如果旧的阈值大于0
        newCap = oldThr; // 新的容量为旧的阈值
    }
    else { // 否则,使用默认值
        newCap = DEFAULT_INITIAL_CAPACITY; // 新的容量为默认初始容量
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY); // 计算新的阈值
    }

    if (newThr == 0) { // 如果新的阈值为0
        float ft = (float)newCap * loadFactor; // 计算扩容后的临界值
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ? (int)ft : Integer.MAX_VALUE); // 根据条件确定最终的阈值
    }

    threshold = newThr; // 更新阈值

    @SuppressWarnings({"rawtypes","unchecked"})
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap]; // 创建新的table数组
    table = newTab; // 更新table引用为新的table数组

    if (oldTab != null) { // 如果旧的table数组不为null
        for (int j = 0; j < oldCap; ++j) { // 遍历旧的table数组
            Node<K,V> e;
            if ((e = oldTab[j]) != null) { // 如果当前位置有节点
                oldTab[j] = null; // 将旧的table数组当前位置置为null
                if (e.next == null) // 如果当前节点的下一个节点为null
                    newTab[e.hash & (newCap - 1)] = e; // 直接放入新的table数组对应位置
                else if (e instanceof TreeNode) // 如果是树节点
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap); // 则进行树节点的拆分操作
                else { // 否则(是链表节点),需要保持原来的顺序
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        next = e.next;
                        if ((e.hash & oldCap) == 0) { // 根据hash值决定放入哪个链表
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab; // 返回新的table数组
}

区别

  1. 线程安全性:HashMap是非线程安全的,而ConcurrentHashMap是线程安全的。在多线程环境下,如果多个线程同时对HashMap进行操作,可能会导致数据不一致或出现异常。而ConcurrentHashMap通过使用分段锁(Segment)或CAS操作等机制来实现高效的并发访问控制,保证了线程安全性。
  2. 性能:由于ConcurrentHashMap采用了分段锁或CAS操作等机制来实现并发控制,它在高并发情况下具有更好的性能表现。而HashMap在多线程环境下需要手动进行同步保护,性能较低。
  3. 迭代器弱一致性:HashMap的迭代器是弱一致的,即在迭代过程中,如果其他线程对HashMap进行修改,可能会导致ConcurrentModificationException异常。而ConcurrentHashMap的迭代器是强一致的,它能够正确地反映出迭代时刻的集合状态。
  4. null值和null键的支持:HashMap可以存储null值和null键,而ConcurrentHashMap不支持存储null值和null键。这是因为ConcurrentHashMap使用了特殊的占位符来表示空槽,不允许使用null值或null键。
  5. 扩容机制:HashMap在扩容时需要重新计算哈希值,并重新分配元素位置,期间可能会导致临时的不一致状态。而ConcurrentHashMap的扩容机制更加复杂,它可以同时进行多个扩容操作,不影响已有的读取操作,保证了高并发环境下的性能和线程安全。

你可能感兴趣的:(java,开发语言)