Java集合:ConcurrentHashMap详解

根据key的hashcode重新计算hash值(主要是为了减少hash冲突),通过segmentFor方法定位到具体的哪个segment,然后调用segment的get方法。

final Segment<K,V> segmentFor(int hash) {

return segments[(hash >>> segmentShift) & segmentMask];

}

segmentFor方法是用来定位到具体的segment的,主要是通过使用hash值的高位与掩码进行位运算,segmentShift和segmentMask是通过上文初始化方法计算而来。

V get(Object key, int hash) {

if (count != 0) { // read-volatile

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

while (e != null) {

if (e.hash == hash && key.equals(e.key)) {

V v = e.value;

if (v != null)

return v;

return readValueUnderLock(e); // recheck

}

e = e.next;

}

}

return null;

}

HashEntry<K,V> getFirst(int hash) {

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

return tab[hash & (tab.length - 1)];

}

根据hash值跟数组长度-1进行位与运算,定位到具体的HashEntry(getFirst方法),遍历该HashEntry链表,找到链表中某个元素的hash值与传入的hash值相同并且使用equals方法比较key相同的元素,如果该元素的value不为空,返回value值;如果为空,则尝试在加锁的情况下再读一次。

get操作的高效之处在于整个get过程不需要加锁,除非读到的值是空的才会加锁重读,我们知道HashTable容器的get方法是需要加锁的,那么ConcurrentHashMap的get操作是如何做到不加锁的呢?原因是它的get方法里将要使用的共享变量都定义成volatile,如用于统计当前Segement大小的count字段和用于存储值的HashEntry的value,定义成volatile的变量,能够在线程之间保持可见性,能够被多线程同时读,并且保证不会读到过期的值,但是只能被单线程写(有一种情况可以被多线程写,就是写入的值不依赖于原值),在get操作里只需要读不需要写共享变量count和value,所以可以不用加锁。之所以不会读到过期的值,是根据Java内存模型的happen before原则,对volatile字段的写入操作先于读操作,即使两个线程同时修改和获取volatile变量,get操作也能拿到最新的值,这是用volatile替换锁的经典应用场景。

/**

  • Reads value field of an entry under lock. Called if value

  • field ever appears to be null. This is possible only if a

  • compiler happens to reorder a HashEntry initialization with

  • its table assignment, which is legal under memory model

  • but is not known to ever occur.

*/

V readValueUnderLock(HashEntry<K,V> e) {

lock();

try {

return e.value;

} finally {

unlock();

}

}

readValueUnderLock:在有锁的状态下再读一次。这似乎有些费解,理论上结点的值不可能为空,这是因为put的时候就进行了判断,如果为空就要抛NullPointerException。空值的唯一源头就是HashEntry中的默认值,因为HashEntry中的value不是final的,非同步读取有可能读取到空值。仔细看下put操作的语句:tab[index] = new HashEntry<K,V>(key, hash, first, value),在这条语句中,HashEntry构造函数中对value的赋值以及对tab[index]的赋值可能被重新排序(方法上面的一大段注释有提到,英语好的可以直接读注释),这就可能导致结点的值为空。这里当value为空时,可能是一个线程正在改变节点,而之前的get操作都未进行锁定,根据bernstein条件,读后写或写后读都会引起数据的不一致,所以这里要对这个e重新上锁再读一遍,以保证得到的是正确值。

3.put方法


public V put(K key, V value) {

if (value == null)

throw new NullPointerException();

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

return segmentFor(hash).put(key, hash, value, false);

}

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

V put(K key, int hash, V value, boolean onlyIfAbsent) {

lock();

try {

int c = count;

if (c++ > threshold) // ensure capacity

rehash();

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;

if (e != null) {

oldValue = e.value;

if (!onlyIfAbsent)

e.value = value;

}

else {

oldValue = null;

++modCount;

tab[index] = new HashEntry<K,V>(key, hash, first, value);

count = c; // write-volatile

}

return oldValue;

} finally {

unlock();

}

}

加锁进行以下操作:判断是否需要扩容,如果需要则调用rehash方法(下面有介绍)。根据hash值跟数组长度-1进行位与运算,定位到具体的HashEntry,遍历该HashEntry,根据传入的的key,使用equals方法找到需要的元素。如果能找到,则将该元素的value值覆盖为传入的value,否则将传入的key、value、hash值作为一个新元素放在该HashEntry的头部,最后进行解锁。入参中的onlyIfAbsent为true时,表示如果该key已经存在value值,则不会覆盖原value值。

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;

}

最后

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

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

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

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

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

}

最后

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

深知大多数Java工程师,想要提升技能,往往是自己摸索成长,自己不成体系的自学效果低效漫长且无助。

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

[外链图片转存中…(img-jLVDZPvq-1715779646097)]

[外链图片转存中…(img-ZamzPrHf-1715779646098)]

[外链图片转存中…(img-coNZmLFN-1715779646098)]

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,不论你是刚入门Java开发的新手,还是希望在技术上不断提升的资深开发者,这些资料都将为你打开新的学习之门!

如果你觉得这些内容对你有帮助,需要这份全套学习资料的朋友可以戳我获取!!

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值