100%
。resize()方法
进行扩容时,需要将旧的HashMap中的Entry移动到新的HashMap中,该操作由transfer()方法
负责完成。transfer()方法
在移动的过程中,如果出现以下情况,将会导致Entry链表形成死循环。ConcurrentHashMap
使用锁分段技术,现将数据分成一段一段的存储,然后给每一段数据加一把锁。只要不是并发访问同一段数据,就不会出现锁竞争的情况。
ConcurrentHashMap
的锁分段技术有效地提高了并发访问率。ConcurrentHashMap
是由 Segment 数组结构和 HashEntry 数组结构组成。ReentrantLock
,在 ConcurrentHashMap
里扮演锁的角色;HashEntry 则用于存储键值对数据,就是HashMap中的Node。ConcurrentHashMap
里包含一个 Segment 数组,每个Segment包含一个HashEntry数组,其实就是一个HashMap,为桶+链表的结构。initialCapacity
、loadFactor
、concurrencyLevel
几个参数来初始化 segments 数组的长度ssize、段偏移量 segmentShift、段掩码 segmentMask ,每个 segment 里的 HashEntry 数组的长度 cap,每个segment的容量阈值threshold。segments数组的初始化
concurrencyLevel
是允许访问的最大并发数,ssize
是segments数组的长度,即整个ConcurrentHashMap
中锁的个数。if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
int sshift = 0;
int ssize = 1;
// ssize是大于等于concurrencyLevel的最小2^N,sshift是1左移的次数,也就是前面的N
//比如我输入的concurrencyLevel=12,那么sshift = 4,ssize =16
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1; //ssize = ssize << 1 , ssize = ssize * 2
}
segmentShift = 32 - sshift;
segmentMask = ssize - 1; //segmentMask的二进制是一个全是1的数
this.segments = Segment.newArray(ssize); //segment个数是ssize,默认为16
concurrencyLevel
计算得出的:先将ssize初始化为1,只要ssize < concurrencyLevel
,就将ssize向左移一位,即将ssize扩大两倍。直到ssize是大于或等于concurrencyLevel
的最小的 2 N 2^N 2NconcurrencyLevel
的最小的 2 N 2^N 2N?答: 为了能通过按位与的hash算法定位segments数组的索引,要求ssize = 2^N
。concurrencyLevel = 14
,则计算出来的ssize = 16
。concurrencyLevel
的最大值为65535( 2 16 − 1 2^{16}-1 216−1),所以ssize 的最大值为65536)( 2 16 2^{16} 216)。segmentShift
和segmentMask
这两个全局变量。前者定位参与hash运算的位数,后者表示hash运算的掩码。segmentShift
,segmentShift = 32 - sshift
。ConcurrentHashMap
里的 hash() 方法输出的最大数是 32 位。segmentShift
的最小值为16。segmentMask
的计算非常简单,segmentMask = ssize - 1
。segmentMask
的二进制数全为1。segmentMask
的最大值为65535,二进制数16位,每位都为1。segment的初始化
initialCapacity
是ConcurrentHashMap 的初始化容量,即segments数组中HashEntry数组的总长度。loadFactor
是每个 segment 的负载因子,在构造方法中初始化segment,需要使用cap
和loadFactor
这两个参数if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = 1;
while (cap < c)
cap <<= 1;
for (int i = 0; i < this.segments.length; ++i)
//cap=2^N,默认是1,也就是每个segment下都构造了cap大小的table数组
this.segments[i] = new Segment<K,V>(cap, loadFactor);
initialCapacity/ssize
,可以得到initialCapacity
是ssize的倍数c。c * ssize < initialCapacity
,则c++
。cap < c
就将cap向左移一位,即将cap扩大两倍。直到cap是大于等于c的最小 2 n 2^n 2n。threshold = cap * loadFactor
。concurrencyLevel = 16
,initialCapocity = 16
,loadFactor = 0.75f
。ConcurrentMap<String, Long> wordCounts = new ConcurrentHashMap<>()
initialCapocity
、loadFactor
、concurrencyLevel
的值:new ConcurrentHashMap<>(initialCapocity)
new ConcurrentHashMap<>(initialCapocity, loadFactor)
new ConcurrentHashMap<>(initialCapocity, loadFactor, concurrencyLevel)
final Segment<K,V> segmentFor(int hash) {
return segments[(hash >>> segmentShift) & segmentMask];
}
>>>
),右移segmentShift
那么多位,意思是让该值的高32 - segmentShift
参与散列运算。segmentMask
进行按位与,计算出键值对在segment中的位置。segmentShift = 32 - 4 = 28
,segmentMask = 16 - 1 = 15
。HashEntry<K,V> getFirst(int hash) {
HashEntry<K,V>[] tab = table;
return tab[hash & (tab.length - 1)];
}
readValueUnderLock(e)
)public V get(Object key) {
// 对key的hashCode进行再散列,使元素均匀分布
int hash = hash(key.hashCode());
// 先定位segment,再获取元素
return segmentFor(hash).get(key, hash);
}
// 真正的get操作
V get(Object key, int hash) {
// count是一个volatile变量,每次put和remove之后的最后一步都要更新count
if (count != 0) {
HashEntry<K,V> e = getFirst(hash);// 定位桶下标
while (e != null) {
// 判断键相等:要求hash值相同且key相等
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v;
return readValueUnderLock(e); // 加锁读
}
e = e.next;
}
}
return null;
}
HashEntry<K,V> getFirst(int hash) {
HashEntry<K,V>[] tab = table;
return tab[hash & (tab.length - 1)];
}
threshold
)。public V put(K key, V value) {
if (value == null)
throw new NullPointerException(); //明确指定value不能为null
int hash = hash(key.hashCode());
return segmentFor(hash).put(key, hash, value, false);
}
//真正的put操作
V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock(); // 对segment加锁,在finally中释放锁
try {
int c = count;// volatile变量,先赋值给局部变量,再读写。
if (c++ > threshold) // ensure capacity
rehash(); //判断容量,如果不够了就扩容
//table是volatile变量,读写volatile变量的开销很大
HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1); //寻找table的下标
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
//遍历单链表,找到key相同的为止,或者直到链表结尾为止
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue;
if (e != null) { //如果有相同的key,那么直接替换
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();
}
}
lock()
。c > threshold
判断是否需要进行扩容,如果需要,调用rehash()方法
进行扩容。HashEntry<K,V>[] tab = table;
int index = hash & (tab.length - 1); //寻找table的下标
HashEntry<K,V> first = tab[index];
HashEntry<K,V> e = first;
finally语句块
中,对整个segment进行解锁,unlock()
。put
, remove
和 clean
方法中加一,直接判断统计前后的modCount值是否发生变化,就可以知道容器大小是否发生变化。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;
}
HashTable
中get、put、remove、clear操作都是用synchronized
关键字进行修饰,是对整个HashTable加锁。而ConcurrentHashMap
在put、remove、clear操作中使用锁,只是对某个segment单独加锁。HashTable
的get操作使用synchronized
字修饰,因此不支持同时读。而ConcurrentHashMap
的get操作在整个过程中不加锁,除非读到的value为null才加锁重读,支持同时读,而且保证不会读到过期值。(使用volatitle关键字代替锁的经典场景)HashTable
的同步方法,其他线程也访问HashTable
的同步方法,必须进入阻塞状态,等待锁的释放。而某个线程占用锁访问ConcurrentHashMap
中的某个segment,其他线程可以访问ConcurrentHashMap
的其他segment。ReentrantLock
,并发度与 Segment 数量相等。1. HashMap,HashTable,三者的区别,底层源码
ConcurrentHashMap
入手:HashMap线程不安全,HashTable效率低下,ConcurrentHashMap
锁分段技术提高并发访问率。ConcurrentHashMap
的结构、HashTable的put、get、remove、clear操作使用synchronized关键字修饰。2. JDK1.7中ConcurrentHashMap的size()操作
3. HashMap与ConcurrentHashMap的区别
ConcurrentHashMap
支持多线程的并发访问:ConcurrentHashMap
的锁分段技术4. ConcurrentHashMap源码实现
ConcurrentHashMap
锁分段技术、初始化过程、get操作、put操作、size操作5. ConcurrentHashMap与HashTable在性能上的差异?形成差异的原因?
synchronized
关键字实现并发访问时的线程安全,效率低下。ConcurrentHashMap
使用锁分段机制实现并发访问,提高了并发访问率。6. JDK1.7和JDK1.8中,ConcurrentHashMap的区别