并发容器(五) — ConcurrentHashMap 源码分析(JDK1.8)

一、概述

关联文章:

1.1 背景

HashMap(三) — 高并发场景下的问题分析 我们知道,在高并发下 HashMap是线程不安全的,主要原因是在同一个桶中并发操作会带来问题,为此我们只需要解决并发操作同一个桶的安全性问题即可,因此在SDK提供的ConcurrentHashMap类中通过分段锁(JDK1.7)或CAS+Sync(JDK1.8)来保证操作的安全性。

1.2 数据结构

  • JDK 1.7 HashMap 是通过 数组+链表来实现的,而ConcurrenthashMap在此基础上使用了分段锁来实现,即最终的数据结构为 Segment数组 + HashEntry数组 + HashEntry链表。ConcurrentHashMap 源码分析(JDK1.7)
  • JDK 1.8 HashMap 是通过 数组+链表+红黑树来实现的,而ConcurrenthashMap在此基础上放弃了原有的分段锁机制,采用CAS+Sync同步块的方案来实现。

ConcurrenthashMap 的数据结构如下图所示:
在这里插入图片描述

1.3 预备知识

下面关于 Unsafe 和 Integer 类中几个方法的含义,请参考ConcurrentHashMap 源码分析(JDK1.7)

  • Unsafe.arrayBaseOffset(Class<?> var) & Unsafe.arrayIndexScale(Class<?> var1)
  • Integer.numberOfLeadingZeros(int i) & 31 - Integer.numberOfLeadingZeros(int)

二、源码分析

JDK1.8版本中,我们主要分析如下几个方法:

  1. 静态代码块
  2. 构造方法
  3. ConcurrentHashMap.put(K key, V value) :存数据
  4. ConcurrentHashMap.get(Object key) :取数据
  5. ConcurrentHashMap.addCount(long x, int check) :计数+扩容
  6. ConcurrentHashMap.helpTransfer(Node<K,V>[] tab, Node<K,V> f) :数据迁移

2.1 静态代码块

通过 Unsafe 类获取数组相关信息。

static {
    try {
        U = sun.misc.Unsafe.getUnsafe();
        // ...
        Class<?> ak = Node[].class;
        // 计算出Node[]第一个元素的偏移地址
        ABASE = U.arrayBaseOffset(ak);
        // 计算出Node[]中每个元素的大小
        int scale = U.arrayIndexScale(ak);
        // 校验两个数组元素占用内存大小是否是2^n。
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");
        // 计算位运算时的偏移量
        ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
    } catch (Exception e) {
        throw new Error(e);
    }
}

2.2 构造方法

// volatile修饰,保证Node[]扩容时,其它线程可见。
transient volatile Node<K,V>[] table;
private transient volatile Node<K,V>[] nextTable;
private transient volatile long baseCount;
private transient volatile int sizeCtl;

public ConcurrentHashMap() {}

// 与JDK1.7不同,构造中没有初始化容器。
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.Node.class
static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val; // volatile 保证节点数据的可见性
    volatile Node<K,V> next; // volatile 保证下个节点的可见性
}

小结:

  • 与JDK1.7的ConcurrentHashMap不同,构造中没有初始化容器。
  • volatile 修饰 Node<K,V>[],保证当前线程创建数组后可以被其它线程看到。
  • JDK1.8的ConcurrentHashMap与HashMap类似。

2.3 ConcurrentHashMap.put(K key, V value)

// ConcurrentHashMap.class
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();
    // 1.hash计算
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        // 2.Node[]不存在,则初始化Node数组,并进入下次for循环。
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        	// 3.tab[i]处的头节点为null时,则直接通过CAS操作添加Node。
            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)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            // 都不满足时,synchronized 锁住f节点(tab[index]头节点)
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                	// 头节点为链表Node节点。
                    if (fh >= 0) {
                        binCount = 1;
                        // 遍历查找。找到了就更新value,没找到就在链表末尾新增Node。
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            // 匹配到Node,则更新值
                            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;
                            // 没有匹配到Node,则新增Node
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    // 头节点为红黑树
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        // 在红黑树中查找。找到了就更新值,没找到就新增TreeBin节点。
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
            	// 遍历链表时binCount会自增,所以当链表长度>=8时,需要转为红黑树。
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // size计数(与JDK1.7的方案不同)
    addCount(1L, binCount);
    return null;
}

put操作流程:

  1. step1:计算 hash 值,并开始遍历Table。
  2. step2:当 Table 未初始化时,则初始化Table并通过 CAS 赋值。初始化结束后则进入下次for循环。
  3. step3:当 Table 初始化时,获取 Table[i] 头节点,并判断是否为Null。
  4. step4:当第3步 Table[i] == NULL 成立时,则直接新建一个Node并添加到 Table[i] 的位置,并执行step8。
  5. step5:当第3步 Table[i] = NULL 不成立时,则继续判断 Table[i] 头节点的hash值是否为 MOVE(正在迁移数据)。如果为MOVE,则本次for循环执行数据迁移任务。
  6. step6:当Table[i] 头节点的hash值不为 MOVE时,开始加锁并执行添加逻辑 (按照链表/红黑树)。
  7. step7:判断 Table[i] 头节点类型 (链表/红黑树)。
    • 链表:优先查询,如果没有匹配到则在链表末尾新增节点添加数据。
    • 红黑树:优先查询,如果没有匹配到则新增红黑树节点。
  8. step8:最后执行计数和扩容判断逻辑。如果存在扩容,则完成数据迁移。

put操作流程图:
在这里插入图片描述

2.4 ConcurrentHashMap.get(Object key)

// hash为负值的几种可能性。
static final int MOVED     = -1; // hash for forwarding nodes
static final int TREEBIN   = -2; // hash for roots of trees
static final int RESERVED  = -3; // hash for transient reservations
    
public V get(Object key) {
    Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
    // hash计算
    int h = spread(key.hashCode());
    // tabAt(tab, (n - 1) & h)计算在table中的位置,并获得头节点。
    if ((tab = table) != null && (n = tab.length) > 0 &&
        (e = tabAt(tab, (n - 1) & h)) != null) {
        // hash值相同,则进一步判断key是否相同,key相同则直接返回。
        if ((eh = e.hash) == h) {
            if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                return e.val;
        }
        //hash值为负值表示正在扩容,这个时候查的是ForwardingNode的find方法来定位到nextTable来
        //eh=-1,说明该节点是一个ForwardingNode,正在迁移,此时调用ForwardingNode的find方法去nextTable里找。
        //eh=-2,说明该节点是一个TreeBin,此时调用TreeBin的find方法遍历红黑树,由于红黑树有可能正在旋转变色,所以find里会有读写锁。
        //eh>=0,说明该节点下挂的是一个链表,直接遍历该链表即可。
        else if (eh < 0)
            return (p = e.find(h, key)) != null ? p.val : null;
        // hash值相同且key不同,则遍历链表继续查找。
        while ((e = e.next) != null) {
            if (e.hash == h &&
                ((ek = e.key) == key || (ek != null && key.equals(ek))))
                return e.val;
        }
    }
    return null;
}

2.5 ConcurrentHashMap.addCount(long x, int check)

主要功能: 计数+数据扩容

参考:ConcurrentHashMap addCount分析


2.6 ConcurrentHashMap.helpTransfer(Node<K,V>[] tab, Node<K,V> f)

主要功能: 数据迁移

参考:ConcurrentHashMap transfer分析


三、小结

  1. JDK1.8的 ConcurrentHashMap 摒弃了JDK1.7的 Segment 分段锁方案,采用 Node+CAS+Synchronied 的方案保证数据的安全操作。
  2. JDK1.8的 ConcurrentHashMap.put() 流程与JDK1.8的 HashMap.put() 流程比较接近,在 HashMap 的基础上增加了并发迁移逻辑和同步代码块(Synchronied)。
  3. JDK1.7的 ConcurrentHashMap 需要两次hash计算,而JDK1.8版本只需要计算一次。
  4. JDK1.7的 Segment 分段锁不能扩容,而JDK1.8的 Node[] 可以实现扩容,且操作粒度更细(Node[]中每一个头节点都可以成为一个锁)。
  5. JDK1.8 Size的计算也与 JDK1.7 计算方式不同。
  6. JDK1.8 链表新增Node时采用尾插法,而 JDK1.7 链表新增Node时采用头插法 。
  7. 缺点:扩容(数据迁移)和size的计算方式太复杂。

四、参考:

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值