摘要
在涉及到Java多线程开发时,如果我们使用HashMap可能会导致死锁问题,使用HashTable效率又不高。而ConcurrentHashMap既可以保持同步也可以提高并发效率,所以这个时候ConcurrentHashmap是我们最好的选择。
为什么使用ConcurrentHashMap
- 在多线程环境中使用HashMap的put方法有可能导致程序死循环,因为多线程可能会导致HashMap形成环形链表,即链表的一个节点的next节点永不为null,就会产生死循环。这时,CPU的利用率接近100%,所以并发情况下不能使用HashMap。
- HashTable通过使用synchronized保证线程安全,但在线程竞争激烈的情况下效率低下。因为当一个线程访问HashTable的同步方法时,其他线程只能阻塞等待占用线程操作完毕。
- ConcurrentHashMap使用分段锁的思想,对于不同的数据段使用不同的锁,可以支持多个线程同时访问不同的数据段,这样线程之间就不存在锁竞争,从而提高了并发效率。
简介
在阅读ConcurrentHashMap的源码时,有一段相关描述。
The primary design goal of this hash table is to maintain concurrent readability(typically method get(), but also iterators and related methods) while minimizing update contention. Secondary goals are to keep space consumption about the same or better than java.util.HashMap, and to support high initial insertion rates on an empty table by many threads.
大致意思就是:
ConcurrentHashMap的主要设计目的是保持并发的可读性(通常是指的get()方法的使用,同时也包括迭代器和相关方法),同时最小化更新征用(即在进行插入操作或者扩容时也可以保持其他数据段的访问)。第二个目标就是在空间利用方面保持与HashMap一致或者更好,并且支持多线程在空表的初始插入速率。
Java7与Java8中的ConcurrentHashMap:
在ConcurrentHashMap中主要通过锁分段技术实现上述目标。
在Java7中,ConcurrentHashMap由Segment数组结构和HashEntry数组组成。Segment是一种可重入锁,是一种数组和链表的结构,一个Segment中包含一个HashEntry数组,每个HashEntry又是一个链表结构。正是通过Segment分段锁,ConcurrentHashMap实现了高效率的并发。
在Java8中,ConcurrentHashMap去除了Segment分段锁的数据结构,主要是基于CAS操作保证保证数据的获取以及使用synchronized关键字对相应数据段加锁实现了主要功能,这进一步提高了并发性。同时同时为了提高哈希碰撞下的寻址性能,Java 8在链表长度超过一定阈值(8)时将链表(寻址时间复杂度为O(N))转换为红黑树(寻址时间复杂度为O(long(N)))。
Java8中ConcurrentHashMap的结构
在Java8中,ConcurrentHashMap弃用了Segment类,但是保留了Segment属性,用于序列化。目前ConcurrentHashMap采用Node类作为基本的存储单元,每个键值对(key-value)都存储在一个Node中。同时Node也有一些子类,TreeNodes用于树结构中(当链表长度大于8时转化为红黑树);TreeBins用于维护TreeNodes。当链表转树时,用于封装TreeNode。也就是说,ConcurrentHashMap的红黑树存放的是TreeBin,而不是treeNode;ForwordingNodes是一个重要的结构,它用于ConcurrentHashMap扩容时,是一个标志节点,内部有一个指向nextTable的属性,同时也提供了查找的方法;
1 | 复制代码static class Node<K,V> implements Map.Entry<K,V> { |
处理Node之外,Node的一个子类ForwardingNodes也是一个重要的结构,它主要作为一个标记,在处理并发时起着关键作用,有了ForwardingNodes,也是ConcurrentHashMap有了分段的特性,提高了并发效率。
1 | 复制代码 /** |
ConcurrentHashMap中的原子操作
在ConcurrentHashMap中通过原子操作查找元素、替换元素和设置元素。这些原子操作起着非常关键的作用,你可以在所有ConcurrentHashMap的基本功能中看到它们的身影。
1 | 复制代码 static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) { |
ConcurrentHashMap的功能实现
1.ConcurrentHashMap初始化
在介绍初始化之前先介绍一个重要的参数sizeCtl,含义如下:
1 | 复制代码 /** |
这个参数起到一个控制标志的作用,在ConcurrentHashMap初始化和扩容都有用到。
ConcurrentHashMap构造函数只是设置了一些参数,并没有对Hash表进行初始化。当在从插入元素时,才会初始化Hash表。在开始初始化的时候,首先判断sizeCtl的值,如果sizeCtl < 0,说明有线程在初始化,当前线程便放弃初始化操作。否则,将SIZECTL设置为-1,Hash表进行初始化。初始化成功以后,将sizeCtl的值设置为当前的容量值。
1 | 复制代码 /** |
2.确定元素在Hash表的索引
通过hash算法可以将元素分散到哈希桶中。在ConcurrentHashMap中通过如下方法确定数组索引:
第一步:
1 | 复制代码static final int spread(int h) { |
第二步:(length-1) & (h ^ (h >>> 16)) & HASH_BITS);
ConcurrentHashMap的put方法
- 如果key或者value为null,则抛出空指针异常;
- 如果table为null或者table的长度为0,则初始化table,调用initTable()方法。
- 计算当前键值的索引位置,如果Hash表中当前节点为null,则将元素直接插入。(注意,这里使用的就是前面锁说的CAS操作)
- 如果当前位置的节点元素的hash值为-1,说明这是一个ForwaringNodes节点,即正在进行扩容。那么当前线程加入扩容。
- 当前节点不为null,对当前节点加锁,将元素插入到当前节点。在Java8中,当节点长度大于8时,就将节点转为树的结构。
1 | 复制代码 /** Implementation for put and putIfAbsent */ |
ConcurrentHashMap的扩容机制
当ConcurrentHashMap中元素的数量达到cap * loadFactor时,就需要进行扩容。扩容主要通过transfer()方法进行,当有线程进行put操作时,如果正在进行扩容,可以通过helpTransfer()方法加入扩容。也就是说,ConcurrentHashMap支持多线程扩容,多个线程处理不同的节点。
- 开始扩容,首先计算步长,也就是每个线程分配到的扩容的节点数(默认是16)。这个值是根据当前容量和CPU的数量来计算(stride = (NCPU > 1) ? (n >>> 3) / NCPU : n),最小是16。
- 接下来初始化临时的Hash表nextTable,如果nextTable为null,初始化nextTable长度为原来的2倍;
- 通过计算出的步长开始遍历Hash表,其中坐标是通过一个原子操作(compareAndSetInt)记录。通过一个while循环,如果在一个线程的步长内便跳过此节点。否则转下一步;
- 如果当前节点为空,之间将此节点在旧的Hash表中设置为一个ForwardingNodes节点,表示这个节点已经被处理过了。
- 如果当前节点元素的hash值为MOVED(f.hash == -1),表示这是一个ForwardingNodes节点,则直接跳过。否则,开始重新处理节点;
- 对当前节点进行加锁,在这一步的扩容操作中,重新计算元素位置的操作与HashMap中是一样的,即当前元素键值的hash与长度进行&操作,如果结果为0则保持位置不变,为1位置就是i+n。其中进行处理的元素是最后一个符合条件的元素,所以扩容后可能是一种倒序,但在Hash表中这种顺序也没有太大的影响。
- 最后如果是链表结构直接获得高位与低位的新链表节点,如果是树结构,同样计算高位与低位的节点,但是需要根据节点的长度进行判断是否需要转化为树的结构。
1 | 复制代码 /** |
ConcurrentHashMap的get方法
ConcurrentHashMap的get方法就是从Hash表中读取数据,而且与扩容不冲突。该方法没有同步锁。
- 通过键值的hash计算索引位置,如果满足条件,直接返回对应的值;
- 如果相应节点的hash值小于0 ,即该节点在进行扩容,直接在调用ForwardingNodes节点的find方法进行查找。
- 否则,遍历当前节点直到找到对应的元素。
1 | 复制代码/** |
总结
ConcurrentHashMap就介绍到这里,以上主要是对ConcurrentHashMap中经常使用到的特性进行分析,如果对其他内容感兴趣,可以阅读相应的源码。
本文转载自: 掘金