源码分析ConcurrentHashMap如何保证线程安全

 更新时间:2023年06月28日 10:33:51   作者:小威要向诸佬学习呀  
这篇文章将结合底层源码为大家详细介绍一下ConcurrentHashMap是如何保证线程安全的,文中是示例代码讲解详细,感兴趣的小伙伴可以了解一下

JDK1.7保证线程安全

ConcurrentHashMap在JDK 1.7和JDK 1.8版本保证线程安全及其底层数据结构是不一样的,这一块是面试中的重点,接下来详细介绍一下它们。

在JDK 1.7中,ConcurrentHashMap采用了分段锁(Segment)的设计来保证线程安全。下面我们将通过详细解读其底层源码,来介绍其线程安全实现原理。

ConcurrentHashMap的主要类是Segment。每个Segment是一个独立的锁,并且维护着一个HashEntry数组。HashEntry是链表节点,存储了键值对。

首先,我们来看一下ConcurrentHashMap的基本数据结构:

static final class HashEntry<K, V> {
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K, V> next;
    HashEntry(int hash, K key, V value, HashEntry<K, V> next) {
        this.hash = hash;
        this.key = key;
        this.value = value;
        this.next = next;
    }
}
static final class Segment<K, V> extends ReentrantLock implements Serializable {
    static final float LOAD_FACTOR = 0.75f;
    transient volatile HashEntry<K, V>[] table;
    transient int count;
    transient int modCount;
    transient int threshold;
    final float loadFactor;
}

每个Segment都是一个继承自ReentrantLock的可重入锁,具备独立的线程安全性。table是Segment内部的HashEntry数组,用于存储键值对。count表示当前Segment中的元素数量,modCount用于记录修改次数,threshold表示扩容的阈值,loadFactor表示加载因子。

接下来,我们看一下ConcurrentHashMap的put操作:

public V put(K key, V value) {
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key.hashCode());
    int segmentIndex = getSegmentIndex(hash);
    return segments[segmentIndex].put(key, hash, value, false);
}
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    lock(); // 获取当前Segment的锁
    try {
        int c = count;
        if (c++ > threshold) // 判断是否需要扩容
            rehash();
        HashEntry<K, V>[] tab = table;
        int index = hash & (tab.length - 1);
        HashEntry<K, V> first = tab[index];
        HashEntry<K, V> e = first;
        while (e != null && (e.hash != hash || !key.equals(e.key))) 
            e = e.next;
        V oldValue;
        if (e != null) { // 键存在,更新值
            oldValue = e.value;
            if (!onlyIfAbsent)
                e.value = value;
        } else { // 键不存在,创建新节点并添加到链表头部
            oldValue = null;
            ++modCount;
            tab[index] = new HashEntry<K, V>(hash, key, value, first);
            count = c; // 更新元素数量
        }
        return oldValue;
    } finally {
        unlock(); // 释放当前Segment的锁
    }
}

在put操作中,首先通过hash函数计算键的散列值hash,然后根据散列值获取对应的Segment。接着,通过Segment的锁保证了当前操作的线程安全。

在获取到Segment的锁之后,首先判断当前Segment中的元素数量count是否超过了阈值threshold,如果超过了则进行扩容。然后通过散列值和数组长度计算出键对应的索引位置index,并从对应的链表开始遍历,寻找是否存在相同的键。

如果找到了相同的键,则更新对应的值;如果没有找到相同的键,则创建一个新的HashEntry节点,并将其添加到链表的头部。

在完成操作后,释放Segment的锁。

通过分段锁的设计,JDK 1.7的ConcurrentHashMap允许多个线程同时操作不同的Segment,从而提高了并发性能。虽然在高并发情况下仍可能存在竞争问题,但通过细粒度的锁设计,可以减少锁竞争的概率,提升整体性能。

JDK1.8保证线程安全

在JDK 1.8中,ConcurrentHashMap进行了重大改进,采用了更加高效的并发控制机制来保证线程安全。相较于JDK 1.7的分段锁设计,JDK 1.8引入了基于CAS(Compare and Swap)操作和链表/红黑树结构的锁机制以及其他优化,大大提高了并发性能。

底层数据结构:

JDK 1.8中的ConcurrentHashMap采用了数组+链表/红黑树的结构。具体来说,它将整个哈希桶(Hash Bucket)划分为若干个节点(Node)。每个节点代表一个存储键值对的单元,可以是链表节点(普通节点)或红黑树节点(树节点),这取决于节点内的键值对数量是否达到阈值。使用红黑树结构可以提高查找、插入、删除等操作的效率。

主要类和数据结构如下:

static final class Node<K, V> implements Map.Entry<K, V> {
    final int hash;
    final K key;
    volatile V value;
    volatile 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;
    }
}
static final class TreeNode<K, V> extends Node<K, V> {
    TreeNode(int hash, K key, V value, Node<K, V> next) {
        super(hash, key, value, next);
    }
    // 省略了红黑树相关的操作代码
}
static final class ConcurrentHashMap<K, V> {
    transient volatile Node<K, V>[] table;
    transient volatile int sizeCtl;
    transient volatile int baseCount;
    transient volatile int modCount;
}

ConcurrentHashMap的线程安全实现原理:

初始状态:在初始状态下,table为null,sizeCtl为0。当第一个元素被插入时,会根据并发级别(Concurrency Level)计算出数组的长度,并使用CAS操作将数组初始化为对应长度的桶。

插入操作

put方法:当进行插入操作时,ConcurrentHashMap首先计算键的散列值,然后根据散列值和数组长度计算出对应的桶位置。接着使用CAS操作尝试插入新节点,如果成功则插入完成;如果失败,则进入下一步。

resize方法:插入节点时,若发现链表中的节点数量已经达到阈值(默认为8),则将链表转化为红黑树,提高查找、插入、删除等操作的效率。在转化过程中,利用synchronized锁住链表或红黑树所在的桶,并进行相应的操作。

forwardTable方法:若节点数量超过阈值(默认为64)且table未被初始化,则使用CAS操作将table指向扩容后的桶数组,并根据需要将链表或红黑树进行分割,以减小线程之间的冲突。

查询操作

get方法:当进行查询操作时,首先计算键的散列值,然后根据散列值和数组长度计算出对应的桶位置。接着从桶位置的链表或红黑树中查找对应的节点。

其他操作

remove方法:当进行删除操作时,首先计算键的散列值,然后根据散列值和数组长度计算出对应的桶位置。接着使用synchronized锁住桶,并进行相应的操作。

综上所述,JDK 1.8的ConcurrentHashMap通过CAS操作、锁机制(synchronized)以及链表/红黑树结构来保证线程安全。CAS操作用于插入新节点和初始化桶数组,锁机制用于链表/红黑树的转化和删除操作,链表/红黑树结构用于提高查找、插入、删除操作的效率。这些优化措施使得ConcurrentHashMap在高并发环境下具有较好的性能表现。

JDK1.7和JDK1.8对比总结

在JDK 1.7和JDK 1.8中,ConcurrentHashMap有以下主要区别:

JDK 1.7中的实现方式:

  • JDK 1.7中的ConcurrentHashMap使用分段锁(Segment Locking)的设计。它将整个哈希表分成多个段(Segment),每个段都有自己的锁。这样可以降低并发操作时锁的争用范围,提高并发性能。
  • 每个段中包含一个HashEntry数组,每个HashEntry是一个链表结构,用于解决哈希冲突。
  • 由于每个段都有自己的锁,不同的线程可以同时访问不同的段,从而提高了并发度。

JDK 1.8中的改进:

JDK 1.8中的ConcurrentHashMap采用了CAS操作、锁机制以及链表/红黑树结构的改进。

  • 数据结构改进:JDK 1.8中使用数组+链表/红黑树的结构,代替了JDK 1.7中的段+链表结构。数组用于存储桶,链表/红黑树用于解决哈希冲突。
  • CAS操作:JDK 1.8使用CAS(Compare and Swap)操作来插入新节点和初始化桶数组。CAS操作是一种乐观锁机制,通过原子操作比较并交换的方式进行,并发安全性更好。
  • 锁的改进:JDK 1.8中引入了基于CAS操作和链表/红黑树结构的锁机制。对于链表/红黑树上的操作,使用synchronized锁住桶,以保证操作的原子性。
  • 链表转化为红黑树:JDK 1.8在插入操作时,当链表中的节点数量达到一定阈值时,会将链表转化为红黑树,提高查找、插入、删除等操作的效率。
  • resize操作的改进:JDK 1.8中的resize操作(扩容)采用了分割链表/红黑树的方式,减小了线程冲突的概率。

总的来说,JDK 1.8中的ConcurrentHashMap在数据结构、CAS操作、锁机制和链表/红黑树结构等方面进行了改进,相较于JDK 1.7,性能更好且并发度更高。这些改进使得JDK 1.8中的ConcurrentHashMap在高并发环境下表现更优秀。

到此这篇关于源码分析ConcurrentHashMap如何保证线程安全的文章就介绍到这了,更多相关ConcurrentHashMap保证线程安全内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • Java连接Vmware中的redis

    Java连接Vmware中的redis

    这篇文章主要为大家介绍了Java连接Vmware中的redis
    2016-01-01
  • IDEA创建Spring Boot Web项目完整图文教程

    IDEA创建Spring Boot Web项目完整图文教程

    在软件开发的浩瀚海洋中,SpringBoot以其独特的魅力和强大的功能,为开发者开辟了一条通往高效、便捷开发之路,这篇文章主要介绍了IDEA创建Spring Boot Web项目的相关资料,需要的朋友可以参考下
    2026-04-04
  • 浅谈Spring-boot事件监听

    浅谈Spring-boot事件监听

    这篇文章主要介绍了浅谈Spring-boot事件监听,小编觉得挺不错的,现在分享给大家,也给大家做个参考。一起跟随小编过来看看吧
    2017-09-09
  • java基础之方法详解

    java基础之方法详解

    这篇文章主要介绍了java基础之方法详解,文中有非常详细的代码示例,对正在学习java基础的小伙伴们有非常好的帮助,需要的朋友可以参考下
    2021-04-04
  • Java8 Stream中对集合数据进行快速匹配和赋值的代码示例

    Java8 Stream中对集合数据进行快速匹配和赋值的代码示例

    这篇文章主要介绍了Java8 Stream中如何对集合数据进行快速匹配和赋值,文中通过代码示例为大家介绍的非常详细,具有一定的参考价值,需要的朋友可以参考下
    2023-06-06
  • Rxjava+Retrofit+MVP实现购物车功能

    Rxjava+Retrofit+MVP实现购物车功能

    这篇文章主要为大家详细介绍了Rxjava+Retrofit+MVP实现购物车功能,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2018-05-05
  • 用Java生成二维码并附带文字信息

    用Java生成二维码并附带文字信息

    这篇文章主要介绍了用Java生成二维码并附带文字信息,文中有非常详细的代码示例,对正在学习java的小伙伴们有非常好的帮助,需要的朋友可以参考下
    2021-04-04
  • SpringBoot开发存储服务器实现过程详解

    SpringBoot开发存储服务器实现过程详解

    这篇文章主要为大家介绍了SpringBoot开发存储服务器实现过程详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2022-12-12
  • Java连接并操作Redis超详细教程

    Java连接并操作Redis超详细教程

    在分布式系统和高并发场景中,Redis 作为高性能内存数据库的地位举足轻重,对于 Java 开发者而言,掌握 Redis 的连接与操作是进阶必备技能,本文从 Java 操作 Redis 的核心需求出发,通过完整代码示例与逐行解析,需要的朋友可以参考下
    2025-05-05
  • Java并发编程this逃逸问题总结

    Java并发编程this逃逸问题总结

    本篇文章给大家详细分析了Java并发编程this逃逸的问题分享,对此有需要的朋友参考下。
    2018-02-02

最新评论