ConcurrentHashMap 线程安全的HashMap

基础描述

ConcurrentHashMap和HashMap的功能是基本一样的,ConcurrentHashMap是 HashMap 的线程安全版本,其内部与HashMap类似同样采用了数组(hash桶)链表/红黑树的方式来实现。

线程的安全性如何保障?在HashTable中是直接在 put 和 get 函数上加synchronized关键字,但是这么做锁的粒度太大(整个容器实例)非常影响并发性能。在JDK1.8中,ConcurrentHashMap采用了CAS与synchronized(在必须需要锁的时候仅对hashSlot的Node加synchronized)来实现对容器操作的线程安全保障。同时在ConcurrentHashMap中不允许key与value存储null值。

多并发下如何实现扩容:在ConcurrentHashMap中采用的是分段扩容法,即每个线程负责一段,默认最小是 16,也就是说如果ConcurrentHashMap中只有 16 个槽位,那么就只会有一个线程参与扩容。如果大于16则根据当前CPU数来进行分配,最大参与扩容线程数不会超过CPU数。在ConcurrenthashMap中通过sizeCtl状态来记录何时扩容以及参与扩容的线程个数。

结构定义

ConcurrentHashMap的结构与HashMap没有太多的区别,不同处在于slot中存储Node部分除本身Node的单链表结构外,比HashMap多了三个类型的节点,

分别是:ReservationNode,ForwardingNode,TreeBin.

//ConcurrentHashMap的slot中默认存储数据的节点,单链表,

//===>链表转换成红黑树的条件.

//=>1,static final int TREEIFY_THRESHOLD = 8; 单slot的hash冲突达到8

//=>2,static final int MIN_TREEIFY_CAPACITY = 64;Hash表总长度大于64.

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的一个子实现,在ConcurrentHashMap.compute系列函数判断记录不存在时

//====>初始插入,用于对节点进行占位.

static final class ReservationNode<K,V> extends Node<K,V> {

    ReservationNode() {

        super(RESERVED, null, null, null);

    }

    Node<K,V> find(int h, Object k) {

        return null;

    }

}

//Node的一个子实现,在ConcurrentHashMap进行扩容时slot的节点.

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;

    }

    Node<K,V> find(int h, Object k) {

        .......

    }

}

//Node的一个子实现,当Slot达到变为红黑树条件后,slot中存储的节点.

//==>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;

    .............

}

//Node的一个子实现,某个slot中红黑树存储的具体记录集

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;

    TreeNode(int hash, K key, V val, Node<K,V> next,

             TreeNode<K,V> parent) {

        super(hash, key, val, next);

        this.parent = parent;

    }

    ........................

}

//ConcurrentHashMap构造函数

//=>private static final int DEFAULT_CAPACITY = 16;默认容量16.

//=>private static final int MAXIMUM_CAPACITY = 1 << 30;最大容量(1073741824).

//注意:HashMap的容量值必须是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;

}

ConcurrentHashMap中记录与扩容相关的几个参数定义

//扩容戳,默认值16,

private static int RESIZE_STAMP_BITS = 16;

//对hash表数组进行扩容时,最大可扩容的线程数量

//值(65535),二进制:0000 0000 0000 0000 1111 1111 1111 1111

private static final int MAX_RESIZERS = (1 << (32 - RESIZE_STAMP_BITS)) - 1;

//扩容戳的位移位数,

private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS;

//(-1):表示hash表开始扩容,

//(-N):表示hash表正在扩容(低16位记录扩容的线程数,高16位记录扩容戳),

//(正数):表示当前hash表容量的0.75(即当size达到0.75时需要扩容)

private transient volatile int sizeCtl;

//hash表扩容时,单个线程处理hash槽位的歩福(即单个线程最小处理16个节点的迁移)

private static final int MIN_TRANSFER_STRIDE = 16;

初始化hash

ConcurrentHashMap的初始化通过initTable函数来实现,通过对sizeCtl的判断与CAS自旋操作来判断是否抢占到资源,如果sizeCtl小于0,说明其它线程正在进行初始化操作(让出CPU执行时间让其它线程处理),否则尝试通过CAS设置sizeCtl的值为-1,如果能成功设置为-1说明当前线程抢占到资源,对table进行初始化,在初始化时如果sizeCtl的值是大于0的值说明在构建ConcurrentHashMap实例时有配置初始化table容量大小,根据此值来初始化,否则按默认的DEFAULT_CAPACITY(16)来初始化table.

//sizeCtl变量:

//=>1, -1表示hash表正在初始化,

//=>2, 小于负1表示hash表正在扩容(高16位记录扩容戳,低16位记录正在扩容的线程数)

//=>3, 0表示初始状态.

//=>4, 大于0的值,说明当存储记录达到这个值时需要扩容(当前容量的0.75).

private transient volatile int sizeCtl;

//初始化hash表,通过CAS自旋锁来判断是否能成功设置sizeCtl的值为-1,(成功表示当前线程抢到资源)

private final Node<K,V>[] initTable() {

    Node<K,V>[] tab; int sc;

    //在hashTable还未初始化完成前,一直迭代(成功初始化或被其它线程初始化成功结束).

    while ((tab = table) == null || tab.length == 0) {

        //如果sizeCtl小于0,说明有其它线程正在进行hash表的初始化或者扩容操作,

        //==>此时当前线程应该让出CPU的执行时间给其它线程有充足的时间完成初始化.

        if ((sc = sizeCtl) < 0)

            Thread.yield(); // lost initialization race; just spin

        //这里表示没有其它线程竞争hash表的初始化操作,CAS设置sizeCtl的值为-1.

        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {

            try {

                if ((tab = table) == null || tab.length == 0) {

                    //在table为null时,sizeCtl如果大于0就是初始化table的容量,否则就是默认的16个.

                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;

                    @SuppressWarnings("unchecked")

                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];

                    table = tab = nt;

                    //scizeCtl = table.lenth -(table.length / 4)即:当前table容量的3/4

                    sc = n - (n >>> 2);

                }

            } finally {

                sizeCtl = sc;

            }

            break;

        }

    }

    return tab;

}

插入数据(put)

1,put主体流程

在ConccurrentHashMap中实现插入与HashMap类似,不同处在于考虑多线程的场景下的插入与size计数的实现上要复杂一些,插入操作具体包含:

1,先判断hashTable是否已经初始化,如果没有,先初始化hash表,并重新刷新table进行判断。

2,对key进行hash判断key对应hashTable的slot,如果slot位置为null时,直接CAS设置hashTable中此slot为一个根据记录(key,value)生成的Node节点作为链表的root节点.

3,如果slot已经存在节点记录,同时节点的hash值为MOVED(-1)说明hashTable正在扩容,执行helpTransfer函数帮助hashTable快速完成扩容迁移操作,在进入helpTransfer时如果扩容完成返回值是hashmap.table数组,否则返回hashmap.nextTable数组。

4,slot当前存储有Node节点,节点类型是链表节点(hash >= 0),迭代到链表的尾部插入当前记录(或者已经存在,替换原Node的值).

5,slot当前存储有Node节点,节点类型是TreeBin,(hash == -2),说明当前slot是红黑树存储,调用TreeBin.putTreeVal实现记录的插入或替换。

6,判断slot中存储的链表数量是否达到转换为红黑树存储的条件(nodeSize >= 8),如果达到转换成红黑树存储(如果hashTable的容量小于64时,会先扩容而不是转换红黑树)。

7,addCount对map的size记录加1,并判断是否需要扩容.

下面分析一下插入数据(put)的主体流程。

分析前,先看看ConcurrentHashMap中如何获取到hashTable中某个slot位置的node节点。

获取hashTable中某个slot位置的节点由tabAt函数实现。

static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {

    //"((long)i << ASHIFT)" 相当于"scale * i",

    //通过Unsafe获取到hashTable中指定下标的Node节点.

    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);

}

//Unsafe中关于Node数组操作的几个定义

Class<?> ak = Node[].class;

//获取取Node[]数组的内存地址偏移量

ABASE = U.arrayBaseOffset(ak);

//这里获取到数组每个元素的歩长(元素寻址的转换因子).

int scale = U.arrayIndexScale(ak);

//获取到每个元素寻址的左移偏移量(如scala是4(100),那么这里得到的结果是28)

ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);

pubVal的具体执行流程:

//ConcurrentHashMap中实现数据的插入

//对外暴露的put函数,其直接调用putVal函数来完成数据插入.

public V put(K key, V value) {

   return putVal(key, value, false);

}

//具体实现数据插入操作的核心流程,onlyIfAbsent:true表示存在不替换旧值,默认为false

final V putVal(K key, V value, boolean onlyIfAbsent) {

    //在ConcurrentHashMap中,插入数据的key与value不能为null.

   if (key == null || value == null) throw new NullPointerException();

    //对key的hash值,spread函数:"(hash ^ (hash >>> 16)) & 0x7fffffff"

    //=>把hash值的低16位与高16位进行异或计算然后与Integer.maxValue按位与.

    //=>这里确保了key.hashCode的值非负数,

    //=>同时因为hashTable的容量是2的N次方,进行XOR运算可有效减少hash冲突.

   int hash = spread(key.hashCode());

   int binCount = 0;

    //死循环,自旋,直到插入成功结束.

   for (Node<K,V>[] tab = table;;) {

       Node<K,V> f; int n, i, fh;

        //如果当前hashTable还未初始化,先对hashTable进行初始化(initTable)

       if (tab == null || (n = tab.length) == 0)

           tab = initTable();

        //获取hash取模后对应table中slot的Node节点,并赋值给临时变量f,

        //"f == null",直接CAS设置table数组对应slot,生成一个新的Node节点,成功后退出迭代.

        //"(n - 1) & hash" 用hash值的低位与数组长度减一按位与(其实就是取模)

        //"tabAt函数",获取table数组中指定下标的Node节点(保证可见性),(通过Unsafe来读取数组地址偏移量)

        //====> Unsafe来读取数组地址偏移量,可参考此函数去实现.        

       else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {

           //CAS设置对应slot为一个新生成的Node节点.因为slot没有存储任何节点,当前node就是链表的root节点.

           if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))

               break;  // no lock when adding to empty bin

       }

        //如果当前slot中root节点的hash值是"MOVED(-1)",说明节点正在进行迁移,

        //==>执行helpTransfer函数,帮助迁移,"(fh == MOVED(-1))"表示节点正在迁移.

        //==>helpTransfer函数返回值:

//====>1,如果进入函数时扩容已经完成,直接返回map.table数组

//====>2,否则参与扩容(扩容线程未达上限),并返回map.nextTable数组.

       else if ((fh = f.hash) == MOVED)

           tab = helpTransfer(tab, f);

       else {

           V oldVal = null;

           //"synchronized"关键字对slot中rootNode节点实现局部加锁,

           synchronized (f) {

               //"(tabAt(tab, i) == f)"

               //=>刷新slot中最新的root节点,检查是否变化(如链表转成了红黑树就需要重新迭代)

               if (tabAt(tab, i) == f) {

                   //"(fh >= 0)"  表示当前slot是链表结构存储.

                   if (fh >= 0) {

                       binCount = 1;

                       //从链表的root节点(f)开始,向下查找到节点插入的位置并插入节点. 

                       for (Node<K,V> e = f;; ++binCount) {

                           K ek; //当前迭代的节点(e)的key值.

                           //如果记录值存在,(key and hash相同),替换节点的value并退出迭代(内层迭代).

                           if (e.hash == hash &&

                               ((ek = e.key) == key ||

                                (ek != null && key.equals(ek)))) {

                               oldVal = e.val;

                               if (!onlyIfAbsent)

                                   e.val = value;

                               break;

                           }

                           //next为null时,表示链表尾部,在链表尾部插入当前记录并退出迭代(内层迭代).

                           Node<K,V> pred = e;

                           if ((e = e.next) == null) {

                               pred.next = new Node<K,V>(hash, key,value, null);

                               break;

                           }

                       }

                   }

                   //"(fh == TREEBIN(-2))" 表示红黑树节点,红黑树存储时slot位置存储的是TreeBin实例

                   else if (f instanceof TreeBin) {

                       Node<K,V> p;

                       binCount = 2;

                       //通过TreeBin.putTreeVal向红黑树插入记录. 

                       if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,value)) != null) {

                           oldVal = p.val;

                           if (!onlyIfAbsent)

                               p.val = value;

                       }

                   }

               }

           }

           //释放锁,因为插入动作已经完成.

           if (binCount != 0) {

               //判断当前slot中链表的存储记录集是否达到TREEIFY_THRESHOLD(8),

               //如果slot链表记录达到8个,并table.length小于MIN_TREEIFY_CAPACITY(64),扩容.

               //否则:对当前slot进行存储结构转换,把链表转换成红黑树.

               if (binCount >= TREEIFY_THRESHOLD)

                   treeifyBin(tab, i);

               if (oldVal != null)

                   return oldVal;

               break;

           }

       }

   }

   //流程执行这里说明记录是一条新插入的记录,返回值为null(没有oldValue)

   //"addCount" 此函数用于设置map的size计数(真实记录数),

//===>并根据slot的hash冲突判断是否需要对table进行扩容.

   addCount(1L, binCount);

   return null;

}

//对hash值的低16位与高16位进行XOR运算,并调整hash值为正数.

static final int spread(int h) {

    //"(h >>> 16)" 把hash值的高16位移动到低16位处.

    //"(h ^ (h >>> 16))"对低16位与高16位进行XOR运算。

    //"& HASH_BITS" 确保hash值是正数.

   return (h ^ (h >>> 16)) & HASH_BITS;

}

2,插入(红黑树)

在ConcurrentHashMap中与HashMap一样,当hashTable中某个slot的冲突达到8个同时hashTable的容量达到64个或以上,slot中存储记录的链表需要转换为红黑树结构存储,因此这里设计到一个从链表转换为红黑树,同时当slot存储的节点代表红黑树节点时,数据的插入也是向红黑树插入。这里主要分为三个部分:

1,链表结构转换为红黑树结构.

2,红黑树节点插入后树的平衡调整.

3,如果插入记录时slot对应的位置是红黑树,向红黑树插入节点.

a,treeifyBin(链表转红黑树)

treeifyBin函数的执行条件是:当前插入的slot中链表存储结构的数量达到8个.在slot插入记录后,链表数量达到8时(由TREEIFY_THRESHOLD控制),触发treeifyBin来进行处理(如下)。

在ConcurrentHahsMap中,slot位置如果是红黑树时存储的并不是TreeNode的root节点,而是一个TreeBin节点,此节点的hash值为-2(TREEBIN),TreeBin节点中维护有红黑树root节点的指针。

static final int TREEIFY_THRESHOLD = 8;

//putVal函数判断是否转换成红黑树

if (binCount != 0) {

    if (binCount >= TREEIFY_THRESHOLD)

        treeifyBin(tab, i);

    if (oldVal != null)

        return oldVal;

    break;

}

接下来分析一下treeifyBin函数的具体实现

在treeifyBin中,如果hashTable的容量小于64时,会通过tryPresize函数对hashTable进行扩容而不是把链表转换为红黑树,只有当hashTable的容量达到64个,同时单slot的hash冲突达到8个时,才会把链表转换为红黑树并把slot位置存储为一个TreeBin节点。

//当链表存储记录值达到8个时,将链表结构转换为红黑树结构存储.

//通过synchronized关键字对slot中存储的节点进行加锁来实现线程安全.

private final void treeifyBin(Node<K,V>[] tab, int index) {

    Node<K,V> b; int n, sc;

    if (tab != null) {

        //如果当前hashTable的容量小于64时,说明当前容量本身太低导致了hash冲突,先扩容.

        if ((n = tab.length) < MIN_TREEIFY_CAPACITY)

            tryPresize(n << 1);

        //获取到指定index在table中存储的node节点(同时节点必须是链表类型节点).

        else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {

            //对index的root节点(b)进行加锁,

            synchronized (b) {

                //重新刷新节点,检查index位置链表的root节点是否发生变化.

                if (tabAt(tab, index) == b) {

                    //顺序从root节点迭代链表,依次添加到红黑树中(此时红黑树特性不满足).

                    //节点(hd)是链表的root节点,节点(tl)是当前迭代节点(p)的前继节点

                    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;

                    }

                    //生成一个TreeBin节点(调整红黑树结构使其满足红黑树特性)重新放回到index的位置.

                    setTabAt(tab, index, new TreeBin<K,V>(hd));

                }

            }//退出同步块

        }

    }

}

TreeBin的构造函数:

此函数作用除了构造一个存储在hashTable对应slot位置的节点外,还需要负责在TreeNode初始化时重新调整树的结构,使其满足红黑树的特性。插入红黑树节点后的平衡调整由balanceInsertion函数实现。

//把一个链表形式的红黑树进行调整并使其满足红黑树的特性.

TreeBin(TreeNode<K,V> b) {

    //TreeBin节点的hash值为TREEBIN(-2) 

    super(TREEBIN, null, null, null);

    //把链表的root节点设置为TreeBin的first节点.

    this.first = b;

    //红黑树的root节点.

    TreeNode<K,V> r = null;

    //从链表头节点开始迭代链表,构建红黑树

    for (TreeNode<K,V> x = b, next; x != null; x = next) {

        next = (TreeNode<K,V>)x.next;

        x.left = x.right = null;

        //step1,第一次迭代先把第一个节点设置为红黑树的root节点(颜色为黑色)

        if (r == null) {

            x.parent = null;

            x.red = false;

            r = x;

        }

        //step2,红黑树已经构建root节点,继续迭代后续后续节点向红黑树插入节点.

        else {

            K k = x.key;

            int h = x.hash;

            Class<?> kc = null;

            //向红黑树中插入节点(x),要实现插入操作,需要从root开始向左或向右查找插入位置.

            for (TreeNode<K,V> p = r;;) {

                int dir, ph;

                K pk = p.key;

                //先得到节点(x要插入的方向)

                //=>1,先比较插入节点(x)的hash与树当前节点(p)的hash值,

                //=>2,如果hash相同,再比较 插入节点(x)的key与树当前节点(p)的key值

                //=>3,如果key(comparable)也相同,通过"System.identityHashCode"比较两个对象的hash值.

                if ((ph = p.hash) > h)

                    dir = -1;

                else if (ph < h)

                    dir = 1;

                else if ((kc == null &&

                          (kc = comparableClassFor(k)) == null) ||

                         (dir = compareComparables(kc, k, pk)) == 0)

                    dir = tieBreakOrder(k, pk);

                //"(p = (dir <= 0) ? p.left : p.right)"

                //==>根据插入方向(dir)开始让节点(p)向左或向右移动.  

                TreeNode<K,V> xp = p;

                if ((p = (dir <= 0) ? p.left : p.right) == null) {

                    //如果当前迭代节点(p)已经是树的叶节点,就在这个位置插入节点..

                    x.parent = xp;

                    if (dir <= 0)

                        xp.left = x;

                    else

                        xp.right = x;

                    //插入节点成功,调整树的平衡,得到调整后的新root节点(r)

                    r = balanceInsertion(r, x);

                    break;

                }

            }

        }

    }

    //设置TreeBin对root节点的引用.

    this.root = r;

    assert checkInvariants(root);

}

b.balanceInsertion(平衡调整)

在红黑树节点插入时进行平衡调整总共有4个分支,其中两个分支是红黑树不超过二层的情况下,这种情况比较简单(见下面代码的step2与step3),另外两个分支每个分支对应三种场景,其中step5与step4是相对称的操作。

//红黑树节点插入后的树平衡处理,传入参数:root表示当前树的根节点,x表示当前插入节点.

static <K,V> TreeNode<K,V> balanceInsertion(TreeNode<K,V> root, TreeNode<K,V> x) {

    //step1,插入节点默认是红色.

    x.red = true;

    //xp=x.parent,xpp=xp.parent,xppl=xp.parent.left,xppr=xp.parent.right

    for (TreeNode<K,V> xp, xpp, xppl, xppr;;) {

        //step2,如果关注节点(x)没有父节点,直接设置节点为黑色并返回此节点(root).

        if ((xp = x.parent) == null) {

            x.red = false;

            return x;

        }

        //step3

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值