一些问题总结:
最大的分段(segment)数为2的16次方,每一个segment的HashEntry[]的最大容量为2的30次方。
默认的分段数和每个segment的HashEntry[]的初始容量均为16。segment的默认加载因子为0.75。定位segment段需要用的两个参数:segmentMask,segmentShift。
这两个参数在构造函数中进行处理,如下:
//ssize为经传入的参数concurrencyLevel计算得到的segment数组的大小,sshift为移位次数
int sshift = 0;
int ssize = 1;// segment数组的长度
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
segmentShift = 32 - sshift;// eg.segmentShift==32-4=28
segmentMask = ssize - 1;// eg.segmentMask==16-1==15
/**
* 根据给定的key的hash值定位到一个Segment
* @param hash
*/
final Segment<K, V> segmentFor(int hash) {
return segments[(hash >>> segmentShift) & segmentMask];
}
3.Segment类(ConcurrentHashMap的内部类):继承ReentrantLock类
属性:
count(用变量volatile修饰),表示该Segment中的包含的所有HashEntry中的key-value的个数。
HashEntry[] table(用变量volatile修饰),表示该Segment中的链表数组。
modCount,并发标记。
threshold,元素个数超出了这个值就扩容 threshold==(int)(capacity * loadFactor),注意是当前segment进行扩容。
4.HashEntry节点的定义:
static final class HashEntry<K, V> {
final K key;// 键
final int hash;//hash值
volatile V value;// 实现线程可见性
final HashEntry<K, V> next;// 下一个HashEntry
.........
}
注意的是:值value为volatile类型,其他均为final,前者意味着对其他线程可见,后者意味着next不可以修改,在删除操作时,若删除目标节点在链表非头节点位置,需要将其前面部分进行copy,倒序连接。copy之前的那一份交给垃圾回收器处理。
如1-2-3-4,删除3,将变成2-1-4。
5.三参构造函数: (int initialCapacity,float loadFactor,int concurrencyLevel)
/**
* 创建ConcurrentHashMap
* @param initialCapacity 用于计算Segment数组中的每一个segment的HashEntry[]的容量, 但是并不是每一个segment的HashEntry[]的容量
* @param loadFactor
* @param concurrencyLevel 用于计算Segment数组的大小(可以传入不是2的几次方的数,但是根据下边的计算,最终segment数组的大小ssize将是2的几次方的数)
*
* 步骤:
* 这里以默认的无参构造器参数为例,initialCapacity==16,loadFactor==0.75f,concurrencyLevel==16
* 1)检查各参数是否符合要求
* 2)根据concurrencyLevel(16),计算Segment[]的容量ssize(16)与扩容移位条件sshift(4)
* 3)根据sshift与ssize计算将来用于定位到相应Segment的参数segmentShift与segmentMask
* 4)根据ssize创建Segment[]数组,容量为ssize(16)
* 5)根据initialCapacity(16)与ssize计算用于计算HashEntry[]容量的参数c(1)
* 6)根据c计算HashEntry[]的容量cap(1)
* 7)根据cap与loadFactor(0.75)为每一个Segment[i]都实例化一个Segment
* 8)每一个Segment的实例化都做下面这些事儿:
* 8.1)为当前的Segment初始化其loadFactor为传入的loadFactor(0.75)
* 8.2)创建一个HashEntry[],容量为传入的cap(1)
* 8.3)根据创建出来的HashEntry的容量(1)和初始化的loadFactor(0.75),计算扩容因子threshold(0)
* 8.4)初始化Segment的table为刚刚创建出来的HashEntry
*/
public ConcurrentHashMap(int initialCapacity,float loadFactor,int concurrencyLevel) {
// 检查参数情况
if (loadFactor <= 0f || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
/**
* 找一个能够正好小于concurrencyLevel的数(这个数必须是2的几次方的数)
* eg.concurrencyLevel==16==>sshift==4,ssize==16
* 当然,如果concurrencyLevel==15也是上边这个结果
*/
int sshift = 0;
int ssize = 1;// segment数组的长度
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;// ssize=ssize*2
}
segmentShift = 32 - sshift;// eg.segmentShift==32-4=28 用于根据给定的key的hash值定位到一个Segment
segmentMask = ssize - 1;// eg.segmentMask==16-1==15 用于根据给定的key的hash值定位到一个Segment
this.segments = Segment.newArray(ssize);// 构造出了Segment[ssize]数组 eg.Segment[16]
/*
* 下面将为segment数组中添加Segment元素
*/
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;// eg.initialCapacity==16,c==16/16==1
if (c * ssize < initialCapacity)// eg.initialCapacity==17,c==17/16=1,这时1*16<17,所以c=c+1==2
++c;// 为了少执行这一句,最好将initialCapacity设置为2的几次方
int cap = 1;// 每一个Segment中的HashEntry[]的初始化容量
while (cap < c)
cap <<= 1;// 创建容量
for (int i = 0; i < this.segments.length; ++i)
// 这一块this.segments.length就是ssize,为了不去计算这个值,可以直接改成i<ssize
this.segments[i] = new Segment<K, V>(cap, loadFactor);
}
默认的构造函数:调用上面的构造函数
/**
* 创建ConcurrentHashMap
*/
public ConcurrentHashMap() {
this(DEFAULT_INITIAL_CAPACITY, // 16
DEFAULT_LOAD_FACTOR, // 0.75f
DEFAULT_CONCURRENCY_LEVEL);// 16
}
注意:
(1)传入的concurrencyLevel只是用于计算Segment数组的大小(可以传入不是2的几次方的数,但是根据下边的计算,最终segment数组的大小ssize将是2的几次方的数),并非真正的Segment数组的大小。
(2)传入的initialCapacity只是用于计算Segment数组中的每一个segment的HashEntry[]的容量, 但是并不是每一个segment的HashEntry[]的容量,而每一个HashEntry[]的容量不是2的几次方。
(3)非常值得注意的是,在默认情况下,创建出的HashEntry[]数组的容量为1,并不是传入的initialCapacity(16),证实了上一点;而每一个Segment的扩容因子threshold,一开始算出来是0,即开始put第一个元素就要扩容,不太理解JDK为什么这样做。
(4)想要在初始化时扩大HashEntry[]的容量,可以指定initialCapacity参数,且指定时最好指定为2的几次方的一个数,这样的话,在代码执行中可能会少执行一句”c++”,具体参看三参构造器的注释。
(5)对于Concurrenthashmap的扩容而言,只会扩当前的Segment,而不是整个Concurrenthashmap中的所有Segment都扩。
6.put函数:put(Object key, Object value)
/**
* 将key-value放入map
* 注意:key和value都不可以为空
* 步骤:
* 1)计算key.hashCode()的hash值
* 2)根据hash值定位到某个Segment
* 3)调用Segment的put()方法
* Segment的put()方法:
* 1)上锁
* 2)从主内存中读取key-value对个数count
* 3)count+1如果大于threshold,执行rehash()
* 4)计算将要插入的HashEntry[]的下标index
* 5)获取HashEntry的头节点HashEntry[index]-->first
* 6)从头结点开始遍历整个HashEntry链表,
* 6.1)若找到与key和hash相同的节点,则判断onlyIfAbsent如果为false,新值覆盖旧值,返回旧值;如果为true,则直接返回旧值(相当于不添加重复key的元素)
* 6.2)若没有找到与key和hash相同的节点,则创建新节点HashEntry,并将之前的有节点作为新节点的next,即将新节点放入链头,然后将新节点赋值给HashEntry[index],将count强制写入主内存,最后返回null
*/
public V put(K key, V value) {
if (key == null || value == null)
throw new NullPointerException();
int hash = hash(key.hashCode());//计算key.hashCode()的hash值
/**
* 根据hash值定位到某个Segment,调用Segment的put()方法
*/
return segmentFor(hash).put(key, hash, value, false);
}
里面的segment的put源码如下:
/**
* 往当前segment中添加key-value
* 注意:
* 1)onlyIfAbsent-->false如果有旧值存在,新值覆盖旧值,返回旧值;true如果有旧值存在,则直接返回旧值,相当于不添加元素(不可添加重复key的元素)
* 2)ReentrantLock的用法
* 3)volatile只能配合锁去使用才能实现原子性
*/
V put(K key, int hash, V value, boolean onlyIfAbsent) {
lock();//加锁:ReentrantLock
try {
int c = count;//当前Segment中的key-value对(注意:由于count是volatile型的,所以读的时候工作内存会从主内存重新加载count值)
if (c++ > threshold) // 需要扩容
rehash();//扩容
HashEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);//按位与获取数组下标:与HashMap相同
HashEntry<K, V> first = tab[index];//获取相应的HashEntry[i]中的头节点
HashEntry<K, V> e = first;
//一直遍历到与插入节点的hash和key相同的节点e;若没有,最后e==null
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue;//旧值
if (e != null) {//table中已经有与将要插入节点相同hash和key的节点
oldValue = e.value;//获取旧值
if (!onlyIfAbsent)
e.value = value;//false 覆盖旧值 true的话,就不添加元素了
} else {//table中没有与将要插入节点相同hash或key的节点
oldValue = null;
++modCount;
tab[index] = new HashEntry<K, V>(key, hash, first, value);//将头节点作为新节点的next,所以新加入的元素也是添加在链头
count = c; //设置key-value对(注意:由于count是volatile型的,所以写的时候工作内存会立即向主内存重新写入count值)
}
return oldValue;
} finally {
unlock();//手工释放锁
}
}
注意:
(1)key和value都不可为null,这一点与HashMap不同。
(2)nlyIfAbsent–>false如果有旧值存在,新值覆盖旧值,返回旧值;true如果有旧值存在,则直接返回旧值,相当于不添加元素
(3)ReentrantLock的用法:必须手工释放锁。可实现Synchronized的效果,原子性。
(4)volatile需要配合锁去使用才能实现原子性,否则在多线程操作的情况下依然不够用,在程序中,count变量(当前Segment中的key-value对个数)通过volatile修饰,实现内存可见性。在有锁保证了原子性的情况下:
a. 当我们读取count变量的时候,会强制从主内存中读取count的最新值;
b. 当我们对count变量进行赋值之后,会强制将最新的count值刷到主内存中去。
通过以上两点,我们可以保证在高并发的情况下,执行这段流程的线程可以读取到最新值。
(5)ConcurrentHashMap基于concurrencyLevel划分出多个Segment来存储key-value,这样的话put的时候只锁住当前的Segment,可以避免put的时候锁住整个map,从而减少了并发时的阻塞现象。
7.rehash操作:在上一步put操作时,可能会引发rehash操作。(非jdk版,自行修改版)
/**
* 步骤:
* 需要注意的是:同一个桶下边的HashEntry链表中的每一个元素的hash值不一定相同,只是hash&(table.length-1)的结果相同
* 1)创建一个新的HashEntry数组,容量为旧数组的二倍
* 2)计算新的threshold
* 3)遍历旧数组的每一个元素,对于每一个元素(即一个链表)
* 3.1)获取头节点e
* 3.2)从头节点开始到最后一个节点(null之前的那个节点)的所有节点计算其将要存储的索引k,然后创建新节点,将新节点赋给newTable[k],并将之前newTable[k]上存在的节点作为新节点的下一节点
*/
void rehash() {
HashEntry<K, V>[] oldTable = table;
int oldCapacity = oldTable.length;
if (oldCapacity >= MAXIMUM_CAPACITY)
return;
HashEntry<K, V>[] newTable = HashEntry.newArray(oldCapacity << 1);//扩容为原来二倍
threshold = (int) (newTable.length * loadFactor);//计算新的扩容临界值
int sizeMask = newTable.length - 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) {
for (HashEntry<K, V> p = e; p != null; p = p.next) {//遍历数组元素中的链表
int k = p.hash & sizeMask;
HashEntry<K, V> n = newTable[k];//获取newTable[k]已经存在的HashEntry,并将此HashEntry赋给n
//创建新节点,并将之前的n作为新节点的下一节点
newTable[k] = new HashEntry<K, V>(p.key, p.hash, n,p.value);
}
}
}
table = newTable;
}
注意:同一个桶下边的HashEntry链表中的每一个元素的hash值不一定相同,只是index = hash&(table.length-1)的结果相同,当table.length发生变化时,同一个桶下各个HashEntry算出来的index会不同。
8.get操作
ConcurrentHashMap的get(Object key)
/**
* 根据key获取value
* 步骤:
* 1)根据key获取hash值
* 2)根据hash值找到相应的Segment
* 调用Segment的get(Object key, int hash)
* 3)根据hash值找出HashEntry数组中的索引index,并返回HashEntry[index]
* 4)遍历整个HashEntry[index]链表,找出hash和key与给定参数相等的HashEntry,例如e,
* 4.1)如没找到e,返回null
* 4.2)如找到e,获取e.value
* 4.2.1)如果e.value!=null,直接返回
* 4.2.2)如果e.value==null,则先加锁,等并发的put操作将value设置成功后,再返回value值
*/
public V get(Object key) {
int hash = hash(key.hashCode());
return segmentFor(hash).get(key, hash);
}
Segment的get(Object key, int hash)
/**
* 根据key和hash值获取value
*/
V get(Object key, int hash) {
if (count != 0) { // read-volatile
HashEntry<K, V> e = getFirst(hash);//找到HashEntry[index]
while (e != null) {//遍历整个链表
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v;
/*
* 如果V等于null,有可能是当下的这个HashEntry刚刚被创建,value属性还没有设置成功,
* 这时候我们读到是该HashEntry的value的默认值null,所以这里加锁,等待put结束后,返回value值
*/
return readValueUnderLock(e);
}
e = e.next;
}
}
return null;
}
/**
* 根据hash值找出HashEntry数组中的索引index,并返回HashEntry[index]
*/
HashEntry<K, V> getFirst(int hash) {
HashEntry<K, V>[] tab = table;
return tab[hash & (tab.length - 1)];
}
V readValueUnderLock(HashEntry<K, V> e) {
lock();
try {
return e.value;
} finally {
unlock();
}
}
注意:get操作基本不用加锁。这个理解起来需要知道两点:
(1)第一步是访问count变量,这是一个volatile变量,由于所有的修改操作在进行结构修改时都会在最后一步写count 变量,通过这种机制保证get操作能够得到几乎最新的结构更新。对于非结构更新,也就是结点值的改变,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。
(2)对得到key相对应的值v是否为null的判断。如果v等于null,有可能是当下的这个HashEntry刚刚被创建,value属性还没有设置成功,这时候我们读到是该HashEntry的value的默认值null,所以这里加锁,等待put结束后,返回value值。
9.remove操作
ConcurrentHashMap的remove(Object key)
/**
* 删除指定key的元素
* 步骤:
* 1)根据key获取hash值
* 2)根据hash值获取Segment
* 调用Segment的remove(Object key, int hash, Object value)
* 1)count-1
* 2)获取将要删除的元素所在的HashEntry[index]
* 3)遍历链表,
* 3.1)若没有hash和key都与指定参数相同的节点e,返回null
* 3.2)若有e,删除指定节点e,并将e之前的节点重新排序后,将排序后的最后一个节点的下一个节点指定为e的下一个节点
* (很绕,不知道JDK为什么这样实现)
*/
public V remove(Object key) {
int hash = hash(key.hashCode());
return segmentFor(hash).remove(key, hash, null);
}
Segment的remove(Object key, int hash, Object value)
V remove(Object key, int hash, Object value) {
lock();
try {
int c = count - 1;//key-value对个数-1
HashEntry<K, V>[] tab = table;
int index = hash & (tab.length - 1);
HashEntry<K, V> first = tab[index];//获取将要删除的元素所在的HashEntry[index]
HashEntry<K, V> e = first;
//从头节点遍历到最后,若未找到相关的HashEntry,e==null,否则,有
while (e != null && (e.hash != hash || !key.equals(e.key)))
e = e.next;
V oldValue = null;
if (e != null) {//将要删除的节点e
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;
/*
* 从头结点遍历到e节点,这里将e节点删除了,但是删除节点e的前边的节点会倒序
* eg.原本的顺序:E3-->E2-->E1-->E0,删除E1节点后的顺序为:E2-->E3-->E0
* E1前的节点倒序排列了
*/
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();
}
}
注意:
(1)remove操作需要加锁
(2)之前介绍过HashEntry结构时,它的值value为volatile类型,其他均为final,前者意味着对其他线程可见,后者意味着next不可以修改,在删除操作时,若删除目标节点在链表非头节点位置,需要将其前面部分进行copy,倒序连接。copy之前的那一份交给垃圾回收器处理。
如1-2-3-4,删除3,将变成2-1-4。
10.containsKey(Object key)/keySet().iterator() 不加锁,比较简单,就不贴源码了。
11.size() (jdk1.7)
public int size() {
// Try a few times to get accurate count. On failure due to
// continuous async changes in table, resort to locking.
final Segment<K,V>[] segments = this.segments;
int size;//节点总数
boolean overflow; // true if size overflows 32 bits
long sum; // sum of modCounts
long last = 0L; // previous sum
int retries = -1; // first iteration isn't retry
try {
for (;;) {
//retry两次,RETRIES_BEFORE_LOCK=2
//当两次均是当前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;
}
}
//若当前modCount总数和前一次modCount总数相同,表示没有其他操作,循环跳出
//否则继续进行下次retry
if (sum == last)
break;
last = sum;
}
} finally {
//释放锁
if (retries > RETRIES_BEFORE_LOCK) {
for (int j = 0; j < segments.length; ++j)
segmentAt(segments, j).unlock();
}
}
return overflow ? Integer.MAX_VALUE : size;
}
注意:
(1)统计所有Segment里元素的大小然后求它们的和,如果直接将各个segment中的count(volatile修饰)相加,这是不可取的。volatile只能保证得到的count是最新的,但是在相加过程中,可能有其他的线程改变count,得到的是不精确的结果
(2)最安全的做法时把segement的put,remove等操作给全部锁住,但是这种方法很低效。
(3)因为在累加count操作过程中,之前累加过的count发生变化的概率太小,所以ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?使用modCount变量,在put , remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。
总结
数据结构:一个指定个数的Segment数组,数组中的每一个元素Segment相当于一个HashTable。
加锁情况(锁分离技术):
(1)put
(2)get中找到了hash与key都与指定参数相同的HashEntry,但是value==null的情况
(3)remove
(4)size():两次尝试后,还未成功,遍历所有Segment,分别加锁(即建立全局锁)