Java集合:ConcurrentHashMap详解

void rehash() {

HashEntry<K,V>[] oldTable = table;

int oldCapacity = oldTable.length;

if (oldCapacity >= MAXIMUM_CAPACITY)

return;

/*

  • Reclassify nodes in each list to new Map. Because we are

  • using power-of-two expansion, the elements from each bin

  • must either stay at same index, or move with a power of two

  • offset. We eliminate unnecessary node creation by catching

  • cases where old nodes can be reused because their next

  • fields won’t change. Statistically, at the default

  • threshold, only about one-sixth of them need cloning when

  • a table doubles. The nodes they replace will be garbage

  • collectable as soon as they are no longer referenced by any

  • reader thread that may be in the midst of traversing table

  • right now.

*/

HashEntry<K,V>[] newTable = HashEntry.newArray(oldCapacity<<1); //新表扩容为原来大小的2倍

threshold = (int)(newTable.length * loadFactor); //重新计算阀值

int sizeMask = newTable.length - 1; //新表的掩码值还是为表长度-1

for (int i = 0; i < oldCapacity ; i++) {

// We need to guarantee that any existing reads of old Map can

// proceed. So we cannot yet null out each bin.

HashEntry<K,V> e = oldTable[i];

if (e != null) {

HashEntry<K,V> next = e.next; //元素e的下一个元素

int idx = e.hash & sizeMask; //计算元素e在新表中的索引位位置

// Single node on list

if (next == null) //如果当前位置只有一个元素,则直接移动到新表的对应位置

newTable[idx] = e;

else {

// Reuse trailing consecutive sequence at same slot

HashEntry<K,V> lastRun = e; //lastRun:最后一个需要处理的元素,初始值为元素e

int lastIdx = idx; //lastIdx:最后一个需要处理的元素的索引位置,初始值为元素e在新表中的索引值

for (HashEntry<K,V> last = next; //遍历该链表,找到最后一个需要处理的元素

last != null;

last = last.next) {

int k = last.hash & sizeMask;

if (k != lastIdx) { //如果当前元素的索引位置跟lastIdx不一致,则将lastIdx和lastRun替换成当前元素的相应值

lastIdx = k;

lastRun = last;

}

}

newTable[lastIdx] = lastRun; //将最后一个需要处理的元素放到新表中

// Clone all remaining nodes

for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {//遍历处理lastRun之前的所有元素

int k = p.hash & sizeMask; //计算当前遍历元素p在新表的索引k

HashEntry<K,V> n = newTable[k]; //取到新表中索引位置k的链表头元素赋值给n

newTable[k] = new HashEntry<K,V>(p.key, p.hash,

n, p.value); //将当前遍历元素p复制到新表的索引位置k的链表头部,next属性指向新表该索引位置原来的链表头n

}

}

}

}

table = newTable; //将新表赋值给table

}

lastRun:最后一个需要处理的元素的意思就是该元素之后的所有元素都跟该元素有相同的索引值(对于新表),所以只需要将该元素放到新表的对应位置,该元素之后的所有元素也就跟着到了新表的对应位置。相当于直接将该链表的最后一截(可能包含若干个元素)直接一次性移到了新表的某个位置。

如果整个循环结束,if (k != lastIdx) 语句没有成立过,就代表当前位置(oldTable[i])的整个HashEntry在新表中的索引位置是一致的,只需要移动一次即可将整个链表移到新表上。根据rehash方法中的那一大段注释提到的“ Statistically, at the default threshold, only about one-sixth of them need cloning when a table doubles”(据统计,在默认阈值下,当表扩大为原来的两倍时,只有约六分之一的元素需要克隆),可以想象,这个if语句没有成立过的可能性应该是挺大的。

4.remove方法


public V remove(Object key) {

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

return segmentFor(hash).remove(key, hash, null);

}

根据key的hashcode重新计算hash值(跟get方法一样),通过segmentFor方法定位到具体的哪个segment,然后调用segment的remove方法。

V remove(Object key, int hash, Object value) {

lock();

try {

int c = count - 1;

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

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

HashEntry<K,V> first = tab[index];

HashEntry<K,V> e = first;

while (e != null && (e.hash != hash || !key.equals(e.key)))

e = e.next;

V oldValue = null;

if (e != null) {

V v = e.value;

if (value == null || value.equals(v)) {

oldValue = v;

// All entries following removed node can stay

// in list, but all preceding ones need to be

// cloned.

++modCount;

HashEntry<K,V> newFirst = e.next;

for (HashEntry<K,V> p = first; p != e; p = p.next)

newFirst = new HashEntry<K,V>(p.key, p.hash,

newFirst, p.value);

tab[index] = newFirst;

count = c; // write-volatile

}

}

return oldValue;

} finally {

unlock();

}

}

加锁进行以下操作:根据hash值跟数组长度-1进行位运算,定位到具体的HashEntry,遍历该HashEntry,根据传入的的key,使用equals方法找到需要的元素,进行以下操作。

HashEntry<K,V> newFirst = e.next;

for (HashEntry<K,V> p = first; p != e; p = p.next)

newFirst = new HashEntry<K,V>(p.key, p.hash,

newFirst, p.value);

tab[index] = newFirst;

该段代码是remove方法中的片段,过程比较特殊,拿出来单独讨论。因为HashEntry使用final修饰,这意味着在第一次设置了next域之后便不能再改变它,因此,此处的remove操作是新建一个HashEntry并将它之前的节点全都克隆一次。至于HashEntry为什么要设置为不变性,这跟不变性的访问不需要同步从而节省时间有关。

用实际例子看上面这段代码更容易懂:

假设1:此时HashEntry为:1 2 3 4 5 6,其中1为链表头,并且1.next = 2,2.next = 3以此类推。

假设2:此时e = 4,即根据key匹配到的元素4是即将remove掉的。

则上面这段代码有以下流程:

HashEntry<K,V>  newFirst = 4.next = 5

for( p = 1; p != 4; p++)

newFirst = new HashEntry(p.key, p.hash, newFirst, p.value);

此循环如下:

p = 1:newFirst = new HashEntry(1.key, 1.hash, 5, 1.value)

p = 2:newFirst = new HashEntry(2.key, 2.hash, 1, 2.value)

p = 3:newFirst = new HashEntry(3.key, 3.hash, 2, 3.value)

p = 4:结束循环

tab[index] = 3;

index为当前链表在HashEntry中的索引位置,所以此时HashEntry为:3 2 1 5 6,被remove的元素之前的元素顺序颠倒了。

remove方法中还有以下这句代码,这句代码在代码中出现非常多次,主要是起什么作用?

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

这句代码是将table赋给一个局部变量tab,这是因为table是 volatile变量,读写volatile变量的开销很大,编译器也不能对volatile变量的读写做任何优化,直接多次访问非volatile实例变量没有多大影响,编译器会做相应优化。

5.replace方法


public boolean replace(K key, V oldValue, V newValue) {

if (oldValue == null || newValue == null)

throw new NullPointerException();

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

return segmentFor(hash).replace(key, hash, oldValue, newValue);

}

根据key的hashcode重新计算hash值(跟get方法一样),通过segmentFor方法定位到具体的哪个segment,然后调用segment的replace方法。

boolean replace(K key, int hash, V oldValue, V newValue) {

lock();

try {

HashEntry<K,V> e = getFirst(hash);

while (e != null && (e.hash != hash || !key.equals(e.key)))

e = e.next;

boolean replaced = false;

if (e != null && oldValue.equals(e.value)) {

replaced = true;

e.value = newValue;

}

return replaced;

} finally {

unlock();

}

}

加锁进行以下操作:根据hash值跟数组长度-1进行位运算,定位到具体的HashEntry(getFirst方法),遍历该HashEntry,使用equals方法比较传入的key和链表中元素中的key,找到所需元素。如果能找到并且该元素的value跟传入的oldValue相等,则将该元素的value替换成newValue。

6.clear方法


public void clear() {

for (int i = 0; i < segments.length; ++i)

segments[i].clear();

}

void clear() {

if (count != 0) {

lock();

try {

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

for (int i = 0; i < tab.length ; i++)

tab[i] = null;

++modCount;

count = 0; // write-volatile

} finally {

unlock();

}

}

}

遍历segments,对每一个segment进行清空操作:加锁进行以下操作,遍历HashEntry数组,将每个HashEntry设置为null,并将count设置为0。

7.size方法


public int size() {

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

long sum = 0;

long check = 0;

int[] mc = new int[segments.length];

// Try a few times to get accurate count. On failure due to

// continuous async changes in table, resort to locking.

for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {

check = 0;

sum = 0;

int mcsum = 0;

for (int i = 0; i < segments.length; ++i) {//第一次统计

sum += segments[i].count;

mcsum += mc[i] = segments[i].modCount;

}

if (mcsum != 0) {

for (int i = 0; i < segments.length; ++i) {//第二次统计

check += segments[i].count;

if (mc[i] != segments[i].modCount) {//modCount发生该变则结束当次尝试

check = -1; // force retry

break;

}

}

}

if (check == sum)

break;

}

if (check != sum) { // Resort to locking all segments

sum = 0;

for (int i = 0; i < segments.length; ++i)

segments[i].lock();

for (int i = 0; i < segments.length; ++i)

sum += segments[i].count;

for (int i = 0; i < segments.length; ++i)

segments[i].unlock();

}

if (sum > Integer.MAX_VALUE)

return Integer.MAX_VALUE;

else

return (int)sum;

}

先在不加锁的情况下尝试进行统计,如果两次统计结果相同,并且两次统计之间没有任何对segment的修改操作(即每个segment的modCount没有改变),则返回统计结果。否则,对每个segment进行加锁,然后统计出结果,返回结果。

8.containsValue方法


自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

2021年Java中高级面试必备知识点总结

在这个部分总结了2019年到目前为止Java常见面试问题,取其面试核心编写成这份文档笔记,从中分析面试官的心理,摸清面试官的“套路”,可以说搞定90%以上的Java中高级面试没一点难度。

本节总结的内容涵盖了:消息队列、Redis缓存、分库分表、读写分离、设计高并发系统、分布式系统、高可用系统、SpringCloud微服务架构等一系列互联网主流高级技术的知识点。

目录:

(上述只是一个整体目录大纲,每个点里面都有如下所示的详细内容,从面试问题——分析面试官心理——剖析面试题——完美解答的一个过程)

部分内容:

对于每一个做技术的来说,学习是不能停止的,小编把2019年到目前为止Java的核心知识提炼出来了,无论你现在是处于什么阶段,如你所见,这份文档的内容无论是对于你找面试工作还是提升技术广度深度都是完美的。

不想被后浪淘汰的话,赶紧搞起来吧,高清完整版一共是888页,需要的话可以点赞+关注
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!
涵盖了:消息队列、Redis缓存、分库分表、读写分离、设计高并发系统、分布式系统、高可用系统、SpringCloud微服务架构等一系列互联网主流高级技术的知识点。

目录:

[外链图片转存中…(img-XHc5qw1s-1712034068297)]

(上述只是一个整体目录大纲,每个点里面都有如下所示的详细内容,从面试问题——分析面试官心理——剖析面试题——完美解答的一个过程)

[外链图片转存中…(img-HdGK0p7m-1712034068297)]

部分内容:

[外链图片转存中…(img-5ynY0Hvp-1712034068298)]

[外链图片转存中…(img-hnaUH4Dw-1712034068298)]

[外链图片转存中…(img-Q97AxXUc-1712034068299)]

对于每一个做技术的来说,学习是不能停止的,小编把2019年到目前为止Java的核心知识提炼出来了,无论你现在是处于什么阶段,如你所见,这份文档的内容无论是对于你找面试工作还是提升技术广度深度都是完美的。

不想被后浪淘汰的话,赶紧搞起来吧,高清完整版一共是888页,需要的话可以点赞+关注
《一线大厂Java面试题解析+核心总结学习笔记+最新讲解视频+实战项目源码》点击传送门即可获取!

  • 9
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值