Java基础知识强化之集合框架笔记77:ConcurrentHashMap之 ConcurrentHashMap的基本操作

1. ConcurrentHashMap的初始化:

下面我们来结合源代码来具体分析一下ConcurrentHashMap的实现,先看下初始化方法:

 1 public ConcurrentHashMap(int initialCapacity,
 2                          float loadFactor, int concurrencyLevel) {
 3     if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
 4         throw new IllegalArgumentException();
 5
 6     if (concurrencyLevel > MAX_SEGMENTS)
 7         concurrencyLevel = MAX_SEGMENTS;
 8
 9     // Find power-of-two sizes best matching arguments
10     int sshift = 0;
11     int ssize = 1;
12     while (ssize < concurrencyLevel) {
13         ++sshift;
14         ssize <<= 1;
15     }
16     segmentShift = 32 - sshift;
17     segmentMask = ssize - 1;
18     this.segments = Segment.newArray(ssize);
19
20     if (initialCapacity > MAXIMUM_CAPACITY)
21         initialCapacity = MAXIMUM_CAPACITY;
22     int c = initialCapacity / ssize;
23     if (c * ssize < initialCapacity)
24         ++c;
25     int cap = 1;
26     while (cap < c)
27         cap <<= 1;
28
29     for (int i = 0; i < this.segments.length; ++i)
30         this.segments[i] = new Segment<K,V>(cap, loadFactor);
31 }

CurrentHashMap的初始化一共有三个参数:

一个initialCapacity表示初始的容量

一个loadFactor表示负载参数

最后一个是concurrentLevel,代表ConcurrentHashMap内部的Segment的数量

ConcurrentLevel一经指定,不可改变,后续如果ConcurrentHashMap的元素数量增加导致ConrruentHashMap需要扩容,ConcurrentHashMap不会增加Segment的数量,而只会增加Segment中链表数组的容量大小,这样的好处是扩容过程不需要对整个ConcurrentHashMap做rehash,而只需要对Segment里面的元素做一次rehash就可以了

整个ConcurrentHashMap的初始化方法还是非常简单的:

先是根据concurrentLevel来new出Segment,这里Segment的数量是不大于concurrentLevel的最大的2的指数,就是说Segment的数量永远是2的指数个,这样的好处是方便采用移位操作来进行hash,加快hash的过程。接下来就是根据intialCapacity确定Segment的容量的大小,每一个Segment的容量大小也是2的指数,同样使为了加快hash的过程。

这边需要特别注意一下两个变量,分别是segmentShift和segmentMask,这两个变量在后面将会起到很大的作用,假设构造函数确定了Segment的数量是2的n次方,那么segmentShift就等于32减去n,而segmentMask就等于2的n次方减一。

2. ConcurrentHashMap的get操作:

前面提到过ConcurrentHashMap的get操作是不用加锁的,我们这里看一下其实现:

1 public V get(Object key) {
2     int hash = hash(key.hashCode());
3     return segmentFor(hash).get(key, hash);
4 }
5  

看第三行,segmentFor这个函数用于确定操作应该在哪一个segment中进行,几乎对ConcurrentHashMap的所有操作都需要用到这个函数,我们看下这个函数的实现:

1 final Segment<K,V> segmentFor(int hash) {
2     return segments[(hash >>> segmentShift) & segmentMask];
3 }

这个函数用了位操作来确定Segment,根据传入的hash值向右无符号右移segmentShift位,然后和segmentMask进行与操作,结合我们之前说的segmentShift和segmentMask的值,就可以得出以下结论:假设Segment的数量是2的n次方,根据元素的hash值的高n位就可以确定元素到底在哪一个Segment中

在确定了需要在哪一个segment中进行操作以后,接下来的事情就是调用对应的Segment的get方法

 1 V get(Object key, int hash) {
 2     if (count != 0) { // read-volatile
 3         HashEntry<K,V> e = getFirst(hash);
 4         while (e != null) {
 5             if (e.hash == hash && key.equals(e.key)) {
 6                 V v = e.value;
 7                 if (v != null)
 8                     return v;
 9                 return readValueUnderLock(e); // recheck
10             }
11             e = e.next;
12         }
13     }
14     return null;
15 }

先看第二行代码,这里对count进行了一次判断,其中count表示Segment中元素的数量,我们可以来看一下count的定义:

1 transient volatile int count;

可以看到count是volatile的,实际上这里里面利用了volatile的语义:

因为实际上put、remove等操作也会更新count的值,所以当竞争发生的时候,volatile的语义可以保证写操作在读操作之前,也就保证了写操作对后续的读操作都是可见的,这样后面get的后续操作就可以拿到完整的元素内容。

然后,在第三行调用了getFirst()来取得链表的头部

1 HashEntry<K,V> getFirst(int hash) {
2     HashEntry<K,V>[] tab = table;
3     return tab[hash & (tab.length - 1)];
4 }
5  

同样,这里也是用位操作来确定链表的头部hash值和HashTable的长度减一做与操作,最后的结果就是hash值的低n位,其中n是HashTable的长度以2为底的结果。

在确定了链表的头部以后,就可以对整个链表进行遍历,看第4行,取出key对应的value的值,如果拿出的value的值是null,则可能这个key,value对正在put的过程中,如果出现这种情况,那么就加锁来保证取出的value是完整的,如果不是null,则直接返回value。

3. ConcurrentHashMap的put操作:

看完了get操作,再看下put操作,put操作的前面也是确定Segment的过程,这里不再赘述,直接看关键的segment的put方法

 1 V put(K key, int hash, V value, boolean onlyIfAbsent) {
 2     lock();
 3     try {
 4         int c = count;
 5         if (c++ > threshold) // ensure capacity
 6             rehash();
 7         HashEntry<K,V>[] tab = table;
 8         int index = hash & (tab.length - 1);
 9         HashEntry<K,V> first = tab[index];
10         HashEntry<K,V> e = first;
11         while (e != null && (e.hash != hash || !key.equals(e.key)))
12             e = e.next;
13
14         V oldValue;
15         if (e != null) {
16             oldValue = e.value;
17             if (!onlyIfAbsent)
18                 e.value = value;
19         }
20         else {
21             oldValue = null;
22             ++modCount;
23             tab[index] = new HashEntry<K,V>(key, hash, first, value);
24             count = c; // write-volatile
25         }
26         return oldValue;
27     } finally {
28         unlock();
29     }
30 }

首先对Segment的put操作是加锁完成的,然后在第五行,如果Segment中元素的数量超过了阈值(由构造函数中的loadFactor算出)这需要进行对Segment扩容,并且要进行rehash,关于rehash的过程大家可以自己去了解,这里不详细讲了。

第8和第9行的操作就是getFirst的过程,确定链表头部的位置

第11行这里的这个while循环是在链表中寻找和要put的元素相同key的元素如果找到就直接更新更新key的value如果没有找到,则进入21行这里,生成一个新的HashEntry并且把它加到整个Segment的头部,然后再更新count的值。

4. ConcurrentHashMap的remove操作:

Remove操作的前面一部分和前面的get和put操作一样,都是定位Segment的过程,然后再调用Segment的remove方法:

 1 V remove(Object key, int hash, Object value) {
 2     lock();
 3     try {
 4         int c = count - 1;
 5         HashEntry<K,V>[] tab = table;
 6         int index = hash & (tab.length - 1);
 7         HashEntry<K,V> first = tab[index];
 8         HashEntry<K,V> e = first;
 9         while (e != null && (e.hash != hash || !key.equals(e.key)))
10             e = e.next;
11
12         V oldValue = null;
13         if (e != null) {
14             V v = e.value;
15             if (value == null || value.equals(v)) {
16                 oldValue = v;
17                 // All entries following removed node can stay
18                 // in list, but all preceding ones need to be
19                 // cloned.
20                 ++modCount;
21                 HashEntry<K,V> newFirst = e.next;
22                 for (HashEntry<K,V> p = first; p != e; p = p.next)
23                     newFirst = new HashEntry<K,V>(p.key, p.hash,
24                                                   newFirst, p.value);
25                 tab[index] = newFirst;
26                 count = c; // write-volatile
27             }
28         }
29         return oldValue;
30     } finally {
31         unlock();
32     }
33 }

 首先remove操作也是确定需要删除的元素的位置,不过这里删除元素的方法不是简单地把待删除元素的前面的一个元素的next指向后面一个就完事了,我们之前已经说过HashEntry中的next是final的,一经赋值以后就不可修改,在定位到待删除元素的位置以后,程序就将待删除元素前面的那一些元素全部复制一遍,然后再一个一个重新接到链表上去,看一下下面这一幅图来了解这个过程:

假设链表中原来的元素如上图所示,现在要删除元素3,那么删除元素3以后的链表就如下图所示:

5. ConcurrentHashMap的size操作:

在前面的章节中,我们涉及到的操作都是在单个Segment中进行的,但是ConcurrentHashMap有一些操作是在多个Segment中进行,比如size操作,ConcurrentHashMap的size操作也采用了一种比较巧的方式,来尽量避免对所有的Segment都加锁。

前面我们提到了一个Segment中的有一个modCount变量,代表的是对Segment中元素的数量造成影响的操作的次数,这个值只增不减,size操作就是遍历了两次Segment每次记录Segment的modCount值,然后将两次的modCount进行比较如果相同则表示期间没有发生过写入操作,就将原先遍历的结果返回如果不相同则把这个过程再重复做一次如果再不相同则就需要将所有的Segment都锁住,然后一个一个遍历了,具体的实现大家可以看ConcurrentHashMap的源码,这里就不贴了。

时间: 2024-10-13 16:20:02

Java基础知识强化之集合框架笔记77:ConcurrentHashMap之 ConcurrentHashMap的基本操作的相关文章

Java基础知识强化之集合框架笔记07:Collection集合的遍历之迭代器遍历

1. Collection的迭代器: 1 Iterator iterator():迭代器,集合的专用遍历方式 2. 代码示例: package cn.itcast_03; import java.util.ArrayList; import java.util.Collection; import java.util.Iterator; /* * Iterator iterator():迭代器,集合的专用遍历方式 * Iterator(迭代器): * Object next():获取元素,并移动

Java基础知识强化之集合框架笔记04:Collection集合的基本功能测试

1. Collection集合的基本功能测试: 1 package cn.itcast_01; 2 3 import java.util.ArrayList; 4 import java.util.Collection; 5 6 /* 7 * 集合的由来: 8 * 我们学习的是面向对象语言,而面向对象语言对事物的描述是通过对象体现的,为了方便对多个对象进行操作,我们就必须把这多个对象进行存储. 9 * 而要想存储多个对象,就不能是一个基本的变量,而应该是一个容器类型的变量,在我们目前所学过的知识

Java基础知识强化之集合框架笔记01:集合的由来与数组的区别

1. 集合的由来: 我们学习的是面向对象语言,而面向对象语言对事物的描述是通过对象体现的,为了方便对多个对象进行操作,我们就必须把这多个对象进行存储.而要想存储多个对象,就不能是一个基本的变量,而应该是一个容器类型的变量,在我们目前所学过的知识里面,有哪些是容器类型的呢?数组和StringBuffer.但是呢?StringBuffer的结果是一个字符串,不一定满足我们的要求,所以我们只能选择数组,这就是对象数组.而对象数组又不能适应变化的需求,因为数组的长度是固定的,这个时候,为了适应变化的需求

Java基础知识强化之集合框架笔记62:Map集合之HashMap嵌套HashMap

1. HashMap嵌套HashMap  传智播客          jc    基础班                      陈玉楼  20                      高跃     22          jy    就业班                      李杰     21                      曹石磊  23  先存储元素,然后遍历元素 2. 代码示例: 1 package cn.itcast_05; 2 3 import java.uti

Java基础知识强化之集合框架笔记11:Collection集合之迭代器的原理及源码解析

1. 迭代器为什么不定义成一个类,而是定义为一个接口 ?  答:假设迭代器定义的是一个类,这样我们就可以创建该类的对象,调用该类的方法来实现集合的遍历.但是呢? 我们想想,Java中提供了很多的集合类,而这些集合类的数据结构是不同的,所以,存储的方式和遍历的方式应该是不同的.进而它们的遍历方式也应该不是一样的,最终,就没有定义迭代器类.        而无论你是哪种集合,你都应该具备获取元素的操作,而且,最好在辅助于判断功能,这样,在获取前,先判断.这样的话就不容易出错.也就是说,判断功能和获取

Java基础知识强化之集合框架笔记60:Map集合之TreeMap(TreeMap&lt;Student,String&gt;)的案例

1. TreeMap(TreeMap<Student,String>)的案例 2. 案例代码: (1)Student.java: 1 package cn.itcast_04; 2 3 public class Student { 4 private String name; 5 private int age; 6 7 public Student() { 8 super(); 9 } 10 11 public Student(String name, int age) { 12 super

Java基础知识强化之集合框架笔记57:Map集合之HashMap集合(HashMap&lt;Student,String&gt;)的案例

1. HashMap集合(HashMap<Student,String>)的案例 HashMap<Student,String>键:Student      要求:如果两个对象的成员变量值都相同,则为同一个对象.值:String HashMap是最常用的Map集合,它的键值对在存储时要根据键的哈希码来确定值放在哪里. HashMap 中作为键的对象必须重写Object的hashCode()方法和equals()方法 2. 代码示例: (1)Student.java,如下: 1 pa

Java基础知识强化之集合框架笔记58:Map集合之LinkedHashMap类的概述

1. LinkedHashMap类的概述 LinkedHashMap:Map接口的哈希表(保证唯一性) 和 链接(保证有序性)列表实现,具有可预知的迭代顺序. 2. 代码示例: 1 package cn.itcast_03; 2 3 import java.util.LinkedHashMap; 4 import java.util.Set; 5 6 /* 7 * LinkedHashMap:是Map接口的哈希表和链接列表实现,具有可预知的迭代顺序. 8 * 由哈希表保证键的唯一性 9 * 由链

Java基础知识强化之集合框架笔记59:Map集合之TreeMap(TreeMap&lt;String,String&gt;)的案例

1. TreeMap类的概述: 键是红黑树结构,可以保证键的排序和唯一性. 2. TreeMap案例: TreeMap<String, String> 代码示例: 1 package cn.itcast_04; 2 3 import java.util.Set; 4 import java.util.TreeMap; 5 6 /* 7 * TreeMap:是基于红黑树的Map接口的实现. 8 * 9 * HashMap<String,String> 10 * 键:String 11