Java8之ConcurrentHashMap实现原理

在java8之前ConcurrentHashMap是使用分段锁来实现并发的,数据结构为hashmap(数组加链表)的基础上再套一层segment数组,锁加在segment元素上。java8实现了粒度更细的加锁,去掉了segment数组,直接使用synchronized锁住hash后得到的数组下标位置中的第一个元素 ,如下图,这样加锁比segment加锁能支持更高的并发量。

另外,在java8中,当链表内的node数量满足一定条件时,链表会变成红黑树,加快搜索速度。

先了解几个知识点:

  1. 默认数组大小为16
  2. 扩容因子为0.75,扩容后数组大小翻倍
  3. 当存储的node总数量 >= 数组长度*扩容因子时,会进行扩容(数组中的元素、链表元素、红黑树元素都是内部类Node的实例或子类实例,这里的node总数量是指所有put进map的node数量)
  4. 当链表长度>=8且数组长度<64时会进行扩容
  5. 当数组下是链表时,在扩容的时候会从链表的尾部开始rehash
  6. 当链表长度>=8且数组长度>=64时链表会变成红黑树
  7. 树节点减少直至为空时会将对应的数组下标置空,下次存储操作再定位在这个下标t时会按照链表存储
  8. 扩容时树节点数量<=6时会变成链表
  9. 当一个事物操作发现map正在扩容时,会帮助扩容
  10. map正在扩容时获取(get等类似操作)操作还没进行扩容的下标会从原来的table获取,扩容完毕的下标会从新的table中获取

总结一下主要方法的实现原理

  1. put方法会首先检查table有没有初始化,没有则初始化table,定位到下标后如果下标内是代表rehash的特殊node,则会帮助扩容,否则进行更新或插入。如果链表数量达到条件则会变为红黑树。在最后增加map的总node数量时,若总数超过table长度的0.75倍则会进行扩容,扩容时会将下标倒序分为几块任务,可由其余线程帮助完成扩容
  2. get时会根据key定位到下标,然后遍历链表或数组查找对应的node,如果下标内是代表rehash的特殊node,则会去临时扩容table内查询数据
  3. remove时会根据下标遍历下标内的链表或者红黑树,如果下标内是代表rehash的特殊node,则会帮组扩容

源码分析 

一.属性介绍

  1. table为Node数组,加锁时锁住的就是table下标内的node
  2. nextTable为扩容时存储扩容后node的数组
  3. baseCount为存储的总node数
  4. sizeCtl在不扩容时值为数组长度*扩容因子,扩容和初始化table时为负数
  5. transferIndex不扩容时为0,扩容时代表扩容的边界,扩容从数组最后一个下标倒序开始,transferIndex被赋值为数组长度,根据步长stride(根据核数计算出的,最小值为16)将扩容工作分段,将transferIndex更新transferIndex-stride,假设n为32,stride为16,则扩容任务分为下32-16、16-0两块,可由其他事物线程帮助完成。

二. 构造方法介绍

常用构造器如下

/**
     * Creates a new, empty map with the default initial table size (16).
     */
    public ConcurrentHashMap() {
    }

    /**
     * Creates a new, empty map with an initial table size
     * accommodating the specified number of elements without the need
     * to dynamically resize.
     *
     * @param initialCapacity The implementation performs internal
     * sizing to accommodate this many elements.
     * @throws IllegalArgumentException if the initial capacity of
     * elements is negative
     */
    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;
    }

    /**
     * Creates a new map with the same mappings as the given map.
     *
     * @param m the map
     */
    public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
        this.sizeCtl = DEFAULT_CAPACITY;
        putAll(m);
    }
  1. 默认构造器不做任何事情
  2. 入参有容量的构造器会将sizeCtl设置为大于容量的最小的2^n ,并不会初始化table
  3. 入参有map的会将sizeCtl设置为16,然后将map中的元素循环插入concurrenthashmap中,达到扩容条件会扩容
  4. 此外还有兼容老版本的带负载因子的构造方法,也都是只给sizeCtl赋值,不初始化table

三.常用方法(get、put、remove)

1.put

public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        //右移16位高位有效计算出的hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //初次put时,因为concurrenthashmap在构造时没有初始化table,所以先初始化table
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
                //n为数组长度,低位有效二次hash后计算出数组的下标,并获取该下标的node
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //如果该下标内没有node,说明此处没有hash冲突,直接将put的k-v构造为node填入此下标位置,跳出循环
                if (casTabAt(tab, i, null,
                        new Node<K,V>(hash, key, value, null)))
                    break;
            }
            else if ((fh = f.hash) == MOVED)
                //如果下表内有node,且该node的hash值为moved(即-1),说明当前concurrenthashmap正在扩容,
                //那么本次操作将会帮助扩容
                tab = helpTransfer(tab, f);
            else {
                //node存在,且该下标位置不是扩容状态
                // (扩容是一个下标一个下标进行的,此时说明当前concurrenthashmap正在扩容可能也在扩容,
                // 但还没处理到该下标,该下标可以进行put操作,并且该线程不帮助扩容)
                V oldVal = null;
                //锁住该node(该node就是该坐标位置的第一个node)
                synchronized (f) {
                    //再次确认下加锁前该node有没有变化
                    if (tabAt(tab, i) == f) {
                        //fh为该node的hash值,如果hash值为正数,说明该下标是按链表存储的
                        if (fh >= 0) {
                            binCount = 1;
                
  • 10
    点赞
  • 49
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
Java 8中的ConcurrentHashMap是线程安全的哈希表实现,它支持并发读写操作而不需要全局锁定。它的实现原理主要有以下几个方面: 1. 分段锁策略:ConcurrentHashMap将整个数据结构分成若干个Segment(段),每个Segment维护着一个独立的散列桶数组。每个Segment内部都有一个可重入锁,不同的线程可以同时访问不同的Segment,从而实现并发读写的能力。 2. 散列桶数组:ConcurrentHashMap使用散列桶数组来存储键值对。每个散列桶上都有一个链表,用于解决哈希冲突。当多个键映射到同一个散列桶时,它们会被链接到同一个链表上。 3. CAS操作:ConcurrentHashMap使用CAS(Compare and Swap)操作来保证并发更新操作的原子性。CAS是一种无锁算法,它通过比较内存中的值与期望值是否相等来确定是否更新。如果相等,则执行更新操作;否则,重新尝试。 4. 扩容机制:ConcurrentHashMap在插入新元素时,如果当前Segment的负载因子(即链表长度)超过阈值,则会触发扩容操作。扩容时,会创建一个新的散列桶数组,并将原来的键值对重新分配到新的散列桶中。这个过程可以通过加锁来保证线程安全。 总的来说,ConcurrentHashMap通过分段锁策略、散列桶数组、CAS操作和扩容机制来实现线程安全的并发读写操作。它在多线程环境下能够提供较好的性能和可伸缩性。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值