ConcurrentHashMap

为什么要使用 ConcurrentHashMap

HashMap死循环分析 ( jdk 1.7 )

  • 引发死循环,是在 HashMap 的扩容操作中,1.7中HashMap是由数组加链表构成的。

正常扩容

1、取当前 table 的 2 倍作为新 table 的大小 ;
2、根据算出的新 table 的大小 new 出一个新的 Entry 数组来,名为 newTable;
3、轮询原 table 的每一个位置,将每个位置上连接的 Entry,算出在新 table 上的位置,并以链表形式连接;
4、原 table 上的所有 Entry 全部轮询完毕之后,意味着原 table 上面的所有 Entry 已经移到了新的 table 上,HashMap 中的 table 指向 newTable。

  • 扩容前

在这里插入图片描述

  • 第一步

在这里插入图片描述

  • 第二步

在这里插入图片描述

  • 第三步

在这里插入图片描述

  • HashMap在1.7使用的是头插法,后进入的元素放到头部,原来的链表变成了倒序。

并发扩容

  • 链表在添加数据,属于头插,当扩容时,比如线程1对key=3和key=7转移到新的位置3,先插key=3,再插key=7,所以key=7是头,key=3挂在key=7下面,完成转移
    这时候线程2又对key=3和key=7进行转移,把key=3插到头部,但是现在3号位置头部是key=7,所以key=3的下一个指向了key=7,如下图所示

在这里插入图片描述

  • 当你去getkey=3或者key=7都不会有问题,但是当你去get一个不存在的key,比如key=11,当遍历这个链表的时候,因为找不到key就继续查找下一个next,这时候就会一直next,进入死循环,cpu会100%

ConcurrentHashMap

1. 在 1.7 下的实现

在这里插入图片描述

  • ConcurrentHashMap 是由 Segment 数组结构和 HashEntry 数组结构组成。 Segment 是一种可重入锁(继承了ReentrantLock),在 ConcurrentHashMap 里扮演锁的角色;HashEntry 则用于存储键值对数据。一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和 HashMap 类似,是一种数组和链表结构。一个 Segment 里包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素, 每个 Segment 守护着一个 HashEntry 数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先获得与它对应的 Segment 锁。
public ConcurrentHashMap(int initialCapacity,float loadFactor,int concurrencyLevel){}
  • ConcurrentHashMap 初始化方法是通过 initialCapacity、loadFactor 和 concurrencyLevel(参数 concurrencyLevel 是用户估计的并发级别,就是说你觉得最多有多少线程共同修改这个 map,根据这个来确定 Segment 数组的大小 concurrencyLevel 默认是 DEFAULT_CONCURRENCY_LEVEL = 16;)等几个参数来初始化 segment 数组、段偏移量 segmentShift、段掩码 segmentMask 和每个 segment 里的 HashEntry 数组来实现的。

  • 并发级别可以理解为程序运行时能够同时更新 ConccurentHashMap 且不产 生锁竞争的最大线程数,实际上就是 ConcurrentHashMap 中的分段锁个数,即 Segment[]的数组长度。ConcurrentHashMap 默认的并发度为 16,但用户也可以在构造函数中设置并发度。当用户设置并发度时,ConcurrentHashMap 会使用大于等于该值的最小 2 幂指数作为实际并发度(假如用户设置并发度为 17,实际并发度则为 32)。

  • 如果并发度设置的过小,会带来严重的锁竞争问题;如果并发度设置的过大,原本位于同一个 Segment 内的访问会扩散到不同的 Segment 中,CPU cache 命中率会下降,从而引起程序性能下降。

  • segments 数组的长度 size 是通过 concurrencyLevel 计算得出的。为了能通过按位与的散列算法来定位 segments 数组的索引,必须保证 segments 数组的长度是 2 的 N 次方(power-of-two size),所以必须计算出一个大于或等于 concurrencyLevel 的最小的 2 的 N 次方值来作为 segments 数组的长度。假如 concurrencyLevel 等于 14、15 或 16,size 都会等于 16,即容器里锁的个数也是 16。

  • ConcurrentHashMap 使用分段锁 Segment 来保护不同段的数据,那么在 插入和获取元素的时候,必须先通过散列算法定位到 Segment 。

  • ConcurrentHashMap 允许多个读操作并发进行,读操作并不需要加锁。 ConcurrentHashMap 实现技术是保证 HashEntry 几乎是不可变的以及 volatile 关键字。

static final class HashEntry<K,V>{
    final int hash;
    final K key;
    volatile V value;
    volatile HashEntry<K, V> next;
}

get 操作

  • get 操作先经过一次再散列,然后使用这个散列值通过散列运算定位到 Segment(使用了散列值的高位部分),再通过散列算法定位到 table(使用了散列值的全部)。整个 get 过程,没有加锁,而是通过 volatile 保证 get 总是可以拿到最新值。

put 操作

  • ConcurrentHashMap 初始化的时候会初始化第一个槽 segment[0],对于其他槽,在插入第一个值的时候再进行初始化。
  • 多个线程同时进入初始化同一个槽 segment[k],但只要有一个成功就可以了,使用cas操作控制。
  • put 方法会通过 tryLock()方法尝试获得锁,获得了锁,node 为 null 进入 try 语句块,没有获得锁,调用 scanAndLockForPut 方法自旋等待获得锁。
  • scanAndLockForPut 方法里在尝试获得锁的过程中会对对应 hashcode 的链表进行遍历,如果遍历完毕仍然找不到与 key 相同的 HashEntry 节点,则为后续的 put 操作提前创建一个 HashEntry。当 tryLock 一定次数后仍无法获得锁,则通过 lock 申请锁。
  • 在获得锁之后,Segment 对链表进行遍历,如果某个 HashEntry 节点具有相同的 key,则更新该 HashEntry 的 value 值,否则新建一个 HashEntry 节点,采用头插法,将它设置为链表的新 head 节点并将原头节点设为新 head 的下一个节点。新建过程中如果节点总数(含新建的 HashEntry)超过 threshold,则调用 rehash()方法对 Segment 进行扩容,最后将新建 HashEntry 写入到数组中。

rehash 操作

  • 扩容是新创建了数组,然后进行迁移数据,最后再将 newTable 设置给属性 table。
  • 为了避免让所有的节点都进行复制操作:由于扩容是基于 2 的幂指来操作, 假设扩容前某 HashEntry 对应到 Segment 中数组的 index 为 i,数组的容量为 capacity,那么扩容后该 HashEntry 对应到新数组中的 index 只可能为 i 或者 i+capacity,因此很多 HashEntry 节点在扩容前后 index 可以保持不变。
  • key=15,key=16;库容前table长度是4,则15和16分别在table的3号和0号位置;扩容后table长度是8,则15在table的7号位置(3+4),16在table的0号位置,没有变化。
  • 扩容的目的是为了减少hash碰撞,让元素散列更均匀

remove 操作

  • 与 put 方法类似,都是在操作前需要拿到锁,以保证操作的线程安全性。

ConcurrentHashMap 的弱一致性

  • 对链表遍历判断是否存在 key 相同的节点以及获得该节点的 value。但 由于遍历过程中其他线程可能对链表结构做了调整,因此 get 和 containsKey 返 回的可能是过时的数据,这一点是 ConcurrentHashMap 在弱一致性上的体现。如 果要求强一致性,那么必须使用 Collections.synchronizedMap()方法。

size、containsValue

  • 首先不加锁循环执行以下操作:循环所有的 Segment,获得对应的值以及所有 Segment 的 modcount 之和。当循环次数超过预定义的值时,需要对所有segment加锁,已经不再是分段锁,高并发情况下,效率低;
  • 如果为了判断是否为空,可以使用isEmpty()。

2. 在 1.8 下的实现

在这里插入图片描述

  • 改进一:取消 segments 字段,直接采用 transient volatile HashEntry<K,V>[] table 保存数据,采用 table 数组元素作为锁,从而实现了对缩小锁的粒度,进一步减少并发冲突的概率,并大量使用了采用了 CAS + synchronized 来保证并发安全性。
  • 改进二:将原先 table 数组+单向链表的数据结构,变更为 table 数组+单 向链表+红黑树的结构。对于 hash 表来说,最核心的能力在于将 key hash 之后能均匀的分布在数组中。如果 hash 之后散列的很均匀,那么 table 数组中的每个队列长度主要为 0 或者 1。但实际情况并非总是如此理想,虽然 ConcurrentHashMap 类默认的加载因子为 0.75,但是在数据量过大或者运气不佳的情况下,还是会存在一些队列长度过长的情况,如果还是采用单向列表方式, 那么查询某个节点的时间复杂度为 O(n);因此,对于个数超过 8(默认值)的列表, jdk1.8 中采用了红黑树的结构,那么查询的时间复杂度可以降低到 O(logN),可以改进性能。
  • 使用 Node(1.7 为 Entry) 作为链表的数据结点,仍然包含 key,value, hash 和 next 四个属性。 红黑树的情况使用的是 TreeNode(extends Node)。
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
}

static final class TreeNode<K,V> extends Node<K,V> {
      TreeNode<K,V> parent;  // red-black tree links
      TreeNode<K,V> left;
      TreeNode<K,V> right;
      TreeNode<K,V> prev;    // needed to unlink next upon deletion
      boolean red;
}
  • 根据数组元素中,第一个结点数据类型是 Node 还是 TreeNode 可以判断该位置下是链表还是红黑树。
  • 用于判断是否需要将链表转换为红黑树的阈值
static final int TREEIFY_THRESHOLD = 8;
  • 用于判断是否需要将红黑树转换为链表的阈值
static final int UNTREEIFY_THRESHOLD = 6;

与 1.8 中 HashMap 不同点:

1、它并不是直接转换为红黑树,而是把这些结点放在 TreeBin 对象中,由 TreeBin 完成对红黑树的包装。

2、TreeNode 在 ConcurrentHashMap 扩展自 Node 类,而并非 HashMap 中的 扩展自 LinkedHashMap.Entry<K,V>类,也就是说 TreeNode 带有 next 指针。

TreeBin

  • 负责 TreeNode 节点。它代替了 TreeNode 的根节点,也就是说在实际的 ConcurrentHashMap“数组”中,存放的是 TreeBin 对象,而不是 TreeNode 对象。 另外这个类还带有了读写锁机制。
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;
       // 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
}

特殊的 ForwardingNode

  • 一个特殊的 Node 结点,hash 值为 -1,其中存储 nextTable 的引用。有 table 发生扩容的时候,ForwardingNode 发挥作用,作为一个占位符放在 table 中表示当前结点为 null 或者已经被移动

sizeCtl

  • 用来控制 table 的初始化和扩容操作。
  • 负数代表正在进行初始化或扩容操作
  • -1 代表正在初始化
  • -N 表示有 N-1 个线程正在进行扩容操作
  • 0 为默认值,代表当时的 table 还没有被初始化
  • 正数表示如果数组没有初始化,记录的是数组初始容量,如果数组已经初始化,记录的是数组的扩容阈值

构造方法

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的大小,如果不指定大小,默认是16,如果指定,则是大于指定容量最小的 2 的幂指数,比如你指定16,则变为32。

在这里插入图片描述
在这里插入图片描述

  • 真正的初始化在放在了是在向 ConcurrentHashMap 中插入元素的时候发生 的。如调用 put、computeIfAbsent、compute、merge 等方法的时候,调用时机 是检查 table==null。

get 操作

  • 给定一个 key 来确定 value 的时候,必须满足两个条件 key 相同 hash 值相同,对于节点可能在链表或树上的情况,需要分别去查找。
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    int h = spread(key.hashCode());
    // 1.根据hash值确定节点位置
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // 2.如果是Node数组中元素就直接返回
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        // 3.eh < 0表示节点在树上,调用树的find方法查找
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
            // 4.走到这,说明是链表,遍历链表找到对应的值返回
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

put 操作

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    int hash = spread(key.hashCode());
    int binCount = 0;
    // 1.死循环,何时插入成功,何时跳出
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
        	// 2.如果table为空,初始化table
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        	// 3.Node数组,如果这个位置为空,使用cas操作放值
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
        	// 4.正在扩容,当前线程帮忙扩容
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 5.锁Node数组中的元素
            // 这个位置是Hash冲突组成链表的头节点或者红黑树的根节点
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                	// 6.fh > 0说明这是一个链表的头节点,不是树的根节点
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 7.put和putIfAbsent方法的实现
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            // 8.如果遍历到最后一个节点,使用尾插法,插到链表尾部
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // 9.按照树的方式插入值
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
            	// 10.达到临界值8,把链表转换成树结构
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 11.Map的元素数量+1,并检查是否需要扩容
    addCount(1L, binCount);
    return null;
}
  • 总结来说,沿用 HashMap 的 put 方法的思想,根据 hash 值计算这个新插入的点在 table 中的位置 i,如果 i 位置是空的,直接放进去,否则进行判断,如果 i 位置是树节点,按照树的方式插入新的节点,否则把 i 插入到链表的末尾;
  • 如果这个位置是空的,那么直接放入,而且不需要加锁操作;
  • 如果这个位置存在结点,说明发生了 hash 碰撞,首先判断这个节点的类型。 如果是链表节点,则得到的结点就是 hash 值相同的节点组成的链表的头节点。需要依次向后遍历确定这个新加入的值所在位置。如果遇到 hash 值与 key 值都与新加入节点是一致的情况,则只需要更新 value 值即可。否则依次向后遍历,直到链表尾插入这个结点。如果加入这个节点以后链表长度大于 8且数组长度大于等于64,就把这个链表转换成红黑树;如果加入这个节点以后链表长度大于 8,但数组长度小于64,则数组扩容。如果这个节点的类型已经是树节点的话,直接调用树节点的插入方法进行插入新的值。

初始化

  • 构造方法中并没有真正初始化,真正的初始化在放在了是在向 ConcurrentHashMap 中插入元素的时候发生的。
// 默认大小事16
private static final int DEFAULT_CAPACITY = 16;

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        // 1.cas把sizeCtl设置为-1,表示正在初始化
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                	// 2.n=16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    // 3.n右移2位,相当于除以4,n-n/4等于n的3/4,0.75*n
                    sc = n - (n >>> 2);
                }
            } finally {
            	// 4.设置数组的扩容阈值
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

transfer

  • 当 ConcurrentHashMap 容量不足的时候,需要对 table 进行扩容。这个方法 的基本思想跟 HashMap 是很像的,但是由于它是支持并发扩容的;
  • 为何要并发扩容?因为在扩容的时候,总是会涉及到从一个“数组”到另一 个“数组”拷贝的操作,如果这个操作能够并发进行,就能利用并发处理去减少扩容带来的时间影响。
  • 整个扩容操作分为两个部分:
    第一部分是构建一个 nextTable,它的容量是原来的 2 倍;
    第二个部分就是将原来 table 中的元素复制到 nextTable 中,这里允许多线程 进行操作。

整个扩容流程就是遍历和复制

  • 为 null 或者已经处理过的节点,会被设置为 forwardNode 节点,当线程准备扩容时,发现节点是 forwardNode 节点,跳过这个节点,继续寻找未处理的节点,找到了,对节点上锁;
  • 如果这个位置是 Node 节点(fh>=0),说明它是一个链表,就构造一个反序链表,把他们分别放在 nextTable 的 i 和 i+n 的位置上;
  • 如果这个位置是 TreeBin 节点(fh<0),说明它是一个红黑树,也做一个反序处理,并且判断是否需要红黑树转链表,把处理的结果分别放在 nextTable 的 i 和 i+n 的位置上;
  • 遍历过所有的节点以后就完成了复制工作,这时让 nextTable 作为新的 table, 并且更新 sizeCtl 为新容量的 0.75 倍 ,完成扩容;
  • 并发扩容其实就是将数据迁移任务拆分成多个小迁移任务,在实现上使用了一个变量 stride 作为步长控制,每个线程每次负责迁移其中的一部分,比如被分配到数组0号位置的迁移,除了除了0号位置,还要处理0+stride号位置的迁移。

remove

  • 移除方法的基本流程和 put 方法很类似,只不过操作由插入数据变为移除数 据而已,而且如果存在红黑树的情况下,会检查是否需要将红黑树转为链表。

treeifyBin

  • 用于将过长的链表转换为 TreeBin 对象。但是他并不是直接转换,而是进行一次容量判断,如果容量没有达到转换的要求,直接进行扩容操作并返回;如果满足条件才将链表的结构转换为 TreeBin ,这与 HashMap 不同的是,它并没有把 TreeNode 直接放入红黑树,而是利用了 TreeBin 这个小容器来封装所有的 TreeNode。

size

  • 在 JDK1.8 版本中,对于 size 的计算,在扩容和 addCount()方法就已经有处理了,可以注意一下 Put 函数,里面就有 addCount()函数,早就计算好的,然后你 size 的时候直接给你。JDK1.7 是在调用 size()方法才去计算,其实在并发集合中去计算 size 是没有多大的意义的,因为 size 是实时在变的。
  • 在具体实现上,计算大小的核心方法都是 sumCount()
final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}
  • 可以看见,统计数量时使用了 baseCount 和 CounterCell 类型的变量 counterCells 。其实 baseCount 就是记录容器数量的,而 counterCells 则是记录 CAS 更新 baseCounter 值时,由于高并发而导致失败的值。这两个变量的变化在 addCount() 方法中有体现,大致的流程就是:
    1、对 baseCount 做 CAS 自增操作。
    2、如果并发导致 baseCount CAS 失败了,则使用 counterCells。
    3、如果 counterCells CAS 失败了,在 fullAddCount 方法中,会继续死循环操作,直到成功。

HashTable

HashTable 容器使用 synchronized 来保证线程安全,但在线程竞争激烈的情 况下 HashTable 的效率非常低下。因为当一个线程访问 HashTable 的同步方法, 其他线程也访问 HashTable 的同步方法时,会进入阻塞或轮询状态。如线程 1 使 用 put 进行元素添加,线程 2 不但不能使用 put 方法添加元素,也不能使用 get 方法来获取元素,所以竞争越激烈效率越低。

HashMap 和 HashTable 有什么区别?

1、HashMap 是线程不安全的,HashTable 是线程安全的;

2、由于线程安全,所以 HashTable 的效率比不上 HashMap;

3、HashMap 最多只允许一条记录的键为 null,允许多条记录的值为 null, 而 HashTable 不允许;

4、HashMap 默认初始化数组的大小为 16,HashTable 为 11,前者扩容时, 扩大两倍,后者扩大两倍+1;

5、HashMap 需要重新计算 hash 值,而 HashTable 直接使用对象的 hashCode

Java 中的另一个线程安全的与 HashMap 极其类似的类是什么?同样是线程安全,它与 HashTable 在线程同步上有什么不同?

1、ConcurrentHashMap 类(是 Java 并发包 java.util.concurrent 中提供的一个线程安全且高效的 HashMap 实现)。

2、HashTable 是使用 synchronize 关键字加锁的原理(就是对对象加锁);
ConcurrentHashMap,在 JDK 1.7 中采用分段锁的方式;JDK 1.8 中 直接采用了 CAS(无锁算法)+ synchronized,也采用分段锁的方式并大大缩小了锁的粒度。

HashMap & ConcurrentHashMap 的区别?

1、除了加锁,原理上无太大区别。

2、HashMap 的键值对允许有 null,但是 ConCurrentHashMap 都不允许。 在数据结构上,红黑树相关的节点类,分别为ConCurrentHashMap-Node<K,V> 与 HashMap-LinkedHashMap.Entry<K,V>

为什么 ConcurrentHashMap 比 HashTable 效率要高?

1、HashTable 使用一把锁(锁住整个链表结构)处理并发问题,多个线程 竞争一把锁,容易阻塞;

2、ConcurrentHashMap JDK 1.7 中使用分段锁(ReentrantLock + Segment + HashEntry),相当于把一 个 HashMap 分成多个段,每段分配一把锁,这样支持多线程访问。锁粒度:基于 Segment,包含多个 HashEntry。JDK 1.8 中使用 CAS + synchronized + Node + 红黑树。锁粒度:Node(首结 点)(实现 Map.Entry<K,V>)。锁粒度降低了。

针对 ConcurrentHashMap 锁机制具体分析(JDK 1.7 VS JDK 1.8)?

1、JDK 1.7 中,采用分段锁的机制,实现并发的更新操作,底层采用数组+链表 的存储结构,包括两个核心静态内部类 Segment 和 HashEntry。
①、Segment 继承 ReentrantLock(重入锁) 用来充当锁的角色,每个 Segment 对象守护每个散列映射表的若干个桶;
②、HashEntry 用来封装映射表的键-值对;
③、每个桶是由若干个 HashEntry 对象链接起来的链表。

2、JDK 1.8 中,采用 Node + CAS + Synchronized 来保证并发安全。取消类 Segment,直接用 table 数组存储键值对;当 Node 对象组成的链表长度超过 TREEIFY_THRESHOLD 时,链表转换为红黑树,提升性能。底层变更为数组 + 链表 + 红黑树。

ConcurrentHashMap 在 JDK 1.8 中,为什么要使用内置锁 synchronized 来代替重入锁 ReentrantLock?

1、JDK 1.8 对 synchronized 做了大量性能上的优化,而且基于 JVM 的 synchronized 优化空间更大,更加自然。

2、在大量的数据操作下,对于 JVM 的内存压力,基于 API 的 ReentrantLock 会开销更多的内存。

ConcurrentHashMap 简单介绍?

1、重要的常量:
private transient volatile int sizeCtl;

  • 负数代表正在进行初始化或扩容操作
  • -1 代表正在初始化
  • -N 表示有 N-1 个线程正在进行扩容操作
  • 0 为默认值,代表当时的 table 还没有被初始化
  • 正数表示如果数组没有初始化,记录的是数组初始容量,如果数组已经初始化,记录的是数组的扩容阈值

2、数据结构

  • Node 是存储结构的基本单元,继承 Map 中的 Entry,用于存储数据;
  • TreeNode 继承 Node,但是数据结构换成了二叉树结构,是红黑树的存储 结构,用于红黑树中存储数据;
  • TreeBin 是封装 TreeNode 的容器,提供转换红黑树的一些条件和锁的控制。

3、存储对象时put() 方法

  • 如果没有初始化,就调用 initTable() 方法来进行初始化;
  • 如果没有 hash 冲突就直接 CAS 无锁插入;
  • 如果需要扩容,就先进行扩容;
  • 如果存在 hash 冲突,就加锁来保证线程安全,两种情况:一种是链表形 式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入;
  • 如果该链表的数量大于阀值 8,就要先转换成红黑树的结构,break 再一 次进入循环
  • 如果添加成功就调用 addCount() 方法统计 size,并且检查是否需要扩容。

4、扩容方法 transfer()

  • 默认容量为 16,扩容时,容量变为原来的两倍。
  • helpTransfer():调用多个工作线程一起帮助进行扩容,这样的效率就会更高。

5、获取对象时get()方法

  • 计算 hash 值,定位到该 table 索引位置,如果是首结点符合就返回;
  • 如果遇到扩容时,会调用标记正在扩容节点 ForwardingNode.find()方法, 查找该节点,匹配就返回;
  • 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回 null。

ConcurrentHashMap 的并发度是什么?

  • 1.7 中程序运行时能够同时更新 ConccurentHashMap 且不产生锁竞争的最大线程数。默认为 16,且可以在构造函数中设置。当用户设置并发度时, ConcurrentHashMap 会使用大于等于该值的最小 2 幂指数作为实际并发度(假如用户设置并发度为 17,实际并发度则为 32)。
  • 1.8 中并发度则无太大的实际意义,主要用处就是当设置的初始容量小于并发度,将初始容量提升至并发度大小。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值