ConcurrentHashMap底层原理分析(put方法)

一、简介

  1. ConcurrentHashMap是J.U.C包里提供的一个线程安全且高效的HashMap,类继承关系如图:
    ConcurrentHashMap类图
  2. ConcurrentHashMap基本结构:
    基本结构

内部维护一个存放Node结点的数组,即table。数组的每个位置代表了一个桶,table可以包含4种不同类型的桶:Node、TreeBin、ForwardingNode、ReservationNode。

  1. ConcurrentHashMap包含五种结点:
  • Node结点:普通的Entry结点,以链表形式存储实际数据;
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;
}

public final int hashCode() {
    return key.hashCode() ^ val.hashCode();
}

//链表查找
Node<K, V> find(int h, Object k) {...}

...
}

数据会先以链表的形式存放在Node结点中,当结点到达一定数目时,链表会转化成红黑树,因为链表查找的平均时间复杂度为O(n),而红黑树为O(logn),这样可以提高查找效率。

  • TreeNode结点:红黑树的结点,存储实际数据;
static final class TreeNode<K, V> extends Node<K, V> {
    boolean red;

    TreeNode<K, V> parent;
    TreeNode<K, V> left;
    TreeNode<K, V> right;
    TreeNode<K, V> prev;

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

    //以当前结点为根结点,开始遍历查找指定key
    Node<K, V> find(int h, Object k) {...}

    ...
}
  • TreeBin结点:提供了红黑树相关的操作,以及加解锁操作;
static final class TreeBin<K, V> extends Node<K, V> {
    TreeNode<K, V> root;                // 红黑树结构的根结点
    volatile TreeNode<K, V> first;      // 链表结构的头结点
    volatile Thread waiter;             // 最近的一个设置WAITER标识位的线程

    volatile int lockState;             // 整体的锁状态标识位,0表示未加锁

    static final int WRITER = 1;        // 二进制001,红黑树的写锁状态
    static final int WAITER = 2;        // 二进制010,红黑树的等待获取写锁状态
    static final int READER = 4;        // 二进制100,红黑树的读锁状态,读可以并发,每多一个读线程,lockState都加上一个READER值

    //在hashCode相等并且不是Comparable类型时,用此方法判断大小
    static int tieBreakOrder(Object a, Object b) {...}

    //将以b为头结点的链表转换为红黑树
    TreeBin(TreeNode<K, V> b) {...}

    //对红黑树的根结点加写锁
    private final void lockRoot() {...}

    //释放写锁
    private final void unlockRoot() {...}

    /**
     * 从根结点开始遍历查找,找到“相等”的结点就返回它,没找到就返回null
     * 当存在写锁时,以链表方式进行查找
     * 两种特殊情况下以链表的方式进行查找:
     *  1. 有线程正持有写锁,这样做能够不阻塞读线程
     *  2. 有线程等待获取写锁,不再继续加读锁,相当于“写优先”模式
     */
    final Node<K, V> find(int h, Object k) {...}

     //查找指定key对应的结点,如果未找到,则插入.
    final TreeNode<K, V> putTreeVal(int h, K k, V v) {...}

    /**
     * 删除红黑树的结点:
     * 1. 红黑树规模太小时,返回true,然后进行 树 -> 链表 的转化;
     * 2. 红黑树规模足够时,不用变换成链表,但删除结点时需要加写锁.
     */
    final boolean removeTreeNode(TreeNode<K, V> p) {...}

    ...
}
  • ForwardingNode结点:在扩展时使用,是一种临时结点,hash值固定为-1,不存储实际数据。扩展时如果旧table某一个桶中的结点已经全部迁移到新的table了,则在这个桶中放置一个FowardingNode结点。另外作为转发结点,在扩容期间如果遇到查询操作,会将该查询转发到nextTable,即新table上,不会阻塞查询;
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;
    }

    // 在新的数组nextTable上进行查找
    Node<K, V> find(int h, Object k) {...}
}

读操作碰到ForwardingNode时,将操作转发到扩容后的新table数组上去执行;写操作碰到它时,则尝试协助扩容。

  • ReservationNode结点:保留结点,hash值固定为-3,不存储实际数据,只在computeIfAbsent和compute这两个方法充当占位符加锁使用;
  1. ConcurrentHashMap提供了5个构造器,但是构造器内部只是计算了table的初始容量(赋值给sizeCtrl),并没有创建table的操作,只有在首次插入键值对的时候才会进行初始化table数组的操作。

如果new实例时传入初始大小initialCapacity,那会调用tableSizeFor返回大于initialCapacity的最小2次幂值,并把这个值设置到sizeCtrl。

private static final int tableSizeFor(int c) {
	int n = c - 1;
	n |= n >>> 1;
	n |= n >>> 2;
	n |= n >>> 4;
	n |= n >>> 8;
	n |= n >>> 16;
	return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}

代码各个阶段的结果如下图所示:
初始化Map大小

如果传入初始大小initialCapacity和负载因子loadFactor,则还需要通过1+initialCapacity/loadFactor计算出实际的初始化大小。一个结点在桶中出现的概率符合泊松分布,使用0.75作为负载因子,可以降低结点在某一个桶中出现的概率,降低hash冲突,也就降低了查询时间,同时也不会因为负载因子过小导致table过大,占用过多的内存。

二、常量

private static final int MAXIMUM_CAPACITY = 1 << 30;   //最大容量

private static final int DEFAULT_CAPACITY = 16;        //默认初始容量

private static final float LOAD_FACTOR = 0.75f;        //负载因子,恒定为0.75

static final int TREEIFY_THRESHOLD = 8;                //链表转树的阈值,即链接结点数大于8时, 链表转换为树

static final int UNTREEIFY_THRESHOLD = 6;              //树转链表的阈值,即树结点树小于6时,树转换为链表

static final int MIN_TREEIFY_CAPACITY = 64;            //只有键值对数量大于MIN_TREEIFY_CAPACITY,链表才能转变成树

private static final int MIN_TRANSFER_STRIDE = 16;     //只有键值对数量小于MIN_TRANSFER_STRIDE,树才能转变成链表
 
private static int RESIZE_STAMP_BITS = 16;             //用于在扩容时生成唯一的随机数

private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1; // 可同时进行扩容操作的最大线程数

private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; // 扩容用

static final int MOVED = -1;                           // 标识ForwardingNode结点(在扩容时才会出现,不存储实际数据)
static final int TREEBIN = -2;                         // 标识红黑树的根结点
static final int RESERVED = -3;                        // 标识ReservationNode结点
static final int HASH_BITS = 0x7fffffff;               // usable bits of normal node hash

static final int NCPU = Runtime.getRuntime().availableProcessors(); // CPU核心数,扩容时使用

三、字段定义

transient volatile Node<K, V>[] table; //Node数组,标识整个Map,首次插入元素时创建,大小总是2的幂次

private transient volatile Node<K, V>[] nextTable; //扩容后的新Node数组,只有在扩容时才非空

/**
 * 控制table的初始化和扩容
 * 0  : 初始默认值
 * -1 : 有线程正在进行table的初始化
 * >0 : table初始化时使用的容量,或初始化/扩容完成后的阈值
 * -(1 + nThreads) : 表示有nThreads个线程正在执行扩容
 */
private transient volatile int sizeCtl;

private transient volatile int transferIndex;//扩容时需要用到的一个下标变量

private transient volatile long baseCount;//计数基值,当没有线程竞争时,计数将加到该变量上

private transient volatile CounterCell[] counterCells;//计数数组,出现并发冲突时使用

private transient volatile int cellsBusy;//自旋标识位,用于CounterCell[]扩容时使用。

// 视图相关字段
private transient KeySetView<K, V> keySet;
private transient ValuesView<K, V> values;
private transient EntrySetView<K, V> entrySet;

四、put方法源码分析

从put()方法切入,实际调用了putVal()方法,主要做了以下事情:

  1. 如果key和value为null时,抛出NullPointerException
  2. 调用扰动函数获取哈希值,对key的hashCode进行高16位不变,低16位和高16位进行异或操作,然后将得出的值去掉符号位。
	static final int HASH_BITS = 0x7fffffff;
    
    static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
    }
  1. 遍历table,进行以下判断:
  • 如果table为空,则开始初始化数组。当sizeCtrl小于0时,表示已经有线程正在进行初始化,此时让出当前线程的CPU占用权;否则通过CAS将sizeCtrl设置成-1,表示当前线程已经开始初始化table。创建一个大小为16(默认大小,如果sizeCtrl大于0,则使用sizeCtrl作为table大小,new一个ConcurrentHashMap的时候会设置sizeCtrl作为初始大小)的Node数组赋值给table,并把sizeCtrl设置成0.75,然后进行下一步。
transient volatile Node<K,V>[] table;
private transient volatile int sizeCtl;
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
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2); //设置sizeCtrl = 0.75 * table.length
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}
  • 如果已经table已经初始化,但是table中索引为(n-1)&hash的位置没有值。则将Node节点CAS进table中索引为(n-1)&hash的位置。
private static final long ABASE;
private static final int ASHIFT;

Class<?> ak = Node[].class;
ABASE = U.arrayBaseOffset(ak);
int scale = U.arrayIndexScale(ak);
if ((scale & (scale - 1)) != 0)
    throw new Error("data type scale not a power of two");
ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);

transient volatile Node<K,V>[] table;

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    //table虽然是volatile变量,但对于其他线程只是引用可见,table里的元素不可见,因此需要调用getObjectVolatile保证每次拿到的数据都是最新的。
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}

static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    // 更新Node数组上的元素
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}

由于是通过(n-1)&hash查找元素,因此需要保证n(即table长度)为2的N次幂才能让key均匀分布,减少哈希冲突。

  • 如果table中索引为(n-1)&hash的位置有值,且该位置的元素hash属性为MOVED,说明此时table正在扩容,当前线程可以协助数据迁移,参考addCount方法。
static final int MOVED     = -1;

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        int rs = resizeStamp(tab.length);
        while (nextTab == nextTable && table == tab &&
           (sc = sizeCtl) < 0) {//有其他线程正在扩容
           //以下情况不需要扩容
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
            //将sizeCtrl加1,然后开始扩容+数据迁移
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}

扩容状态下其他线程对集合进行插入、修改、删除、合并、compute等操作时遇到 ForwardingNode 结点会调用该方法。

  • 如果插入的节点不在table上,则遍历链表或者红黑树,如果存在相同哈希值和Key的节点时更新Node/TreeNode的value,否则插入新的Node/TreeNode结点。

使用同步锁synchronized,插入相同hash的元素时会阻塞。

  • 如果链表长度大于8,则将链表转换成红黑树,但是如果table的长度小于64,则只将table扩容一倍,不转换成红黑树,这是为了避免table建立初期,多个键值对恰好放入同一个链表中导致不必要的转换。
static final int TREEIFY_THRESHOLD = 8;
static final int MIN_TREEIFY_CAPACITY = 64;
private static final int MAXIMUM_CAPACITY = 1 << 30;

private final void treeifyBin(Node<K,V>[] tab, int index) {
    Node<K,V> b; int n, sc;
    if (tab != null) {
        //table的容量小于64时,直接进行table扩容,不进行红黑树转换
        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
            tryPresize(n << 1);
            
        //table的容量大于等于64时,将链表转换成红黑树
        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
            synchronized (b) {//给桶的第一个元素加锁
                if (tabAt(tab, index) == b) {
                    TreeNode<K,V> hd = null, tl = null;
                    
                    //遍历链表,建立红黑树
                    for (Node<K,V> e = b; e != null; e = e.next) {
                        TreeNode<K,V> p =
                            new TreeNode<K,V>(e.hash, e.key, e.val,
                                              null, null);
                        if ((p.prev = tl) == null)
                            hd = p;
                        else
                            tl.next = p;
                        tl = p;
                    }
                    //将TreeNode封装成TreeBin,并放到table[index]中
                    setTabAt(tab, index, new TreeBin<K,V>(hd));
                }
            }
        }
    }
}
  1. 更改计数以及扩容,这里先讨论计数的部分,扩容部分后面再做介绍。如果没有并发,直接累加baseCount,如果存在并发,则累加counterCells。
private final void addCount(long x, int check) {
    CounterCell[] as;
    long b, s;
    if ((as = counterCells) != null ||
            // 首先尝试更新baseCount
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {

        // 更新失败,说明出现并发冲突,则将计数值累加到CounterCells[ThreadLocalRandom.getProbe() & counterCells的长度]
        CounterCell a;
        long v;
        int m;
        boolean uncontended = true;
        if (as == null || (m = as.length - 1) < 0 ||
                //根据线程安全的随机数和counterCells长度计算元素索引
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||   
                !(uncontended = U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
            // 计数值累加到CounterCells失败
            fullAddCount(x, uncontended);
            return;
        }
        if (check <= 1)
            return;
        s = sumCount();
    }

    ...(扩容的内容第七节再讨论)
}

如果累加counterCells失败,则会调用fullAddCount()方法,代码逻辑如下:

  • 如果线程安全的随机数为0,则重新生成随机数;
if ((h = ThreadLocalRandom.getProbe()) == 0) {
    ThreadLocalRandom.localInit(); 
    h = ThreadLocalRandom.getProbe();
    wasUncontended = true;//表示当前不存在锁竞争
}
  • 如果counterCells已经初始化,尝试加锁,然后将计数插入或者更新counterCells中,如果失败则进行扩容(原来的2倍);
//counterCells[(n-1)&h]没有被使用(n为counterCells的长度),尝试加锁,然后将计数插入counterCells
if ((a = as[(n - 1) & h]) == null) {
    if (cellsBusy == 0) {//counterCells没有加锁 
        CounterCell r = new CounterCell(x); 
        if (cellsBusy == 0 &&
            //尝试加锁,通过CAS将cellsBusy从0设置为1
            U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
            boolean created = false;
            try {             
                CounterCell[] rs; int m, j;
                if ((rs = counterCells) != null &&
                    (m = rs.length) > 0 &&
                    rs[j = (m - 1) & h] == null) {//重新判断一次
                    rs[j] = r;//将创建的CounterCell赋值到counterCells[(n-1)&h]
                    created = true;
                }
            } finally {
                cellsBusy = 0;//释放锁
            }
            if (created)//如果创建的CounterCell加到counterCells则停止自旋
                break;
            continue; 
        }
    }
    collide = false;//counterCells扩展标识,锁被占用时不需要扩展
}
else if (!wasUncontended)//待研究。。。
    wasUncontended = true; 
//如果counterCells[(n - 1) & h]已被使用,则把计数累加到该CounterCell的value值
else if (U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))
    break;
//当counterCells大小大于CPU核数后,不再进行扩容
else if (counterCells != as || n >= NCPU)
    collide = false; 
else if (!collide)
    collide = true;
//如果更新CounterCell失败,则尝试加锁,然后扩容counterCells为原来的2倍
else if (cellsBusy == 0 &&
         U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
    try {
        if (counterCells == as) {
            CounterCell[] rs = new CounterCell[n << 1];//扩容后的大小为原来的2倍
            for (int i = 0; i < n; ++i)//将数据迁移到新的counterCells
                rs[i] = as[i];
            counterCells = rs;
        }
    } finally {
        cellsBusy = 0;//释放锁
    }
    collide = false;
    continue;               
}
h = ThreadLocalRandom.advanceProbe(h);
  • 如果counterCells没有初始化且没有加锁,则尝试对它进行加锁,如果加锁成功就初始化counterCells,大小为2;
else if (cellsBusy == 0 && counterCells == as &&
             U.compareAndSwapInt(this, CELLSBUSY, 0, 1)) {
        boolean init = false;
        try {
            if (counterCells == as) {
                //初始化大小为2的CounterCell数组
                CounterCell[] rs = new CounterCell[2];
                //将创建的CounterCell存入CounterCell数组,存放索引为0或者1
                rs[h & 1] = new CounterCell(x);
                counterCells = rs;
                init = true;
            }
        } finally {
            //释放锁
            cellsBusy = 0;
        }
        if (init)
            break;
    }
  • 如果counterCells正在进行初始化,则尝试直接累加到baseCount上;
else if (U.compareAndSwapLong(this, BASECOUNT, v = baseCount, v + x))
        break;

五、结语

本篇主要介绍了ConcurrentHashMap的基本原理,以及添加元素时如何保证一致性,写的比较零散。关于如何计数、如何获取元素、如何扩容有空再整理了。

by alen.wen

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值