ConcurrentHashMap结构及原理

前面我们学习了HashMap和Hashtable,因为多线程环境下,使用Hashmap进行put操作会引起死循环,导致CPU利用率接近100%,所以在并发情况下不能使用HashMap。

而Hashtable使用synchronized来保证线程安全,但在线程竞争激烈的情况下Hashtable的效率非常低下。因为当一个线程访问Hashtable的同步方法时,其他线程访问Hashtable的同步方法时,可能会进入阻塞或轮询状态。

jdk1.7中ConcurrentHashMap使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。有些方法需要跨段,比如size()和containsValue(),它们可能需要锁定整个表而而不仅仅是某个段,这需要按顺序锁定所有段,操作完毕后,又按顺序释放所有段的锁。

jdk1.7的ConcurrentHashMap实现如下:

ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment是一种可重入锁ReentrantLock,在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。ConcurrentHashMap完全允许多个读操作并发进行,读操作并不需要加锁。

jdk1.8的ConcurrentHashMap实现:

JDK1.8的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap。

首先看ConcurrentHashMap的部分源码:

public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
    implements ConcurrentMap<K,V>, Serializable {

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

    /**
     * The next table to use; non-null only while resizing.
     */
    private transient volatile Node<K,V>[] nextTable;
    ......
}

 

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;

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

注意这里用的volatile,而HashMap的Node没有使用这个。TreeNode继承与Node,但是数据结构换成了二叉树结构,它是红黑树的数据的存储结构,用于红黑树中存储数据,当链表的节点数大于8时会转换成红黑树的结构。

ConcurrentHashMap的初始化其实是一个空实现,并没有做任何事,这里后面会讲到,这也是和其他的集合类有区别的地方,初始化操作并不是在构造函数实现的,而是在put操作中实现。

get方法:

 1 public V get(Object key) {
 2         Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
 3         int h = spread(key.hashCode());
 4         if ((tab = table) != null && (n = tab.length) > 0 &&
 5             (e = tabAt(tab, (n - 1) & h)) != null) {
 6             if ((eh = e.hash) == h) {
 7                 if ((ek = e.key) == key || (ek != null && key.equals(ek)))
 8                     return e.val;
 9             }
10             else if (eh < 0)
11                 return (p = e.find(h, key)) != null ? p.val : null;
12             while ((e = e.next) != null) {
13                 if (e.hash == h &&
14                     ((ek = e.key) == key || (ek != null && key.equals(ek))))
15                     return e.val;
16             }
17         }
18         return null;
19     }

get方法的步骤:

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

put方法:

 1 public V put(K key, V value) {
 2         return putVal(key, value, false);
 3     }
 4 
 5     /** Implementation for put and putIfAbsent */
 6     final V putVal(K key, V value, boolean onlyIfAbsent) {
 7         if (key == null || value == null) throw new NullPointerException();
 8         int hash = spread(key.hashCode());
 9         int binCount = 0;
10         for (Node<K,V>[] tab = table;;) {
11             Node<K,V> f; int n, i, fh;
12             if (tab == null || (n = tab.length) == 0)
13                 tab = initTable();
14             else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
15                 if (casTabAt(tab, i, null,
16                              new Node<K,V>(hash, key, value, null)))
17                     break;                   // no lock when adding to empty bin
18             }
19             else if ((fh = f.hash) == MOVED)
20                 tab = helpTransfer(tab, f);
21             else {
22                 V oldVal = null;
23                 synchronized (f) {
24                     if (tabAt(tab, i) == f) {
25                         if (fh >= 0) {
26                             binCount = 1;
27                             for (Node<K,V> e = f;; ++binCount) {
28                                 K ek;
29                                 if (e.hash == hash &&
30                                     ((ek = e.key) == key ||
31                                      (ek != null && key.equals(ek)))) {
32                                     oldVal = e.val;
33                                     if (!onlyIfAbsent)
34                                         e.val = value;
35                                     break;
36                                 }
37                                 Node<K,V> pred = e;
38                                 if ((e = e.next) == null) {
39                                     pred.next = new Node<K,V>(hash, key,
40                                                               value, null);
41                                     break;
42                                 }
43                             }
44                         }
45                         else if (f instanceof TreeBin) {
46                             Node<K,V> p;
47                             binCount = 2;
48                             if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
49                                                            value)) != null) {
50                                 oldVal = p.val;
51                                 if (!onlyIfAbsent)
52                                     p.val = value;
53                             }
54                         }
55                     }
56                 }
57                 if (binCount != 0) {
58                     if (binCount >= TREEIFY_THRESHOLD)
59                         treeifyBin(tab, i);
60                     if (oldVal != null)
61                         return oldVal;
62                     break;
63                 }
64             }
65         }
66         addCount(1L, binCount);
67         return null;
68     }

put方法的步骤:

  1. 如果没有初始化就先调用initTable()方法来进行初始化过程
  2. 如果没有hash冲突就直接CAS插入
  3. 如果还在进行扩容操作就先进行扩容如果存在hash冲突,就加锁
    synchronized
    来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入
  4. 最后一个如果该链表的数量大于阈值8,就要先转换成黑红树的结构,break再一次进入循环
  5. 如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容

JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发。

 

转载于:https://www.cnblogs.com/jameszheng/p/10229529.html

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值