Java进阶篇--并发容器之ConcurrentHashMap

目录

ConcurrentHashMap简介

关键属性及类

ConcurrentHashMap的关键属性

ConcurrentHashMap的关键内部类

CAS关键操作

重点方法

实例构造器方法

initTable方法

put方法

get方法

transfer方法

与size相关的一些方法

总结


ConcurrentHashMap简介

ConcurrentHashMap是Java中的一个线程安全的哈希表实现,它采用了锁分段的思想提高并发度,适用于多线程环境下进行哈希表操作。相比于普通的HashMap,在多线程环境下,ConcurrentHashMap可以提高并发性能和吞吐量。

ConcurrentHashMap基于分段锁技术实现。具体来说,ConcurrentHashMap将哈希表分为多个段(Segment),每个段都是一个独立的哈希表,且对于每个段的操作都是线程安全的。当多个线程并发访问不同的段时,它们之间是互不干扰的,因此可以实现真正的同时读写操作,从而提高了并发性能。除此之外,ConcurrentHashMap还提供了一些线程安全的方法,如putIfAbsent()、replace()、remove()等,在多线程环境下使用这些方法可以保证操作的原子性。

在JDK1.6版本中,ConcurrentHashMap使用了segment继承了ReentrantLock充当锁的角色,为每一个segment提供了线程安全的保障;segment维护了哈希散列表的若干个桶,每个桶由HashEntry构成的链表。

而到了JDK1.8版本,ConcurrentHashMap底层数据结构改变为采用数组+链表+红黑树的数据形式,并且使用了synchronized以及CAS无锁操作来保证线程安全性。

总之,如果需要在多线程环境下进行哈希表操作,ConcurrentHashMap是一个很好的选择。

关键属性及类

ConcurrentHashMap的关键属性

1、table:装载Node的数组,作为ConcurrentHashMap的数据容器。它采用懒加载方式,在第一次插入数据时进行初始化操作,并且数组的大小总是2的幂次方。

2、nextTable:在扩容时使用的数组。平时为null,只有在扩容时才会被赋值为非null。

3、sizeCtl:用来控制table数组的大小。根据是否初始化和是否正在扩容有几种情况:

  1. 当值为负数时:如果为-1表示正在初始化,如果为-N则表示当前正有N-1个线程进行扩容操作;
  2. 当值为正数时:如果当前数组为null,则表示table在初始化过程中,sizeCtl表示需要新建数组的长度;
  3. 若已经初始化,表示当前数据容器(table数组)的可用容量,也可以理解为临界值(即插入节点数超过该临界值就需要扩容)。具体指的是数组的长度n乘以加载因子loadFactor;
  4. 当值为0时,即数组长度为默认初始值。

4、sun.misc.Unsafe U:在ConcurrentHashMap的实现中,通过大量使用U.compareAndSwapXXXX方法来修改ConcurrentHashMap的一些属性。这些方法利用了CAS算法来保证线程安全性。CAS(Compare and Swap)是一种乐观策略,假设每一次操作都不会产生冲突,只有在冲突发生时才进行重试。CAS操作依赖于现代处理器指令集,通过底层的CMPXCHG指令来实现。

CAS(Compare and Swap)操作的核心思想就是通过比较当前变量的实际值V与期望的旧值O是否相同来判断是否有其他线程对该变量进行了修改。如果当前变量的实际值V与期望的旧值O相同,说明该变量没有被其他线程修改过,此时可以安全地将新值N赋给该变量。CAS操作会尝试将新值N赋给变量,并返回操作是否成功的结果。操作成功意味着变量的值被成功更新。但如果当前变量的实际值V与期望的旧值O不相同,说明该变量已经被其他线程修改过,此时将新值N赋给该变量操作就是不安全的,可能会导致数据不一致。在这种情况下,需要进行重试,重新比较当前变量的实际值和期望的旧值,直到成功为止。

在ConcurrentHashMap中,CAS操作是通过sun.misc.Unsafe类提供的方法实现的,该类可以直接操控内存和线程底层操作,可以理解为java中的“指针”。该成员变量的获取是在静态代码块中,具体来说,在Unsafe类中有一个私有的静态成员变量theUnsafe,用于持有Unsafe实例。在类加载时,静态代码块会执行,并通过反射来获取Unsafe实例并赋值给theUnsafe变量。

private static final sun.misc.Unsafe theUnsafe;

static {
    try {
        // 使用反射获取Unsafe实例
        java.lang.reflect.Field unsafeField = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
        unsafeField.setAccessible(true);
        theUnsafe = (sun.misc.Unsafe) unsafeField.get(null);
    } catch (Exception e) {
        throw new RuntimeException("无法获取不安全实例", e);
    }
}

通过这种方式,可以绕过了Unsafe类的限制,获取到了Unsafe实例,从而可以使用其中的方法进行底层的内存和线程操作。

需要注意的是,由于sun.misc.Unsafe是Java平台的内部API,不建议直接在生产代码中使用它,因为它的不稳定性和不可移植性。

总结:这些属性在ConcurrentHashMap的实现中起着重要的作用,确保了线程安全性和并发操作的正确性。

ConcurrentHashMap的关键内部类

1、Node类用于存储ConcurrentHashMap中的键值对,并具有next域链接到下一个节点。它实现了Map.Entry接口,包含hash、key、val以及next等属性。为了保证内存可见性,这些属性都使用volatile修饰。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;

    Node(int hash, K key, V val, Node<K,V> next) {
        this.hash = hash;
        this.key = key;
        this.val = val;
        this.next = next;
    }

    // 实现Map.Entry接口
    public K getKey() {
        return key;
    }

    public V getValue() {
        return val;
    }

    public V setValue(V value) {
        V oldValue = val;
        val = value;
        return oldValue;
    }
}

2、TreeNode类继承自Node类,用于在ConcurrentHashMap的红黑树中存储节点。它包含了红黑树中的节点结构,如parent、left、right、prev和red等属性。

static final class TreeNode<K,V> extends Node<K,V> {
    TreeNode<K,V> parent;  // 红黑树连接
    TreeNode<K,V> left;
    TreeNode<K,V> right;
    TreeNode<K,V> prev;    // 删除时需要解除下一个引用
    boolean red;

    TreeNode(int hash, K key, V val, Node<K,V> next) {
        super(hash, key, val, next);
    }

    // ...
}

3、TreeBin类是一个中间类,用于包装多个TreeNode节点。实际上,ConcurrentHashMap的数组中存储的是TreeBin对象,而不是TreeNode对象。TreeBin包含了红黑树的根节点root,以及first、waiter和lockState等属性,用于管理并发访问。

static final class TreeBin<K,V> extends Node<K,V> {
    TreeNode<K,V> root;                 // 红黑树的根节点
    volatile TreeNode<K,V> first;       // 链表的第一个节点
    volatile Thread waiter;             // 当前等待锁的线程
    volatile int lockState;             // 锁的状态

    // 锁的状态常量
    static final int WRITER = 1;        // 写锁
    static final int WAITER = 2;        // 等待写锁
    static final int READER = 4;        // 读锁

    // ...
}

4、ForwardingNode类是一个特殊节点,在扩容时会出现。它的key、value和hash属性都为null,并且具有一个指向新表格(nextTable)的引用。

static final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable;        // 指向下一个表

    ForwardingNode(Node<K,V>[] tab) {
        super(MOVED, null, null, null);
        this.nextTable = tab;
    }

    // ...
}

总结:这些内部类协同工作,使得ConcurrentHashMap能够实现高效并发的键值对存储和访问操作。

CAS关键操作

在ConcurrentHashMap中,CAS操作常用于以下三个方法:

1、tabAt方法用来获取table数组中指定索引位置的节点元素,其定义如下:

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

它使用了Java的Unsafe类的getObjectVolatile方法读取指定内存地址的值,实现了与get()方法类似的功能。需要注意的是,在ConcurrentHashMap中,Node类型的元素是作为数组的元素存储的,因此需要使用位运算来计算内存地址。
2、casTabAt方法用来利用CAS操作设置table数组中指定索引位置的节点元素,其定义如下:

static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

它使用了Java的Unsafe类的compareAndSwapObject方法执行CAS操作。该方法接受四个参数:操作目标数组,目标数组元素的偏移量,期望值和新值。如果目标数组中指定位置的元素与期望值相等,则将其替换为新值,并返回true表示操作成功;否则不做任何修改,并返回false表示操作失败。
3、setTabAt方法用来设置table数组中指定索引位置的节点元素,它与casTabAt方法不同的是,它不使用CAS操作,而是直接覆盖原有元素,其定义如下:

static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

它使用了Java的Unsafe类的putObjectVolatile方法写入指定内存地址的值,实现了与set()方法类似的功能。需要注意的是,由于该方法直接覆盖原有元素,因此可能会导致数据丢失,应谨慎使用。

重点方法

实例构造器方法

在使用 ConcurrentHashMap 时,可以通过调用不同的构造器方法来创建对象。ConcurrentHashMap 提供了如下几个常用的构造器方法:

  1. ConcurrentHashMap():无参构造器方法,创建一个初始容量为16的 ConcurrentHashMap 对象,这个对象在创建时,其 table 数组还未初始化,只有在第一次插入数据时才会初始化。
  2. ConcurrentHashMap(int initialCapacity):带初始容量参数的构造器方法,创建一个指定初始容量的 ConcurrentHashMap 对象。
  3. ConcurrentHashMap(Map<? extends K, ? extends V> m):带 Map 参数的构造器方法,创建一个包含给定 Map 中所有映射的 ConcurrentHashMap 对象。
  4. ConcurrentHashMap(int initialCapacity, float loadFactor):带初始容量和加载因子参数的构造器方法,创建一个指定初始容量和加载因子的 ConcurrentHashMap 对象。
  5. ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel):带初始容量、加载因子和并发级别参数的构造器方法,创建一个指定初始容量、加载因子和并发级别的 ConcurrentHashMap 对象。

需要注意的是,这些参数的具体含义如下:

  • initialCapacity:初始容量,即 ConcurrentHashMap 在创建时的大小,默认为16。
  • loadFactor:加载因子,用于计算 ConcurrentHashMap 容量的阈值,默认为0.75。
  • concurrencyLevel:并发级别,表示可同时更新的线程数,默认为16。在并发修改时,使用分段锁机制来提高并发性能,将数据分成多个段,每个段都有自己的锁,不同的线程可以同时操作不同的段。

根据实际需求,选择合适的构造器方法创建 ConcurrentHashMap 对象,并传入相应的参数值即可。

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class main {
    public static void main(String[] args) {
        // 构造器方法1:无参构造器方法
        ConcurrentHashMap<String, Integer> map1 = new ConcurrentHashMap<>();

        // 构造器方法2:带初始容量参数的构造器方法
        ConcurrentHashMap<String, Integer> map2 = new ConcurrentHashMap<>(16);

        // 构造器方法3:带Map参数的构造器方法
        ConcurrentHashMap<String, Integer> originalMap = new ConcurrentHashMap<>();
        originalMap.put("key1", 1);
        originalMap.put("key2", 2);
        ConcurrentHashMap<String, Integer> map3 = new ConcurrentHashMap<>(originalMap);

        // 构造器方法4:带初始容量、加载因子和并发级别参数的构造器方法
        ConcurrentHashMap<String, Integer> map4 = new ConcurrentHashMap<>(16, 0.75f, 4);

        // 构造器方法5:带初始容量参数和并发级别参数的构造器方法
        ConcurrentHashMap<String, Integer> map5 = new ConcurrentHashMap<>(16, 4);

        ExecutorService executorService = Executors.newFixedThreadPool(4);

        // 创建五个线程,每个线程向不同的map中插入和移除元素
        executorService.execute(() -> {
            for (int i = 0; i < 10; i++) {
                map1.put("Thread1:" + i, i);
                map1.remove("Thread1:" + (i - 2));
            }
        });

        executorService.execute(() -> {
            for (int i = 0; i < 10; i++) {
                map2.put("Thread2:" + i, i);
                map2.remove("Thread2:" + (i - 2));
            }
        });

        executorService.execute(() -> {
            for (int i = 0; i < 10; i++) {
                map3.put("Thread3:" + i, i);
                map3.remove("Thread3:" + (i - 2));
            }
        });

        executorService.execute(() -> {
            for (int i = 0; i < 10; i++) {
                map4.put("Thread4:" + i, i);
                map4.remove("Thread4:" + (i - 2));
            }
        });

        executorService.execute(() -> {
            for (int i = 0; i < 10; i++) {
                map5.put("Thread5:" + i, i);
                map5.remove("Thread5:" + (i - 2));
            }
        });

        executorService.shutdown();

        while (!executorService.isTerminated()) {
            // 等待所有线程执行完毕
        }

        // 输出各个map中的元素
        System.out.println("map1: " + map1);
        System.out.println("map2: " + map2);
        System.out.println("map3: " + map3);
        System.out.println("map4: " + map4);
        System.out.println("map5: " + map5);
    }
}

该示例代码创建了五个不同配置的 ConcurrentHashMap 对象,并使用五个线程并发地向每个 ConcurrentHashMap 中插入和移除元素。每个线程都执行类似的操作,以测试 ConcurrentHashMap 的并发安全性和效果。最后,输出每个 ConcurrentHashMap 中的元素。

initTable方法

initTable方法是在ConcurrentHashMap第一次插入数据时候调用,它会根据sizeCtl的值(即ConcurrentHashMap的大小)来初始化table数组,table数组是ConcurrentHashMap的主要存储数据结构。

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; // 定义table数组
    int sc;
    while ((tab = table) == null || tab.length == 0) { // 如果table为null或长度为0,则进行初始化操作
        if ((sc = sizeCtl) < 0) { // 如果sizeCtl小于0,则让出当前线程的CPU时间片
            Thread.yield();
        } else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 如果成功获取到锁
            try {
                if ((tab = table) == null || tab.length == 0) { // 如果table仍然为null或长度为0,则进行初始化操作
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY; // 计算初始化长度
                    Node[] nt = new Node[n]; // 新建一个长度为n的Node数组
                    table = tab = nt; // 将新的table设置为当前的table
                    sc = n - (n >>> 2); // 更新sizeCtl
                }
            } finally {
                sizeCtl = sc; // 释放锁并更新sizeCtl
            }
            break;
        }
    }
    return tab; // 返回初始化后的table
}

这段代码实现的是ConcurrentHashMap的初始化方法,也就是当table为空或者长度为0时,初始化table。函数会返回初始化后的table。

put方法

put方法是向ConcurrentHashMap中插入一个键值对,它首先会对key进行hash计算得到一个桶的位置,在该桶的位置上加锁,然后查找当前桶中是否已经存在该key,如果存在则替换掉原有的value,否则将该键值对插入到桶中。如果桶的节点数超过了链表长度阈值(默认8),则会将该桶转换为红黑树,以提高查询性能。在插入完成后,如果节点数比sizeCtl的值大2倍,并且表还没有扩容,则会启动扩容操作。

public V put(K key, V value) {
    if (value == null) { // value为空时抛出异常
        throw new NullPointerException();
    }
    int hash = spread(key.hashCode()); // 计算hash值
    int binCount = 0; // 记录链表或红黑树中节点个数
    for (Node<K,V>[] tab = table;;) { // 循环查找table中是否包含key
        Node<K,V> f; // 当前桶的第一个节点
        int n, i, fh; // 当前table长度、当前桶的索引、当前桶的第一个节点的hash值
        if (tab == null || (n = tab.length) == 0) { // 如果table为null或长度为0,则进行初始化操作
            tab = initTable();
        } else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 如果当前桶的第一个节点为null,则尝试在该桶中添加新节点
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value, null))) { // 使用CAS操作更新当前桶的第一个节点,如果成功,则添加成功
                break;
            }
        } else if ((fh = f.hash) == MOVED) { // 如果当前桶的第一个节点的hash值为MOVED,表明当前桶正在进行扩容操作,需要帮助扩容
            tab = helpTransfer(tab, f);
        } else { // 如果当前桶的第一个节点既不为null也不为MOVED,则在链表或红黑树中查找key对应的节点
            V oldVal = null; // 记录原来的值
            synchronized (f) { // 对当前桶的第一个节点加锁
                if (tabAt(tab, i) == f) { // 再次检查当前桶的第一个节点是否发生了变化,防止其它线程修改了该节点并将该节点向当前桶的后面移动
                    if (fh >= 0) { // 如果当前桶的第一个节点为链表节点
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) { // 遍历链表
                            K ek;
                            if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { // 如果找到了key对应的节点
                                oldVal = e.value; // 记录原来的值
                                e.value = value; // 使用新值更新节点的value
                                break;  // 替换成功
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) { // 如果遍历到了链表的末尾,即没有找到key对应的节点
                                pred.next = new Node<K,V>(hash, key, value, null); // 在链表的末尾添加新节点
                                break;  // 插入成功
                            }
                        }
                    } else if (f instanceof TreeBin) { // 如果当前桶的第一个节点为红黑树节点
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) { // 在红黑树中查找key对应的节点,并使用新值替换旧值
                            oldVal = p.value; // 记录原来的值
                            p.value = value;
                        } else {
                            ++size; // 新增节点
                        }
                        break;
                    }
                }
            }
            if (binCount != 0) { // 如果在链表或红黑树中找到了key对应的节点,则根据节点个数调整链表或红黑树的结构
                if (binCount >= TREEIFY_THRESHOLD) { // 如果节点个数超过了阈值,需要将链表转化为红黑树
                    treeifyBin(tab, i);
                }
                if (oldVal != null) { // 找到了key对应的节点,需要返回旧值
                    return oldVal;
                }
                break;
            }
        }
    }
    addCount(1L, binCount); // 更新计数器并返回null
    return null;
}

这段代码实现的是ConcurrentHashMap的put方法,用于向ConcurrentHashMap中插入键值对。如果该键已存在,会将原来的值替换为新值并返回原来的值;如果该键不存在,插入新的键值对并返回null。 

get方法

get方法是从ConcurrentHashMap中根据key取值,它首先会对key进行hash计算得到一个桶的位置,然后查找当前桶中是否存在该key,如果不存在则返回null,否则返回该key对应的value。

public V get(Object key) {
    Node<K,V>[] tab; // 当前的table
    Node<K,V> e, p; // 用于遍历节点
    int n, eh; // table长度,当前节点的hash值
    K ek; // 当前节点的键
    int h = spread(key.hashCode()); // 计算键的哈希值
    if ((tab = table) != null && (n = tab.length) > 0 && (e = tabAt(tab, (n - 1) & h)) != null) { // 检查table是否为空,并且根据哈希值找到对应的桶
        if ((eh = e.hash) == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { // 如果第一个节点就是目标节点
            return e.value;  // 返回对应的值
        } else if (eh < 0) { // 如果第一个节点的hash值小于0,说明该桶使用了红黑树存储节点
            if ((p = e.find(h, key)) != null) { // 在红黑树中查找目标节点
                return p.value;  // 返回对应的值
            }
        } else { // 如果第一个节点不是目标节点,且不是红黑树节点,则在链表中遍历查找
            while ((e = e.next) != null) {
                if (e.hash == h && ((ek = e.key) == key || (ek != null && key.equals(ek)))) { // 如果找到了目标节点
                    return e.value;  // 返回对应的值
                }
            }
        }
    }
    return null;  // 没有找到目标节点,返回null
}

这段代码实现的是ConcurrentHashMap的get方法,用于根据给定的键获取对应的值。代码中会根据键的哈希值计算出对应的桶索引,并在该桶中查找键对应的节点。如果找到了节点,则返回其对应的值;如果没有找到节点,则返回null。

transfer方法

transfer方法是在ConcurrentHashMap扩容时的转移数据操作,它会将源桶中的所有节点重新分配到新的桶中,同时保持节点之间的相对顺序不变。在节点重新分配过程中,如果桶中的节点数量小于等于6个,则会将其转换为链表,否则转换为红黑树。在节点分配完成后,如果节点数比sizeCtl的值小于等于当前ConcurrentHashMap大小的16分之一,并且表还没有收缩,则会启动收缩操作。

private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride; // 当前table的长度和步长
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) { // 根据CPU数量计算步长,如果小于最小步长,则使用最小步长
        stride = MIN_TRANSFER_STRIDE;
    }
    if (nextTab == null) { // 如果nextTab为空,则创建一个容量为当前table两倍的新数组nextTab
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = new Node[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {
            sizeCtl = Integer.MAX_VALUE; // 创建新数组失败时,设置sizeCtl为最大值,表示扩容失败
            return;
        }
        nextTable = nextTab; // 将nextTab赋值给nextTable
        transferIndex = n; // 将transferIndex设置为当前table的长度,表示从头开始迁移节点
    }
    int nextn = nextTab.length; // nextTab的长度
    ForwardingNode<K,V> fwd = new ForwardingNode<>(nextTab); // 创建一个ForwardingNode节点,用于标记正在进行扩容的table
    boolean advance = true; // 是否继续进行扩容操作
    boolean finishing = false; // 是否是最后一次迁移操作
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; // 当前节点
        int fh; // 当前节点的哈希值
        while (advance) {
            int nextIndex, delta; // 下一个节点的索引和步长
            while (i >= bound) { // 如果当前节点超过了bound,则更新bound,继续下一轮迁移
                bound = (stride > 1) ? (bound + stride) : n;
                if (bound <= n) { // bound小于等于n时,根据步长计算下一个i的值
                    i = bound - stride;
                } else {
                    i = n;
                    advance = false; // 当i超过n时,停止迁移操作
                    break;
                }
            }
            if (!advance) {
                break;
            }
            if ((nextIndex = (delta = ((n + nextn) << 1) / n) * (i & (n - 1))) >= nextn || nextIndex < 0) {
                throw new Error("Illegal forward index: " + nextIndex); // 检查计算得到的nextIndex是否越界
            }
            if (i + delta > bound) { // 如果计算得到的下一个bound超过了n,则将bound设置为n,并将delta调整为bound和i之间的距离
                bound = n;
                delta = bound - i;
            }
            int boundIndex = i + delta; // 计算boundIndex,表示下一轮迁移的边界
            Object a;
            while (i < boundIndex && (a = tabAt(tab, i)) == null) { // 在当前位置找到非空节点前的第一个节点
                if (casTabAt(tab, i, null, fwd)) { // 将当前位置设置为fwd节点,标记为正在进行迁移操作
                    setTabAt(nextTab, nextIndex + (i & (n - 1)), a); // 在nextTab中设置对应位置的节点为a
                    break;
                }
                ++i;
            }
            if (i >= boundIndex) { // 如果i超过了boundIndex,说明该范围内的节点都已经迁移完成,继续下一轮迁移
                break;
            }
            if ((f = (Node<K,V>)a).hash < 0) { // 如果当前节点的哈希值小于0,说明已经被扩容过,直接跳过
                if (f instanceof ForwardingNode) { // 如果当前节点是ForwardingNode节点,说明正在进行扩容
                    tab = ((ForwardingNode<K,V>)f).nextTable; // 获取下一个table,继续扩容
                    advance = true;
                    break;
                }
                continue; // 已经被扩容了,继续下一次循环
            }
            setTabAt(nextTab, nextIndex + (i & (n - 1)), f); // 将节点复制到nextTab中对应的位置
            ++i;
        }
        if (!advance) { // advance为false时,说明当前轮迁移完成,停止迁移操作
            break;
        }
        finishing = casFinishing(false, true); // 将finishing状态CAS为true,表示最后一次迁移操作
    }
    if (finishing) { // 如果是最后一次迁移操作
        recheckResize(); // 重新检查是否需要进行扩容
    }
}

这段代码实现了ConcurrentHashMap的扩容操作。在并发环境下,当table中的节点数量达到一定阈值时,需要对table进行扩容,以提高并发性能。

在扩容过程中,首先计算扩容的步长(stride),然后创建一个新的数组nextTab作为扩容后的table。如果创建新数组失败,将sizeCtl设置为最大值,表示扩容失败。

接下来,通过循环迁移节点的方式,将原table中的节点复制到新的nextTab中。每次迁移的范围由步长控制,遍历当前范围内的节点,将其复制到nextTab的对应位置上。

在迁移过程中,会遇到一些特殊情况,如节点已经被扩容过、当前节点是ForwardingNode节点等,需要作出相应的处理。

最后,如果是最后一次迁移操作,将finishing状态设置为true,并重新检查是否需要进行扩容。

与size相关的一些方法

ConcurrentHashMap中的size()方法用于返回Map中的映射数量,即键值对的数量。由于ConcurrentHashMap可以包含更多的映射数,而无法以int表示,因此返回的是一个估计值。由于ConcurrentHashMap是一个并发安全的数据结构,多个线程可以同时进行插入和删除操作,因此在统计元素个数时,不可能在"stop the world"的情况下让其他线程都停下来。

为了解决这个问题,并发地统计元素数量并作出扩容决策,ConcurrentHashMap引入了mappingCount字段和addCount()方法。

1、mappingCount:它是一个用来统计元素数量的字段,并不一定保证准确性,但会尽可能反映出当前元素的数量。该方法与size()相似,但返回的是一个long类型的值。

//size()方法
public int size() {
    long n = sumCount();
    return ((n < 0L) ? 0 :
            (n > (long)Integer.MAX_VALUE) ? Integer.MAX_VALUE :
            (int)n);
}
//上述代码实现了size()方法,它首先调用sumCount()方法获取映射数量,并将得到的结果转换为int类型返//回。具体转换逻辑如下:
//1、如果n小于0,则返回0。
//2、如果n大于Integer.MAX_VALUE,则返回Integer.MAX_VALUE。
//3、否则,将n转换为int类型返回。
//需要注意的是,由于存在并发插入或删除操作,所以返回的值是一个估计值,并不一定完全准确。


//mappingCount()方法
public long mappingCount() {
    long n = sumCount();
    return (n < 0L) ? 0L : n; // 忽略临时的负值
}
//另外,ConcurrentHashMap还提供了mappingCount()方法来返回映射数量。该方法与size()相似,但返回的是一个long类型的值。
//mappingCount()方法内部同样调用了sumCount()方法获取映射数量,然后直接返回该值。如果n小于0,则返回0。这里忽略了临时的负值,因为在计算映射数量时可能存在并发修改的情况。

2、addCount()方法:这个方法用于更新baseCount的值,并检查是否需要进行扩容。baseCount是通过累加mappingCount得到的一个近似值,用于快速估算元素数量。当调用addCount()方法时,会对baseCount进行更新,同时检查是否达到扩容的条件。

private final void addCount(long x, int check) {
    CounterCell[] as; long b, s;
    // 尝试使用CAS方法更新baseCount的值 
    if ((as = counterCells) != null ||
        !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
        CounterCell a; long v; int m;
        boolean uncontended = true;
        // 如果counterCells为null或者长度小于0,或者无法获取到CounterCell,
        // 或者无法使用CAS方法更新CounterCell的值,则调用fullAddCount()方法完成添加计数值的操作。
        if (as == null || (m = as.length - 1) < 0 ||
            (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
            !(uncontended =
              U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();
    }
    // 当check值大于等于0时,表示需要检查是否需要进行扩容操作。
    if (check >= 0) {
        Node<K,V>[] tab, nt; int n, sc;
        while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
               (n = tab.length) < MAXIMUM_CAPACITY) {
            int rs = resizeStamp(n);
            //
            if (sc < 0) {
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                    transferIndex <= 0)
                    break;
                 // 如果已经有其他线程在执行扩容操作,
                 // 则等待其他线程完成扩容操作;否则,当前线程尝试发起扩容操作。
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                    transfer(tab, nt);
            }
            // 当前线程是唯一的或是第一个发起扩容的线程,此时nextTable=null。
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                         (rs << RESIZE_STAMP_SHIFT) + 2))
                transfer(tab, null);
            s = sumCount();
        }
    }
}

这是ConcurrentHashMap中的addCount()方法的源码。它用于将计数值添加到Map的大小中,并在需要时触发扩容操作。

在该方法中,首先尝试使用CAS方法更新baseCount的值。如果成功,直接返回。否则,尝试通过CounterCell来更新计数值。如果还是失败了,就调用fullAddCount()方法来完成添加计数值的操作。

当check参数大于等于0时,表示需要检查是否需要进行扩容操作。如果Map的大小已经超过了sizeCtl的值(初始值为DEFAULT_CAPACITY),并且表格还没有达到最大容量(MAXIMUM_CAPACITY),就会尝试进行扩容。在实现中,会判断当前是否有其他线程正在执行扩容操作。如果是,则先等待其他线程完成扩容操作;否则,当前线程就会尝试发起扩容操作。如果当前线程是第一个发起扩容操作的线程,则会将sizeCtl的值设为新的扩容戳记和2(表示当前线程正在扩容)的组合;如果已经有其他线程正在进行扩容操作,则会将sizeCtl的值加1,表示等待其他线程完成扩容操作。

需要注意的是,addCount()方法是ConcurrentHashMap实现中的内部方法,不建议直接修改或调用该方法。通常情况下,我们只需要使用ConcurrentHashMap提供的公共方法来操作Map,而不需要关心其内部实现。

注意:ConcurrentHashMap类的addCount()方法只在Java 8及以后的版本中支持。如果你使用的是较旧的Java版本,可以使用putIfAbsent()方法实现相同的功能。

import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.LongAdder;

public class main {

    public static void main(String[] args) {
        // 创建一个线程安全的ConcurrentHashMap对象
        ConcurrentHashMap<String, LongAdder> map = new ConcurrentHashMap<>();

        // 添加一些数据到Map中
        map.put("Apple", new LongAdder());
        map.put("Banana", new LongAdder());
        map.put("Orange", new LongAdder());

        // 输出初始的size值
        System.out.println("初始的size值:" + map.size());

        // 使用putIfAbsent()方法将新的键值对添加到Map中,并更新size的值
        String key = "Grapes";
        LongAdder value = new LongAdder();
        value.increment(); // 假设value需要自增1
        map.putIfAbsent(key, value);

        // 输出添加后的size值
        System.out.println("添加后的size值:" + map.size());

        // 输出添加后的键值对
        LongAdder result = map.get(key);
        if (result != null) {
            System.out.println("添加后的键值对:" + result.intValue());
        } else {
            System.out.println("键值对不存在");
        }
    }
}

总之,ConcurrentHashMap通过mappingCount和addCount()方法来估计元素的数量,并在适当的时机进行扩容。这种设计可以在一定程度上提高并发性能,但也导致了size()方法返回的结果可能不是完全准确的。如果需要获取准确的元素个数,可以考虑使用加锁等方式来保证操作的原子性。

总结

总结起来,JDK6和JDK7中的ConcurrentHashMap使用了Segment来减小锁粒度,并且在put操作时需要锁住Segment,而get操作不需要加锁。它使用volatile关键字来确保可见性,并且在统计全局映射数量时会多次尝试计算modcount来判断是否有其他线程进行了修改操作,如果没有,则直接返回size。如果有,则需要依次锁住所有的Segment来计算。

而JDK8之后的ConcurrentHashMap摒弃了Segment的设计,直接针对Node数组中的每一个桶进行处理,减小了锁粒度。为了防止链表过长导致性能下降,当链表长度超过8时,会转换为红黑树。

主要的设计变化包括:

  1. 不再使用Segment,而是使用Node来减小锁粒度。
  2. 引入MOVED状态,在resize过程中,其他线程仍然可以进行put操作而不会被阻塞。
  3. 使用3个CAS操作来保证Node的一些操作的原子性,取代了锁的使用。
  4. sizeCtl的不同取值代表不同的含义,起到控制作用。
  5. 使用synchronized关键字而不是ReentrantLock。

这些改进使得新版的ConcurrentHashMap在并发场景下具有更好的性能和扩展性。

注意:更多关于1.7版本与1.8版本的ConcurrentHashMap的实现对比,可以参考这篇文章

  • 3
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

正在奋斗的程序猿

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

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

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

打赏作者

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

抵扣说明:

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

余额充值