前言
在HashMap的基础上,继续学习ConcurrentHashMap,其利用到基本原理是,将哈希表中的锁竞争分段到每一个节点。而并不是将整个哈希表锁住。
在JDK8之前,ConcurrentHashMap 的实现是让每一个节点持有一个锁,然后当访问对应节点的时候,则使用这个锁只锁住一个节点而不是整个哈希表。这个理念与Innodb希望用行锁取代表锁提高并发量很类似。
static class Segment<K,V> extends ReentrantLock implements Serializable {
private static final long serialVersionUID = 2249069246763182397L;
final float loadFactor;
Segment(float lf) {
this.loadFactor = lf; }
}
可以看出这个对象实现了 ReentrantLock,那么其本身就是一把锁。
不过在JDK8及其之后,由于JVM本身就对于 synchronized 进行了锁优化,有可以从锁消除,偏向锁,轻量级锁,重量级锁升级过程,在没有竞争的场景下,偏向锁和轻量级锁的性能已经与 ReentrantLock 差不多甚至可能会更好,那么于是jdk8源码中就直接使用 synchronized 来取代了 Segment 的设计。
一、数据结构
学习了HashMap之后,可以知道 ConcurrentHashMap 的底层数据结构也是一个数组。
其结构与HashMap的数据结构几乎一样,甚至我会以为代码就是copy过来的。
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
volatile V val;
volatile Node<K,V> next;
}
/**
* The array of bins. Lazily initialized upon first insertion.
* Size is always a power of two. Accessed directly by iterators.
*/
transient volatile Node<K,V>[] table;
/**
* The next table to use; non-null only while resizing.
*/
private transient volatile Node<K,V>[] nextTable;
但是在数据的结构上面有一点区别,区别在于现在底层的数组表时由两个数组构成而不是一个。原因在于ConcurrentHashMap 是一个并发支持的数据结构,可同时支持数据的update和query,那么如果有多个线程同时对ConcurrentHashMap进行插入和查找,并且在这时发生了rehash,那么其实如果只有一个表,query是会查询到脏数据。
如果这个我们访问1,4时,应该使用原来的索引还是新的索引呢?
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;
}
}
static final int MOVED = -1; // hash for forwarding nodes
一个特殊节点,当正在发生rehash时,会通过这个节点替换原有的节点,并通过这个节点可以指向新的哈希表节点。
前向节点的哈希值固定为-1。
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;
}
}
一个特殊节点,在使用compute函数时的占位节点,固定哈希值为-3
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;
// values for lockState
static final int WRITER = 1; // set while holding write lock
static final int WAITER = 2; // set when waiting for write lock
static final int READER = 4; // increment value for setting read lock
}
一个红黑树的出口节点,这个节点会持有红黑树的root。并且会在这里进行一些红黑树的无锁并发操作。
二、基本常量
/**
* Base counter value, used mainly when there is no contention,
* but also as a fallback during table initialization
* races. Updated via CAS.
*/
private transient volatile long baseCount;
/**
* Table initialization and resizing control. When negative, the
* table is being initialized or resized: -1 for initialization,
* else -(1 + the number of active resizing threads). Otherwise,
* when table is null, holds the initial table size to use upon
* creation, or 0 for default. After initialization, holds the
* next element count value upon which to resize the table.
*/
private transient volatile int sizeCtl;
/**
* The next table index (plus one) to split while resizing.
*/
private transient volatile int transferIndex;
/**
* Table of counter cells. When non-null, size is a power of 2.
*/
private transient volatile CounterCell[] counterCells;
- baseCount是哈希表中一个基本的计数的变量,哈希表在没有竞争的时候,会将递增的数字写入到baseCount中,但是如果在写入的时候发生了竞争关系,那么其实就不会将增长的数量写入到 baseCount 而是使用其他的策略写入
- sizeCtl 是在哈希表中的尺寸控制,如果在哈希表中处于正常的操作状态,那么sizeCtl就是哈希表的容量,而如果哈希表为负值,那么就表明当前的哈希表正在进行扩容阶段。高16位会保存rehash的一个校验指纹,而低16位会保存当前正在rehash的线程数
- transferIndex 是哈希表在扩容阶段时将要rehash的游标,每个线程会从这个游标上获取一个范围
- counterCells 是用于并发竞争场景下,计算哈希表节点数量的一个额外数据结构,如果在数量增长的时候未发生竞争,那么就跟这个变量没有关系,但是如果发生了竞争,则会将增长的数量随机写入到一个CounterCell中去,随机写入到一个CounterCell的逻辑其实也是类似于一个分段锁的概念
三、基本操作
1.get查询
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 通过 spread 函数将哈希码的高位扩散到低位,与HashMap的计算方式很类似
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 首先查找普通节点,如果直接找到其键了那么直接返回
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.