并发容器,深入理解CurrentHashMap分段锁,源代码剖析

本文深入剖析了CurrentHashMap的并发原理,重点讲解了其分段锁机制。在JDK1.7中,CurrentHashMap使用Segment分段锁,插入操作时对每个Segment加锁;而在JDK1.8中,锁的粒度更细,仅针对单个节点使用同步锁。文章详细介绍了两个版本的初始化、插入操作及扩容策略,并提到了与HashMap和HashTable的区别。

CurrentHashMap

20201228

15:03

目录

-

- 1. 引言

- 2. 分段锁数据结构

- 3. JDK1.7实现

    - 3.1 初始化

    - 3.2 插入

- 4. JDK1.8实现

    - 4.1 初始化

    - 4.2 插入

- 5. REF

 

 

1. 引言

CurrentHashMap(CHM)是并发容器的代表,是一种实现细粒度锁提高并发性能的容器。

它的核心思想是分段锁,即缩小每次锁的数据的数量,从而提高并发性能。

话又说回来,我回想锁一大堆机制,其实本质上应该不是一件复杂的事。类比一下,这件事类似于几人搬箱子(假设一个人只能搬一个),提高效率的方式:

  • 要么你把箱子拆成多份(细粒度的锁)
  • 要么一个人搬,其他人就在旁边等等,不要休息(因为休息阻塞会花费大量的时间,乐观锁就是基于自旋等待,假设自旋很快)

CurrentHashMap在jdk1.7和1.8之间发生了较大的变化,但事实上和HashMap的实现很像。JDK1.7引入了segment的二阶段数据结构,每次只对segment加锁,JDK1.8则和hashMap非常像,只是它加锁只加hash值指向的节点。

2. 分段锁数据结构

这是CHM的灵魂,不过下图其实是JDK1.7的实现,而JDK1.8并没有这种显示的两阶段数据结构,但是思想是一样的。

 

3. JDK1.7实现

3.1 初始化

  • 1.7 是创建立马初始化,1.8是put的时候进行初始化
  • 初始化主要是初始segment表和一些计算hash用的标志,掩码等

    1 public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {

    2     // 参数校验

    3     if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)

    4         throw new IllegalArgumentException();

    5     // 校验并发级别大小,大于 1<<16,重置为 65536

    6     if (concurrencyLevel > MAX_SEGMENTS)

    7         concurrencyLevel = MAX_SEGMENTS;

    8     // Find power-of-two sizes best matching arguments

    9     // 2的多少次方

   10     int sshift = 0;

   11     int ssize = 1;

   12     // 这个循环可以找到 concurrencyLevel 之上最近的 2的次方值

   13     while (ssize < concurrencyLevel) {

   14         ++sshift;

   15         ssize <<= 1;

   16     }

   17     // 记录段偏移量,计算hash,mod用

   18     this.segmentShift = 32 - sshift;

   19     // 记录段掩码

   20     this.segmentMask = ssize - 1;

   21     // 设置容量

   22     if (initialCapacity > MAXIMUM_CAPACITY)

   23         initialCapacity = MAXIMUM_CAPACITY;

   24     // c = 容量 / ssize ,默认 16 / 16 = 1,这里是计算每个 Segment 中的类似于 HashMap 的容量

   25     int c = initialCapacity / ssize;

   26     if (c * ssize < initialCapacity)

   27         ++c;

   28     int cap = MIN_SEGMENT_TABLE_CAPACITY;

   29     //Segment 中的类似于 HashMap 的容量至少是2或者2的倍数

   30     while (cap < c)

   31         cap <<= 1;

   32     // create segments and segments[0]

   33     // 创建 Segment 数组,设置 segments[0]

   34     Segment<K,V> s0 = new Segment<K,V>(loadFactor, (int)(cap * loadFactor),

   35                          (HashEntry<K,V>[])new HashEntry[cap]);

   36     Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];

   37     UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]

   38     this.segments = ss;

   39 }

3.2 插入

对于整个map的插入 由于是分段的数据结构,逻辑就是

  • 获得分段位置
  • 往分段位置的hash表中插入

    1 public V put(K key, V value) {

    2     Segment<K,V> s;

    3     if (value == null)

    4         throw new NullPointerException();

    5     int hash = hash(key);

    6     // hash 值无符号右移 28位(初始化时获得),然后与 segmentMask=15 做与运算

    7     // 其实也就是把高4位与segmentMask(1111)做与运算

    8     int j = (hash >>> segmentShift) & segmentMask;

    9     if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck

   10          (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment

   11         // 如果查找到的 Segment 为空,初始化

   12         s = ensureSegment(j);

   13     return s.put(key, hash, value, false);

   14 }

分段表初始化过程需要一些加锁,保证不要重复初始化或数据丢失 这里边应用了双重加锁,以及乐观锁标志的技巧

    1 private Segment<K,V> ensureSegment(int k) {

    2     final Segment<K,V>[] ss = this.segments;

    3     long u = (k << SSHIFT) + SBASE; // raw offset

    4     Segment<K,V> seg;

    5     // 判断 u 位置的 Segment 是否为null

    6     if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {

    7         Segment<K,V> proto = ss[0]; // use segment 0 as prototype

    8         // 获取0号 segment 里的 HashEntry<K,V> 初始化长度

    9         int cap = proto.table.length;

   10         // 获取0号 segment 里的 hash 表里的扩容负载因子,所有的 segment 的 loadFactor 是相同的

   11         float lf = proto.loadFactor;

   12         // 计算扩容阀值

   13         int threshold = (int)(cap * lf);

   14         // 核心代码,创建一个 cap 容量的 HashEntry 数组

   15         HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];

   16         if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck

   17             // 再次检查 u 位置的 Segment 是否为null,因为这时可能有其他线程进行了操作

   18             Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);

   19             // 自旋检查 u 位置的 Segment 是否为null

   20             while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))

   21                    == null) {

   22                 // 使用CAS 赋值,只会成功一次

   23                 if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))

   24                     break;

   25             }

   26         }

   27     }

   28     return seg;

   29 }

segment位置上的插入,采用lock,核心任务

  • 插入
  • 检测是否覆盖
  • 扩容

    1 final V put(K key, int hash, V value, boolean onlyIfAbsent) {

    2     // 获取 ReentrantLock 独占锁,获取不到,scanAndLockForPut 获取。

    3     HashEntry<K,V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);

    4     V oldValue;

    5     try {

    6         HashEntry<K,V>[] tab = table;

    7         // 计算要put的数据位置

    8         int index = (tab.length - 1) & hash;

    9         // CAS 获取 index 坐标的值

   10         HashEntry<K,V> first = entryAt(tab, index);

   11         for (HashEntry<K,V> e = first;;) {

   12             if (e != null) {

   13                 // 检查是否 key 已经存在,如果存在,则遍历链表寻找位置,找到后替换 value

   14                 K k;

   15                 if ((k = e.key) == key ||

   16                     (e.hash == hash && key.equals(k))) {

   17                     oldValue = e.value;

   18                     if (!onlyIfAbsent) {

   19                         e.value = value;

   20                         ++modCount;

   21                     }

   22                     break;

   23                 }

   24                 e = e.next;

   25             }

   26             else {

   27                 // first 有值没说明 index 位置已经有值了,有冲突,链表头插法。

   28                 if (node != null)

   29                     node.setNext(first);

   30                 else

   31                     node = new HashEntry<K,V>(hash, key, value, first);

   32                 int c = count + 1;

   33                 // 容量大于扩容阀值,小于最大容量,进行扩容

   34                 if (c > threshold && tab.length < MAXIMUM_CAPACITY)

   35                     rehash(node);

   36                 else

   37                     // index 位置赋值 node,node 可能是一个元素,也可能是一个链表的表头

   38                     setEntryAt(tab, index, node);

   39                 ++modCount;

   40                 count = c;

   41                 oldValue = null;

   42                 break;

   43             }

   44         }

   45     } finally {

   46         unlock();

   47     }

   48     return oldValue;

   49 }

 

4. JDK1.8实现

1.8的实现和HashMap基本一致,只是加锁针对单个节点加锁,使用的是同步锁(对比HashTable,它是对整个方法使用同步,另外,看源码发现,HashTable并不是红黑树实现的)。

4.1 初始化

  • CAS完成初始化的过程,经典的设计,它通过控制sizeCtl作为悲观锁控制初始化独占
  • sizeCtl值经过巧妙的设计的标记量,它表示初始大小,节点数量,以及小于0是标志有线程在初始化,经典设计

 

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

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

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

    4         // 判断是否可以初始化

    5         if ((sc = sizeCtl) < 0)

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

      7         // CAS修改初始化标志量

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

    9             try {

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

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

   12                     @SuppressWarnings("unchecked")

   13                     // 经典设计,先new再赋值成员引用

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

   15                     table = tab = nt;

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

   17                 }

   18             } finally {

   19                 sizeCtl = sc;

   20             }

   21             break;

   22         }

   23     }

   24     return tab;

   25 }

4.2 插入

参考hashMap的源码解析,基本作的事情一致:

  • 计算hash值
  • 找到位置,初始化或插入
  • 是否覆盖
  • 转红黑树?
  • 扩容

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

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

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

    4     int binCount = 0;

    5     for (Node<K,V>[] tab = table;;) { // 经典风格,for完成循环,可以作初始化

     6         Node<K,V> f; int n, i, fh; // 经典风格,局部变量统一初始化,一行

    7         // Node表为空,调用初始化

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

    9             tab = initTable();

   10         // hash指向位置链表为空,初始化,CAS在该位置上放置节点

   11         else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {// 经典风格,判断带赋值

   12             if (casTabAt(tab, i, null,

   13                             new Node<K,V>(hash, key, value, null)))

   14                 break;                   // no lock when adding to empty bin

   15         }

   16         else if ((fh = f.hash) == MOVED)// 不懂,忽略

   17             tab = helpTransfer(tab, f);

   18         // hash指向位置非空

   19         else {

   20             V oldVal = null;

   21             // 同步锁开始插入,f是hash位置的节点

   22             synchronized (f) {

   23                 // hash位置还是刚刚的头部节点(未转为树,多线程情况下,从f取值到这里可能被转为树)

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

   25                     if (fh >= 0) {

   26                         binCount = 1;

   27                         // 开始遍历链表,覆盖,插入

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

   29                             K ek;

   30                             if (e.hash == hash &&

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

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

   33                                 oldVal = e.val;

   34                                 if (!onlyIfAbsent)

   35                                     e.val = value;

   36                                 break;

   37                             }

   38                             Node<K,V> pred = e;

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

   40                                 pred.next = new Node<K,V>(hash, key,

   41                                                             value, null);

   42                                 break;

   43                             }

   44                         }

   45                     }

   46                     // hash指向位置是红黑树节点,则使用红黑树插入

   47                     else if (f instanceof TreeBin) {

   48                         Node<K,V> p;

   49                         binCount = 2;

   50                         if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,

   51                                                         value)) != null) {

   52                             oldVal = p.val;

   53                             if (!onlyIfAbsent)

   54                                 p.val = value;

   55                         }

   56                     }

   57                 }

   58             }

   59             // bitCount链表记录长度,看看是否转换为红黑树

   60             if (binCount != 0) {

   61                 if (binCount >= TREEIFY_THRESHOLD)

   62                     treeifyBin(tab, i);

   63                 if (oldVal != null)

   64                     return oldVal;

   65                 break;

   66             }

   67         }

   68     }

   69     // 增加计数,是否扩容rehash,binCount作为标记,如果大于零,才表明插入新元素

   70     addCount(1L, binCount);

   71     return null;

   72 }

 

5. REF

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值