concurrenthashmap inittable_四千字从源码分析ConcurrentHashMap的底层原理(JDK1.8)

本文深入解析了ConcurrentHashMap的工作原理,包括其实现线程安全的机制、数据结构、初始化及扩容过程等。并介绍了如何使用ConcurrentHashMap进行高效并发操作。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

Map中用到最多的是HashMap,有关HashMap的介绍和底层源码的分析可以看我之前的文章。

HashMap有个很致命的问题就是他并非线程安全,因此在多线程环境下使用HashMap会出现问题,HashTable线程安全,但是它的效率太低了,ConcurrentHashMap就出现了,ConcurrentHashMap兼顾了线程安全和速度,下面就从底层源码出发来了解一下ConcurrentHashMap。这里用到的JDK版本是1.8。

1.ConcurrentHashMap概述

首先看一下官方对ConcurrentHashMap的介绍,这段介绍来自Java Platform SE8

A hash table supporting full concurrency of retrievals and high expected concurrency for updates. This class obeys the same functional specification as Hashtable, and includes versions of methods corresponding to each method of Hashtable. However, even though all operations are thread-safe, retrieval operations do not entail locking, and there is not any support for locking the entire table in a way that prevents all access.

英文好的同学直接看上面的英语介绍,下面是我蹩脚的翻译

一个支持检索时完全并发性和更新时高并发性的哈希表。这个类遵循与Hashtable相同的函数规范,并包含与每个Hashtable方法对应的方法版本。但是,即使所有操作都是线程安全的,检索操作也不需要加锁,并且不支持以阻止所有访问的方式锁定整个表。

简单来讲,和HashTable相比,ConcurrentHashMap效率更高,并且不会对整张表进行加锁,检索时也不需要加锁。

2.ConcurrentHashMap的使用

ConcurrentHashMap使用不难,注意ConcurrentHashMap传入的key和value不能为空,put操作为key和value均添加了@NotNull注解

f08d12731ad921d70029541a87739e0e.png

ConcurrentHashMap的使用和其他的Map类集合相同,用的比较多的如put、get、remove。下面展示这些常见的用法:

ConcurrentHashMap cchashMap = new ConcurrentHashMap();//put添加数据cchashMap.put("1","java");cchashMap.put("2","C");cchashMap.put("3","C++");System.out.println(cchashMap.get("1")); //javaSystem.out.println(cchashMap.size()); //3cchashMap.remove("1");System.out.println(cchashMap.size()); //2//其中一种遍历方式Iterator<Map.Entry<String,String>> iterator=cchashMap.entrySet().iterator();while (iterator.hasNext()){    Map.Entry<String,String> entry=iterator.next();    System.out.println(entry.getKey()+":"+entry.getValue());}

 3.ConcurrentHashMap的原理解析

ConcurrentHashMap做到了线程安全,其并发性通过CAS+synchronized锁来实现

ConcurrentHashMap底层和Hashmap一样通过数组+链表+红黑树的方式实现。

JDK1.8中的ConcurrentHashMap数据结构如下所示:

28f8dc3f45557285fca87c1c273d3e59.png

Node是ConcurrentHashMap中存放key、value以及key的hash值的数据结构:

static class Node<K,V> implements Map.Entry<K,V> {    final int hash;    final K key;    volatile V val;    volatile Node next;    //具体内部方法参照源码}

当链表转化成红黑树时,用TreeNode存储对象

static final class TreeNode<K,V> extends Node<K,V> {    TreeNode parent;  // red-black tree links    TreeNode left;    TreeNode right;    TreeNode prev;    // needed to unlink next upon deletion    boolean red;    //具体方法见源码内部}

在数组中,转变为红黑树后存放的不是TreeNode对象,而是TreeBin对象

static final class TreeBin<K,V> extends Node<K,V> {    TreeNode root;    volatile TreeNode first;    volatile Thread waiter;    volatile int lockState;    // values for lockState    static final int WRITER = 1; // set while holding write lock    static final int WAITER = 2; // set when waiting for write lock    static final int READER = 4; // increment value for setting read lock    //具体方法见源码内部}

4.ConcurrentHashMap初始化

ConcurrentHashMap提供了五种构造方法:

//无参构造方法,创建一个concurrenthashmap对象public ConcurrentHashMap() {}//传入初始容量的参数,如果传入的值非2的幂次方,tableSizeFor会将值修改为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;}//传入一个map集合,执行put操作public ConcurrentHashMap(Map extends K, ? extends V> m) {    this.sizeCtl = DEFAULT_CAPACITY;    putAll(m);}//传入初试容量与负载因子后执行最后一个构造方法public ConcurrentHashMap(int initialCapacity, float loadFactor) {    this(initialCapacity, loadFactor, 1);}//修改初始值和负载因子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 bins        initialCapacity = concurrencyLevel;   // as estimated threads    long size = (long)(1.0 + (long)initialCapacity / loadFactor);    int cap = (size >= (long)MAXIMUM_CAPACITY) ?        MAXIMUM_CAPACITY : tableSizeFor((int)size);    this.sizeCtl = cap;}

ConcurrentHashMap的构造方法都没有实际对table进行初始化,对table的初始化会放在put时。

下面是初始化的代码,在初始化table中,就体现出了线程安全的一些操作,比如第六行代码使用CAS操作来控制只能有一个线程初始化table。

private final Node[] initTable() {    Node[] tab; int sc;    while ((tab = table) == null || tab.length == 0) { //如果表为空则执行初始化操作        if ((sc = sizeCtl) < 0)  //如果sizeCtl小于0,说明此时有其他线程在初始化或扩展表            Thread.yield(); // 使当前线程由执行状态,变成为就绪状态,让出cpu时间        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { //通过cas操作去竞争初始化表的操作,设定为-1表示要初始化了            try {                if ((tab = table) == null || tab.length == 0) {//如果指定了大小就创建指定大小的数组,否则创建默认的大小                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;                     @SuppressWarnings("unchecked")                    Node[] nt = (Node[])new Node,?>[n];                    table = tab = nt;                    sc = n - (n >>> 2);                }            } finally {                sizeCtl = sc;  //sizeCtl长度为数组长度的3/4            }            break;        }    }    return tab;}

代码中sizeCtl是用来控制table初始化和扩容的,初始化时制定了大小,为数组的3/4。当其为负值时,表示表正在初始化或扩容。-1表示初始化,-(1+n)表示几个线程正在扩容

5.put操作

调用put方法后会跳转到putVal方法中执行其中的代码,简单来讲:第一次添加元素时,默认容量为16,当table为空时,直接将元素放在table上,如果不为空,则通过链表或红黑树的方式存放。链表转红黑树的条件为:链表长度大于等于8,并且table容量大于等于64。详细过程我已经在代码中注释出来。

public V put(K key, V value) {    return putVal(key, value, false);}final V putVal(K key, V value, boolean onlyIfAbsent) {    if (key == null || value == null) throw new NullPointerException(); //判断key和value是否为空    int hash = spread(key.hashCode());//计算key的hash值    int binCount = 0;  //用来计算该节点的元素个数    for (Node[] tab = table;;) {        Node f; int n, i, fh;        if (tab == null || (n = tab.length) == 0) //第一次put时进行初始化            tab = initTable();        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { //通过&运算计算这个key在table中的位置            if (casTabAt(tab, i, null,                         new Node(hash, key, value, null)))                break;                   // 如果该位置没有元素,通过cas操作添加元素,此时没有上锁        else if ((fh = f.hash) == MOVED)  //如果检测到hash值时MOVED,表示正在进行数组扩张的数据复制阶段            tab = helpTransfer(tab, f);  //执行helpTransfer方法帮助复制,减少性能损失        else {            /*            *如果这个位置有元素就进行加锁,            *如果是链表,就遍历所有元素,如果存在相同key,则覆盖value,否则将数据添加在尾部            *如果是红黑树,则调用putTreeVal的方式添加元素            *最后判断同一节点链表元素个数是否达到8个,达到就转链表为红黑树或扩容            */            V oldVal = null;            synchronized (f) {//加锁                if (tabAt(tab, i) == f) {                    if (fh >= 0) {                        binCount = 1;                        for (Node e = f;; ++binCount) { //遍历链表,存在相同key则覆盖,否则添加元素到尾部                            K ek;                            if (e.hash == hash &&                                ((ek = e.key) == key ||                                 (ek != null && key.equals(ek)))) {                                oldVal = e.val;                                if (!onlyIfAbsent)                                    e.val = value;                                break;                            }                            Node pred = e;                            if ((e = e.next) == null) {                                pred.next = new Node(hash, key,                                                          value, null);                                break;                            }                        }                    }                    else if (f instanceof TreeBin) { //如果是红黑树,则调用putTreeVal方法存入元素                        Node p;                        binCount = 2;                        if ((p = ((TreeBin)f).putTreeVal(hash, key,                                                       value)) != null) {                            oldVal = p.val;                            if (!onlyIfAbsent)                                p.val = value;                        }                    }                }            }            if (binCount != 0) {                 if (binCount >= TREEIFY_THRESHOLD) //当一个节点中元素数量大于等于8的时候,执行treeifyBin                    treeifyBin(tab, i);                if (oldVal != null)                    return oldVal;                break;            }        }    }    addCount(1L, binCount);    return null;}

6.扩容操作

在上一段代码中我们可以看到,当一条链表中元素个数大于等于8时,会执行treeifyBin来判断是扩容还是转化为红黑树。

/**当table长度小于64的时候,扩张数组长度一倍,否则把链表转化为红黑树*/private final void treeifyBin(Node[] tab, int index) {    Node b; int n, sc;    if (tab != null) {        if ((n = tab.length) < MIN_TREEIFY_CAPACITY) //如果table长度小于64            tryPresize(n << 1);  //table长度扩大一倍        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) { //否则,将链表转为树            synchronized (b) {                if (tabAt(tab, index) == b) {                    TreeNode hd = null, tl = null;                    for (Node e = b; e != null; e = e.next) {                        TreeNode p =                            new TreeNode(e.hash, e.key, e.val,                                              null, null);                        if ((p.prev = tl) == null)                            hd = p;                        else                            tl.next = p;                        tl = p;                    }                    setTabAt(tab, index, new TreeBin(hd)); //把头节点放入容器TreeBin中                }            }        }    }}

再来看一下扩容的操作,扩容操作传入的参数是size,会通过size计算出一个c值,然后用c值和sizeCtl进行比较,直到sizeCtl大于等于c时,才会停止扩容。

private final void tryPresize(int size) {    //计算c的大小,如果size比最大容量一半还大,则直接等于最大容量,否则通过tableSizeFor计算出一个2的幂次方的数    //计算出的这个c会与sizeCtl进行比较,一直到sizeCtl>=c时才会停止扩容    int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :        tableSizeFor(size + (size >>> 1) + 1);    int sc;    //另sc等于sizeCtl    while ((sc = sizeCtl) >= 0) {        Node[] tab = table; int n;        //如果table为空则初始化,这里和初始化时代码一样        if (tab == null || (n = tab.length) == 0) {            n = (sc > c) ? sc : c;            if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {                try {                    if (table == tab) {                        @SuppressWarnings("unchecked")                        Node[] nt = (Node[])new Node,?>[n];                        table = nt;                        sc = n - (n >>> 2);                    }                } finally {                    sizeCtl = sc;                }            }        }        //如果c比sizeCtl要小或者table的长度大于最大长度才停止扩容        else if (c <= sc || n >= MAXIMUM_CAPACITY)            break;        else if (tab == table) {            int rs = resizeStamp(n);            //如果正在扩容(sc<0),帮助扩容            if (sc < 0) {                Node[] nt;                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);            }            //否则直接进行扩容            else if (U.compareAndSwapInt(this, SIZECTL, sc,                                         (rs << RESIZE_STAMP_SHIFT) + 2))                transfer(tab, null);        }    }}

到这里put操作就算是结束了。

7.get操作

看完put后后面的操作就简单了,get操作不设计线程安全,因此不用加锁。首先通过hash值判断该元素放在table的哪个位置,通过遍历的方式找到指定key的值,不存在返回null

public V get(Object key) {    Node[] tab; Node e, p; int n, eh; K ek;    int h = spread(key.hashCode()); //计算key的hash值    //如果table不为空并且table的容量大于0并且key在table的位置不等于空    if ((tab = table) != null && (n = tab.length) > 0 &&        (e = tabAt(tab, (n - 1) & h)) != null) {        if ((eh = e.hash) == h) {             //如果table上的key就是要找的key,返回value            if ((ek = e.key) == key || (ek != null && key.equals(ek)))                return e.val;        }        else if (eh < 0)            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;        }    }    return null;}

8.remove操作

调用remove方法后会自动跳转到replaceNode方法中,删除节点的主要过程为首先判断table是否为空,再判断是否正在扩容,通过遍历的方式找到节点后删除。通过对单个链表或红黑树加锁的方式使得可以多线程删除元素。

public V remove(Object key) {    return replaceNode(key, null, null);}final V replaceNode(Object key, V value, Object cv) {    int hash = spread(key.hashCode());    for (Node[] tab = table;;) {        Node f; int n, i, fh;        //如果table为空或者发现不存在该key,直接退出循环        if (tab == null || (n = tab.length) == 0 ||            (f = tabAt(tab, i = (n - 1) & hash)) == null)            break;         //如果等于MOVED,表示其他线程正在扩容,帮助扩容           else if ((fh = f.hash) == MOVED)            tab = helpTransfer(tab, f);        else {            V oldVal = null;            boolean validated = false;            synchronized (f) {                //二次校验,如果tabAt(tab, i)不等于f,说明已经被修改了                if (tabAt(tab, i) == f) {                    if (fh >= 0) {                        validated = true;                        for (Node e = f, pred = null;;) {                            K ek;                            //找到对应的节点                            if (e.hash == hash &&                                ((ek = e.key) == key ||                                 (ek != null && key.equals(ek)))) {                                V ev = e.val;                                //删除节点或者更新节点的条件                                if (cv == null || cv == ev ||                                    (ev != null && cv.equals(ev))) {                                    oldVal = ev;                                    //更新节点                                    if (value != null)                                        e.val = value;                                    //删除非头节点                                        else if (pred != null)                                        pred.next = e.next;                                    //删除头节点                                        else                                        setTabAt(tab, i, e.next);                                }                                break;                            }                            //继续遍历                            pred = e;                            if ((e = e.next) == null)                                break;                        }                    }                    //如果是红黑树则按照树的方式删除或更新节点                    else if (f instanceof TreeBin) {                        validated = true;                        TreeBin t = (TreeBin)f;                        TreeNode r, p;                        if ((r = t.root) != null &&                            (p = r.findTreeNode(hash, key, null)) != null) {                            V pv = p.val;                            if (cv == null || cv == pv ||                                (pv != null && cv.equals(pv))) {                                oldVal = pv;                                if (value != null)                                    p.val = value;                                else if (t.removeTreeNode(p))                                    setTabAt(tab, i, untreeify(t.first));                            }                        }                    }                }            }            if (validated) {                if (oldVal != null) {                    //如果删除了节点,更新长度                    if (value == null)                        addCount(-1L, -1);                    return oldVal;                }                break;            }        }    }    return null;}

9.总结

ConcurrentHashmap通过cas和synchronized锁的方式实现了线程安全,通过一个Node数组保存map键值对,在通过数组下通过链表和红黑树保存元素。第一次调用构造方法时不会初始化table,初始化table会在put操作时初始化。

因为可以让多个线程同时处理,在ConcurrentHashmap中增加了一个sizeCtl变量,这个变量用来控制table的初始化和扩容,

sizeCtl :默认为0,用来控制table的初始化和扩容操作

-1 代表table正在初始化

-N 取-N对应的二进制的低16位数值为M,此时有M-1个线程进行扩容

其余情况:

1、如果table未初始化,表示table需要初始化的大小。

2、如果table初始化完成,表示table的容量,默认是table大小的0.75倍

第一次添加元素时,默认容量为16,当table为空时,直接将元素放在table上,如果不为空,则通过链表或红黑树的方式存放。链表转红黑树的条件为:链表长度大于等于8,并且table容量大于等于64。

 以上是我对ConcurrentHashmap底层源码的总结,如果有任何问题可以在评论区反馈或者私信我

<think>我们正在讨论ConcurrentHashMapJDK1.8中的实现原理。在JDK1.8中,ConcurrentHashMap进行了重大改进,放弃了分段锁(Segment)的设计,转而采用更细粒度的桶(bucket)级别锁,并大量使用CAS操作来提升并发性能。 主要特点: 1. 数据结构:与HashMap类似,使用数组+链表/红黑树。当链表长度超过阈值(默认为8)时,链表会转换为红黑树;当红黑树节点数小于阈值(默认为6)时,会退化为链表。 2. 并发控制:使用CAS(Compare and Swap)和synchronized关键。CAS用于无锁的更新操作(如初始化数组、更新节点等),而synchronized则用于锁定桶的头节点,这样锁的粒度更小,并发度更高。 具体实现原理: - 初始化:使用sizeCtl变量控制初始化和扩容状态,通过CAS设置sizeCtl来保证只有一个线程进行初始化。 - put操作: a. 如果数组为空,则先初始化数组(initTable)。 b. 计算key的hash值,定位到数组下标。如果该位置为空,则使用CAS尝试添加节点(如果CAS失败则重试)。 c. 如果该位置不为空(可能是链表或红黑树),则使用synchronized锁住头节点,然后进行插入或更新操作。 d. 插入后判断链表长度是否超过阈值,超过则转换为红黑树。 - get操作:无锁,因为节点的value和next都是volatile的,保证了可见性。 - size操作:使用一个CounterCell数组(类似LongAdder的分段计数)来减少竞争,提高并发性能。 下面我们通过代码来展示关键部分:</think>### ConcurrentHashMapJDK 1.8 中的实现原理详解 ConcurrentHashMapJDK 1.8 中进行了重大重构,抛弃了 JDK 1.7 的分段锁设计,采用了更高效、更细粒度的并发控制策略。以下是核心实现原理: #### 1. 底层数据结构 ```java // 核心数据结构 transient volatile Node<K,V>[] table; // 哈希表数组 private transient volatile int sizeCtl; // 控制标识符 private transient volatile CounterCell[] counterCells; // 并发计数 ``` - **数组 + 链表/红黑树**:与 HashMap 类似 - **Node 节点**:存储键值对,链表节点 - **TreeNode 节点**:红黑树节点(当链表长度 ≥ 8 时转换) - **ForwardingNode**:扩容时使用的特殊节点 #### 2. 核心并发控制机制 **CAS + synchronized 桶级锁**: ```java final V putVal(K key, V value, boolean onlyIfAbsent) { // ... for (Node<K,V>[] tab = table;;) { if (tab == null || (n = tab.length) == 0) tab = initTable(); // CAS初始化 else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value))) break; // CAS添加新节点 } else { synchronized (f) { // 锁定桶的头节点 // 处理链表/红黑树插入 } } } // ... } ``` - **CAS 操作**: - 初始化哈希表(`initTable()`) - 添加新节点到空桶 - 更新 sizeCtl 控制变量 - **synchronized 锁**: - 锁定桶的头节点(非空桶操作) - 锁粒度 = 单个桶(哈希槽) #### 3. 扩容机制(多线程协同) ```java private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) { // 每个线程处理一段桶区间 while (advance) { // 分配任务区间 [bound, i] } // 处理桶内节点 synchronized (f) { // 将节点迁移到新表 } // 设置ForwardingNode标识 setTabAt(tab, i, fwd); } ``` - **并行扩容**: - 线程协助迁移:当线程访问到 ForwardingNode 时参与扩容 - 任务分割:将哈希表划分为多个区间,不同线程处理不同区间 - **无锁查询**: - 扩容期间读操作无需加锁 - 通过 ForwardingNode 访问新表 #### 4. 计数机制(LongAdder 风格) ```java final long sumCount() { CounterCell[] as = counterCells; long sum = baseCount; if (as != null) { for (CounterCell a : as) { if (a != null) sum += a.value; } } return sum; } ``` - **分段计数**: - `baseCount`:基础计数值 - `CounterCell[]`:分段计数器数组 - **减少竞争**: - 更新时优先尝试 CAS 更新 baseCount - 竞争激烈时使用 CounterCell 分散更新 #### 5. 红黑树优化 ```java static final class TreeBin<K,V> extends Node<K,V> { TreeNode<K,V> root; volatile TreeNode<K,V> first; volatile int lockState; // 读写锁状态 // 读锁计数 static final int WAITER = 2; // 写等待 static final int WRITER = 1; // 写锁定 } ``` - **树桶锁**: - 使用读写锁状态(lockState)控制并发 - 读操作共享锁,写操作独占锁 - **链表化优化**: - 扩容时如果树节点 ≤ 6,退化为链表 ### 性能优势对比 JDK 1.7 | 特性 | JDK 1.7(分段锁) | JDK 1.8(CAS + synchronized) | |--------------|--------------------------|-------------------------------| | 锁粒度 | 段级别(默认16段) | 桶级别(哈希槽) | | 并发度 | 最大=段数 | 最大=桶数 | | 扩容机制 | 单线程扩容 | 多线程协同扩容 | | 计数实现 | 全局锁更新 | LongAdder 风格分段计数 | | 数据结构 | 数组+链表 | 数组+链表/红黑树 | | 内存消耗 | 较高(段对象开销) | 较低 | ### 关键设计思想图解 ```mermaid graph TD A[put操作] --> B{桶是否为空?} B -->|是| C[CAS添加新节点] B -->|否| D[synchronized锁桶头节点] D --> E{桶结构} E -->|链表| F[链表插入] E -->|红黑树| G[树插入] F --> H{长度≥8?} H -->|是| I[转为红黑树] G --> J[维护树结构] K[get操作] --> L[无锁访问] L --> M{桶结构} M -->|链表| N[遍历链表] M -->|红黑树| O[树查找] P[扩容] --> Q[设置ForwardingNode] Q --> R[多线程迁移数据] R --> S[完成扩容] ``` ### 总结 JDK 1.8ConcurrentHashMap 通过以下创新实现高并发: 1. **细粒度锁**:桶级别锁(synchronized)替代分段锁 2. **CAS 优化**:无锁操作处理空桶添加和初始化 3. **协同扩容**:多线程并行迁移数据 4. **分段计数**:LongAdder 风格减少计数竞争 5. **读写分离**:红黑树桶使用读写状态控制 这种设计在保持线程安全的同时,大幅提升了并发性能,特别适合高并发场景。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值