Java并发容器—ConcurrentHashMap
1.JDK1.7版本
jdk1.7的实现结构图如下所示
[外链图片转存失败(img-Fy93gKVd-1566009849146)(/home/shidongxuan/.config/Typora/typora-user-images/1566002052129.png)]
CurrentHashMap是由Segment数组和HashEntry数组结构组成. Segment是一种可重入锁(ReentrantLock), 在ConcurrentHashMap里扮演锁的角色, HashEntry用于存储键值对数据. 综上, ConcurrentHashMap采用了分段锁技术, 其中Segment继承与ReentrantLock. 不像HashTable那样不管是put操作还是get操作都需要做同步处理, 理论上ConcurrentHashMap支持段级别的线程并发, 当一个线程占用锁访问一个Segment时, 不会影响其他Segment
1>put方法
//先定位到Segment, 之后在对应的Segment上进行具体的put
public V put(K key, V value) {
Segment<K,V> s;
if (value == null)
throw new NullPointerException();
int hash = hash(key);
int j = (hash >>> segmentShift) & segmentMask;
if ((s = (Segment<K,V>)UNSAFE.getObject // nonvolatile; recheck
(segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
s = ensureSegment(j);
return s.put(key, hash, value, false);
}
//具体的put
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//先尝试对segment加锁, 如果加锁成功, 那么node=null,如果加锁失败则调用
//scanAndLockForPut方法去获取锁, 这个方法中, 获取锁后会返回对应的HashEntry
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
//优化点, 这时已经加了锁,只会有一个线程访问table, 但是table是volatile修饰的
//所以将其赋值给一个局部变量, 减少volatile带来的开销
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
//获取到对应位置上的HashEntry链表
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
if (e != null) {
//e不为空, 说明出现了hash冲突, 遍历当前链表
//如果找到一个key相同的, 则根据onlyIfAbsent判断是否覆盖value值
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
else {
//说明e为空, 一种可能是在上面遍历到最后没有找到key相同的, 也可能是一开始就是null
//第一次没有获取到锁, 通过scanAndLockForPut方法获取到了锁,放到敌营位置上
if (node != null)
node.setNext(first);
else
//否则新new出一个节点对象
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;//节点数+1
//判断是否需要扩容
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
//数组无须扩容, 就直接插入node到指定index位置
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
下面是获取锁的scanAndLockForPut方法
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
//如果尝试加锁失败,那么就对segment[hash]对应的链表进行遍历找到需要put的这个entry所在的链表中的位置,
//这里之所以进行一次遍历找到坑位,主要是为了通过遍历过程将遍历过的entry全部放到CPU高速缓存中,
//这样在获取到锁了之后,再次进行定位的时候速度会十分快,这是在线程无法获取到锁前并等待的过程中的一种预热方式。
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
//获取锁失败,初始时retries=-1必然开始先进入第一个if
if (retries < 0) {//<1>
if (e == null) { //<1.1>
//e=null代表两种意思,第一种就是遍历链表到了最后,仍然没有发现指定key的entry;
//第二种情况是刚开始时确实太过entryForHash找到的HashEntry就是空的,即通过hash找到的table中对应位置链表为空
//当然这里之所以还需要对node==null进行判断,是因为有可能在第一次给node赋值完毕后,然后预热准备工作已经搞定,
//然后进行循环尝试获取锁,在循环次数还未达到<2>以前,某一次在条件<3>判断时发现有其它线程对这个segment进行了修改,
//那么retries被重置为-1,从而再一次进入到<1>条件内,此时如果再次遍历到链表最后时,因为上一次遍历时已经给node赋值过了,
//所以这里判断node是否为空,从而避免第二次创建对象给node重复赋值。
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))//<1.2> 遍历过程发现链表中找到了我们需要的key的坑位
retries = 0;
else//<1.3> 当前位置对应的key不是我们需要的,遍历下一个
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {//<2>
// 尝试获取锁次数超过设置的最大值,直接进入阻塞等待,这就是所谓的有限制的自旋获取锁,
//之所以这样是因为如果持有锁的线程要过很久才释放锁,这期间如果一直无限制的自旋其实是对系统性能有消耗的,
//这样无限制的自旋是不利的,所以加入最大自旋次数,超过这个次数则进入阻塞状态等待对方释放锁并获取锁。
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {//<3>
// 遍历过程中,有可能其它线程改变了遍历的链表,这时就需要重新进行遍历了。
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
其核心思想就是通过MAX_SCAN_RETRIES控制自旋次数, 防止无限制的自旋和浪费资源. 这个方法的作用就是遍历获取所然后进行数据插入
2>rehash方法
/**
* Doubles size of table and repacks entries, also adding the
* given node to new table
* 对数组进行扩容,由于扩容过程需要将老的链表中的节点适用到新数组中,所以为了优化效率,可以对已有链表进行遍历,
* 对于老的oldTable中的每个HashEntry,从头结点开始遍历,找到第一个后续所有节点在新table中index保持不变的节点fv,
* 假设这个节点新的index为newIndex,那么直接newTable[newIndex]=fv,即可以直接将这个节点以及它后续的链表中内容全部直接复用copy到newTable中
* 这样最好的情况是所有oldTable中对应头结点后跟随的节点在newTable中的新的index均和头结点一致,那么就不需要创建新节点,直接复用即可。
* 最坏情况当然就是所有节点的新的index全部发生了变化,那么就全部需要重新依据k,v创建新对象插入到newTable中。
*/
@SuppressWarnings("unchecked")
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
int newCapacity = oldCapacity << 1;
threshold = (int)(newCapacity * loadFactor);
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
for (int i = 0; i < oldCapacity ; i++) {
HashEntry<K,V> e = oldTable[i];
if (e != null) {
HashEntry<K,V> next = e.next;
int idx = e.hash & sizeMask;
if (next == null) // Single node on list 只有单个节点
newTable[idx] = e;
else { // Reuse consecutive sequence at same slot
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}//这个for循环就是找到第一个后续节点新的index不变的节点。
newTable[lastIdx] = lastRun;
// Clone remaining nodes
//第一个后续节点新index不变节点前所有节点均需要重新创建分配。
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, p.value, n);
}
}
}
}
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
3>ensureSegment方法
private Segment<K,V> ensureSegment(int k) {
final Segment<K,V>[] ss = this.segments;
long u = (k << SSHIFT) + SBASE; // raw offset
Segment<K,V> seg;
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
Segment<K,V> proto = ss[0]; // 以初始化时创建的第一个坑位的ss[0]作为模版进行创建
int cap = proto.table.length;
float lf = proto.loadFactor;
int threshold = (int)(cap * lf);
HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) { // 二次检查是否有其它线程创建了这个Segment
Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
== null) {
//这里通过自旋的CAS方式对segments数组中偏移量为u位置设置值为s,这是一种不加锁的方式,
//万一有多个线程同时执行这一步,那么只会有一个成功,而其它线程在看到第一个执行成功的线程结果后
//会获取到最新的数据从而发现需要更新的坑位已经不为空了,那么就跳出while循环并返回最新的seg
if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
break;
}
}
}
return seg;
}
这个方法核心是利用自旋CAS来创建对应的Segment, 这种思想是不加锁保证线程安全.
4>get方法
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);//获取key对应hash值
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;//获取对应h值存储所在segments数组中内存偏移量
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
//通过Unsafe中的getObjectVolatile方法进行volatile语义的读,获取到segments在偏移量为u位置的分段Segment,
//并且分段Segment中对应table数组不为空
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {//获取h对应这个分段中偏移量为xxx下的HashEntry的链表头结点,然后对链表进行 遍历
//###这里第一次初始化通过getObjectVolatile获取HashEntry时,获取到的是主存中最新的数据,但是在后续遍历过程中,有可能数据被其它线程修改
//从而导致其实这里最终返回的可能是过时的数据,所以这里就是ConcurrentHashMap所谓的弱一致性的体现,containsKey方法也一样
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
这里这个方法是弱一致性的, 所以可能会返回过时的数据
5>size方法
public int size() {
final Segment<K,V>[] segments = this.segments;
int size;
boolean overflow; // 是否溢出
long sum; // 存储本次循环过程中计算得到的modCount的值
long last = 0L; // 存储上一次遍历过程中计算得到的modCount的和
int retries = -1; // first iteration isn't retry
try {
for (;;) {//无限for循环,结束条件就是任意前后两次遍历过程中modcount值的和是一样的,说明第二次遍历没有做任何变化
if (retries++ == RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
ensureSegment(j).lock(); // force creation
}
sum = 0L;
size = 0;
overflow = false;
for (int j = 0; j < segments.length; ++j) {
Segment<K,V> seg = segmentAt(segments, j);
if (seg != null) {
sum += seg.modCount;
int c = seg.count;
if (c < 0 || (size += c) < 0)
overflow = true;
}
}
if (sum == last)
break;
last = sum;
}
} finally {
//由于只有在retries等于RETRIES_BEFORE_LOCK时才会执行强制加锁,并且由于是用的retries++,
//所以强制加锁完毕后,retries的值是一定会大于RETRIES_BEFORE_LOCK的,
//这样就防止正常遍历而没进行加锁时进行锁释放的情况
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
如果要统计整个ConcurrentHashMap中的元素个数, 将每个Segment中的元素数量加起来就好了, 正好Segment中的count是一个volatile变量. 但如果在累加的时候, 其他线程进行了插入或删除操作, 结果就不准了. 所以最安全的方法是对所有Segment的put, remove, clean方法锁住, 但是这种方法很低效.
因为在累加count的过程中, 之前累加过的count发生变化的几率很小, 所以ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment的大小, 如果统计过程中count发生了变化, 则再采用加锁的方式统计所有Segment的大小
2.JDK1.8版本
在jdk1.8中, ConcurrentHashMap通过一个Node<K,V>[]数组来保存添加到map中的键值对, 在同一个数组位置是通过链表或红黑树的形式来表示, 其实原理类似于jdk1.8中的HashMap
1>put方法
public V put(K key, V value) {
return putVal(key, value, false);
}
/*
* 当添加一对键值对的时候,首先会去判断保存这些键值对的数组是不是初始化了,如果没有的话就初始化数组
* 然后通过计算hash值来确定放在数组的哪个位置
* 如果这个位置为空则直接添加,如果不为空的话,则取出这个节点来
* 如果取出来的节点的hash值是MOVED(-1)的话,则表示当前正在对这个数组进行扩容,复制到新的数组,则当前线程也去帮助复制
* 最后一种情况就是,如果这个节点,不为空,也不在扩容,则通过synchronized来加锁,进行添加操作
* 然后判断当前取出的节点位置存放的是链表还是树
* 如果是链表的话,则遍历整个链表,直到取出来的节点的key来个要放的key进行比较,如果key相等,并且key的hash值也相等的话,
* 则说明是同一个key,则覆盖掉value,否则的话则添加到链表的末尾
* 如果是树的话,则调用putTreeVal方法把这个元素添加到树中去
* 最后在添加完成之后,会判断在该节点处共有多少个节点(注意是添加前的个数),如果达到8个以上了的话,
* 则调用treeifyBin方法来尝试将处的链表转为树,或者扩容数组
*/
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();//K,V都不能为空,否则的话跑出异常
int hash = spread(key.hashCode()); //取得key的hash值
int binCount = 0; //用来计算在这个节点总共有多少个元素,用来控制扩容或者转移为树
for (Node<K,V>[] tab = table;;) { //
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0)
tab = initTable(); //第一次put的时候table没有初始化,则初始化table
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { /
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null))) //创建一个Node添加到数组中区,null表示的是下一个节点为空
break; // no lock when adding to empty bin
}
/*
* 如果检测到某个节点的hash值是MOVED,则表示正在进行数组扩张的数据复制阶段,
* 则当前线程也会参与去复制,通过允许多线程复制的功能,一次来减少数组的复制所带来的性能损失
*/
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
/*
* 如果在这个位置有元素的话,就采用synchronized的方式加锁,
* 如果是链表的话(hash大于0),就对这个链表的所有元素进行遍历,
* 如果找到了key和key的hash值都一样的节点,则把它的值替换到
* 如果没找到的话,则添加在链表的最后面
* 否则,是树的话,则调用putTreeVal方法添加到树中去
*
* 在添加完之后,会对该节点上关联的的数目进行判断,
* 如果在8个以上的话,则会调用treeifyBin方法,来尝试转化为树,或者是扩容
*/
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) { //再次取出要存储的位置的元素,跟前面取出来的比较
if (fh >= 0) { //取出来的元素的hash值大于0,当转换为树之后,hash值为-2
binCount = 1;
for (Node<K,V> e = f;; ++binCount) { //遍历这个链表
K ek;
if (e.hash == hash && //要存的元素的hash,key跟要存储的位置的节点的相同的时候,替换掉该节点的value即可
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent) //当使用putIfAbsent的时候,只有在这个key没有设置值得时候才设置
e.val = value;
break;
}
Node<K,V> pred = e;
if ((e = e.next) == null) { //如果不是同样的hash,同样的key的时候,则判断该节点的下一个节点是否为空,
pred.next = new Node<K,V>(hash, key, //为空的话把这个要加入的节点设置为当前节点的下一个节点
value, null);
break;
}
}
}
else if (f instanceof TreeBin) { //表示已经转化成红黑树类型了
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, //调用putTreeVal方法,将该元素添加到树中去
value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD) //当在同一个节点的数目达到8个的时候,则扩张数组或将给节点的数据转为tree
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount); //计数
return null;
}
2>扩容transfer方法
private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
int n = tab.length, stride;
if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
stride = MIN_TRANSFER_STRIDE; // subdivide range
if (nextTab == null) { // initiating
try {
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1]; // 构建一个nextTable,大小为table两倍
nextTab = nt;
} catch (Throwable ex) { // try to cope with OOME
sizeCtl = Integer.MAX_VALUE;
return;
}
nextTable = nextTab;
transferIndex = n;
}
int nextn = nextTab.length;
ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
boolean advance = true;
boolean finishing = false; // to ensure sweep before committing nextTab
//通过for自循环处理每个槽位中的链表元素,默认advace为真,通过CAS设置transferIndex属性值,并初始化i和bound值,i指当前处理的槽位序号,bound指需要处理的槽位边界,先处理槽位15的节点;
for (int i = 0, bound = 0;;) {
Node<K,V> f; int fh;
while (advance) { // 遍历table中的每一个节点
int nextIndex, nextBound;
if (--i >= bound || finishing)
advance = false;
else if ((nextIndex = transferIndex) <= 0) {
i = -1;
advance = false;
}
else if (U.compareAndSwapInt
(this, TRANSFERINDEX, nextIndex,
nextBound = (nextIndex > stride ?
nextIndex - stride : 0))) {
bound = nextBound;
i = nextIndex - 1;
advance = false;
}
}
if (i < 0 || i >= n || i + n >= nextn) {
int sc;
if (finishing) { // //如果所有的节点都已经完成复制工作 就把nextTable赋值给table 清空临时对象nextTable
nextTable = null;
table = nextTab;
sizeCtl = (n << 1) - (n >>> 1); //扩容阈值设置为原来容量的1.5倍 依然相当于现在容量的0.75倍
return;
}
// 利用CAS方法更新这个扩容阈值,在这里面sizectl值减一,说明新加入一个线程参与到扩容操作
if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
return;
finishing = advance = true;
i = n; // recheck before commit
}
}
//如果遍历到的节点为空 则放入ForwardingNode指针
else if ((f = tabAt(tab, i)) == null)
advance = casTabAt(tab, i, null, fwd);
//如果遍历到ForwardingNode节点 说明这个点已经被处理过了 直接跳过 这里是控制并发扩容的核心
else if ((fh = f.hash) == MOVED)
advance = true; // already processed
else {
synchronized (f) {
if (tabAt(tab, i) == f) {
Node<K,V> ln, hn;
if (fh >= 0) { // 链表节点
int runBit = fh & n; // resize后的元素要么在原地,要么移动n位(n为原capacity),详解见:https://huanglei.rocks/coding/194.html#4%20resize()%E7%9A%84%E5%AE%9E%E7%8E%B0
Node<K,V> lastRun = f;
//以下的部分在完成的工作是构造两个链表 一个是原链表 另一个是原链表的反序排列
for (Node<K,V> p = f.next; p != null; p = p.next) {
int b = p.hash & n;
if (b != runBit) {
runBit = b;
lastRun = p;
}
}
if (runBit == 0) {
ln = lastRun;
hn = null;
}
else {
hn = lastRun;
ln = null;
}
for (Node<K,V> p = f; p != lastRun; p = p.next) {
int ph = p.hash; K pk = p.key; V pv = p.val;
if ((ph & n) == 0)
ln = new Node<K,V>(ph, pk, pv, ln);
else
hn = new Node<K,V>(ph, pk, pv, hn);
}
//在nextTable的i位置上插入一个链表
setTabAt(nextTab, i, ln);
//在nextTable的i+n的位置上插入另一个链表
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd);
//设置advance为true 返回到上面的while循环中 就可以执行i--操作
advance = true;
}
//对TreeBin对象进行处理 与上面的过程类似
else if (f instanceof TreeBin) {
TreeBin<K,V> t = (TreeBin<K,V>)f;
TreeNode<K,V> lo = null, loTail = null;
TreeNode<K,V> hi = null, hiTail = null;
int lc = 0, hc = 0;
//构造正序和反序两个链表
for (Node<K,V> e = t.first; e != null; e = e.next) {
int h = e.hash;
TreeNode<K,V> p = new TreeNode<K,V>
(h, e.key, e.val, null, null);
if ((h & n) == 0) {
if ((p.prev = loTail) == null)
lo = p;
else
loTail.next = p;
loTail = p;
++lc;
}
else {
if ((p.prev = hiTail) == null)
hi = p;
else
hiTail.next = p;
hiTail = p;
++hc;
}
}
// (1)如果lo链表的元素个数小于等于UNTREEIFY_THRESHOLD,默认为6,则通过untreeify方法把树节点链表转化成普通节点链表;(2)否则判断hi链表中的元素个数是否等于0:如果等于0,表示lo链表中包含了所有原始节点,则设置原始红黑树给ln,否则根据lo链表重新构造红黑树。
ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
(hc != 0) ? new TreeBin<K,V>(lo) : t;
hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
(lc != 0) ? new TreeBin<K,V>(hi) : t;
setTabAt(nextTab, i, ln);
setTabAt(nextTab, i + n, hn);
setTabAt(tab, i, fwd); // tab[i]已经处理完了
advance = true;
}
}
}
}
}
}
如何在扩容时,并发地复制与插入?
- 遍历整个table,当前节点为空,则采用CAS的方式在当前位置放入fwd
- 当前节点已经为fwd(with hash field “MOVED”),则已经有有线程处理完了了,直接跳过 ,这里是控制并发扩容的核心
- 当前节点为链表节点或红黑树,重新计算链表节点的hash值,移动到nextTable相应的位置(构建了一个反序链表和顺序链表,分别放置在i和i+n的位置上)。移动完成后,用Unsafe.putObjectVolatile在tab的原位置赋为为fwd, 表示当前节点已经完成扩容。
3>get方法
读取操作,不需要同步控制,比较简单
- 空tab,直接返回null
- 计算hash值,找到相应的bucket位置,为node节点直接返回,否则返回null
ublic V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
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.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
3.HashTable与ConcurrentHashMap对比
ConcurrentHashMap能完全替代HashTable吗?
hash table虽然性能上不如ConcurrentHashMap,但并不能完全被取代,两者的迭代器的一致性不同的, hash table的迭代器是强一致性的,而concurrenthashmap是弱一致的。 ConcurrentHashMap的get,clear,iterator 都是弱一致性的。
- Hashtable的任何操作都会把整个表锁住,是阻塞的。好处是总能获取最实时的更新,比如说线程A调用putAll写入大量数据,期间线程B调用get,线程B就会被阻塞,直到线程A完成putAll,因此线程B肯定能获取到线程A写入的完整数据。坏处是所有调用都要排队,效率较低。
- ConcurrentHashMap 是设计为非阻塞的。在更新时会局部锁住某部分数据,但不会把整个表都锁住。同步读取操作则是完全非阻塞的。好处是在保证合理的同步前提下,效率很高。坏处 是严格来说读取操作不能保证反映最近的更新。例如线程A调用putAll写入大量数据,期间线程B调用get,则只能get到目前为止已经顺利插入的部分数据。
选择哪一个,是在性能与数据一致性之间权衡。ConcurrentHashMap适用于追求性能的场景,大多数线程都只做insert/delete操作,对读取数据的一致性要求较低。