1.7版本
基本策略是在segments的基础上再细分table,每一个都是一个并发可读的hashtable。
为了节省空间,
table的默认大小=16
static final int DEFAULT_INITIAL_CAPACITY = 16;
默认加载因子
static final float DEFAULT_LOAD_FACTOR = 0.75f;
默认并发等级
static final int DEFAULT_CONCURRENCY_LEVEL = 16;
最大容量=2^30
static final int MAXIMUM_CAPACITY = 1 << 30;
每个Segment的最小table数=2,防止使用后立即被resizing
static final int MIN_SEGMENT_TABLE_CAPACITY = 2;
segment的最大数量216,,最多224?
static final int MAX_SEGMENTS = 1 << 16; // slightly conservative
?????????????
static final int RETRIES_BEFORE_LOCK = 2;
构造函数
initialCapacity,初始化map的容量,既map能装多少键值对。
loadFactor,负载因子,给Segment计算其包含的table在什么时候被rehash(扩容,重新分布)
concurrencyLevel
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
//Segments最大数,这里默认是2^16.concurrencyLevel不能大于此值
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
//这里的意思是ssize是Segments最终的大小,它只能是2的幂。
//起初是2^0=1,通过和传入的concurrencyLevel比较,取一个不大于concurrencyLevel的2的幂作为Segments最后的容量。
//sshift相当于ssize转换成2进制之后,1后面0的个数。比如ssize==2^4,二进制就是10000,sshift就是4。
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
//int一共32位,segmentShift即时ssize左边数?
this.segmentShift = 32 - sshift;
//segmentMask是segments的容量-1,既最后一个位置的index。
this.segmentMask = ssize - 1;
//整个initialCapacity代表ConcurrentHashMap的初始化时的容量
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
//除以ssize,既segments的大小,既得到每个segment中的table的容量c
int c = initialCapacity / ssize;
//因为可能有余数,所以table容量c * segments容量ssize必须大于initialCapacity。
if (c * ssize < initialCapacity)
++c;
//调整segments的table的容量必须是2的幂,并且不小于计算出来的c
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// create segments and segments[0]
//这里就是创建了,注意的事loadFactor负载因子是用来计算table的rehash的阈值的,
//比如table的cap=8,loadFactor=0.75,那么阈值threshold就是6,
//也就是table里面装了6个hashEntry后就会触发rehash。
Segment<K,V> s0 =
new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
(HashEntry<K,V>[])new HashEntry[cap]);
Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
this.segments = ss;
}
private static class Holder {
/**
* Enable alternative hashing of String keys?
*
* <p>Unlike the other hash map implementations we do not implement a
* threshold for regulating whether alternative hashing is used for
* String keys. Alternative hashing is either enabled for all instances
* or disabled for all instances.
*/
static final boolean ALTERNATIVE_HASHING;
static {
// Use the "threshold" system property even though our threshold
// behaviour is "ON" or "OFF".
String altThreshold = java.security.AccessController.doPrivileged(
new sun.security.action.GetPropertyAction(
"jdk.map.althashing.threshold"));
int threshold;
try {
threshold = (null != altThreshold)
? Integer.parseInt(altThreshold)
: Integer.MAX_VALUE;
// disable alternative hashing if -1
if (threshold == -1) {
threshold = Integer.MAX_VALUE;
}
if (threshold < 0) {
throw new IllegalArgumentException("value must be positive integer.");
}
} catch(IllegalArgumentException failed) {
throw new Error("Illegal value for 'jdk.map.althashing.threshold'", failed);
}
ALTERNATIVE_HASHING = threshold <= MAXIMUM_CAPACITY;
}
}
第二部分成员变量
Holder.ALTERNATIVE_HASHING
final int segmentMask;
final int segmentShift;
final Segment<K,V>[] segments
Segment
//最大重试次数,如果cpu核大于1,取64,否则取1.
static final int MAX_SCAN_RETRIES = Runtime.getRuntime().availableProcessors() > 1 ? 64 : 1;
//被volatile修饰的HashEntry的table。
transient volatile HashEntry<K,V>[] table;
//table中有多少格子被初始化放入了HashEntry的数量。
transient int count;
//“修改次数统计”,对Segment中的table的任何修改都会往上加一
transient int modCount;
//table何时进行rehash的门槛值,这个值是通过负载因子loadFactor和整个table的大小capacity相乘而得。
//比如默认的loadfactor是0.75f,如果table的大小是16,那么threshold既为12.
//意即当table数组中有超过12个位置都被初始化放入了HashEntry的时候,就会在put操作的时候触发rehash
transient int threshold;
//负载因子,作用如上解释。
final float loadFactor;
put方法被ConcurrentHashMap的put和putInAbsent方法使用,key和value很好理解,hash是我们传入的key的哈希值,onlyAbsent表示是否只能在key尚未存在于table中的时候放入。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
//在多次尝试中去获得锁,node不一定是最新的啊????
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
//通过key的hash值定位应该放在哪个table格子中。
int index = (tab.length - 1) & hash;
//得到这个位置的第一个HashEntry
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {//将首节点赋值给变量e,开始循环
//第一种情况,首节点不为null,判断当前e==key,或者传入的key和e的hash,equals2方法均相等,此时在onlyIfAbsent为false的情况下修改e节点的值,增加修改次数统计modCount+1,跳出循环。如果e和key不匹配,则E向链表后一节点移动,继续下一次循环。
if (e != null) {
K k;
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
oldValue = e.value;
if (!onlyIfAbsent) {
e.value = value;
++modCount;
}
break;
}
e = e.next;
}
//第二种情况,e为null,已找遍了这个格子。说明key对应的节点从来没被创建过。
else {
//如果刚才已经在scanAndLockForPut方法中为key创建了对应的Node,将他放到头结点的前面成为新的头结点。如果刚才在scanAndLockForPut没创建成,创建HashEntry。
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
//segment中包含的HashEntry加一
int c = count + 1;
//如果segment包含的HashEntry数既count已经大于threshold,并且table的长度还没有超过最大table容量2^30,则rehash????。如果不需要rehash,直接将key对应的Node放入table的格子中,这时就真正将key-value放了ConcurrentHashMap了。segment的修改次数+1。
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
//将初始化好的第一个HashEntry放入table中
setEntryAt(tab, index, node);
++modCount;//Segment的修改次数+1
count = c;
oldValue = null;//原来没有初始化,oldValue自然是null
break;
}
}
} finally {
//最后释放Segment的锁
unlock();
}
//返回这个key对应的oldValue
return oldValue;
}
scanAndLockForPut方法
这个方法在干嘛呢?
就是不停的通过tryLock去尝试获得Segment的锁。如果第一次就顺利获得了锁,就直接返回,结果也是null。
否则进入while循环,
先去尝试找key是否已经在链表中,如果在,返回的结果node就是null,如果没创建这个key的HashEntry,则返回的node就是新创建的HashEntry。
之后不停的去尝试获取锁,每隔2次尝试锁后,再次检查该table格子的头结点是否变化了,如果变化了。重复第一步遍历链表的动作。
所以这个方法做了2件事。
第一:在低于64次的重试中一定要拿到Segment的锁。
第二:如果key没有对应的已经创建过的HashEntry,创建并返回这个HashEntry,否则返回null.
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
//entryForHash是一通过key的hash值计算key应该在table的哪一个格子中。
//返回这个格子中的HashEntry的头结点,所以结果的变量名叫first.
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
//重试次数默认设置为-1
int retries = -1; // negative while locating node
//再次尝试去锁住Segment,如果成功了返回null??????如果失败了没锁成功,开始循环,一直到得到锁为止。
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {//第一次锁失败了,到达这儿,这时retries为-1,进入代码块。
if (e == null) {
//第一步:
//如果first节点为null,既table中的这个格子从来没被命中过,也就是没有初始化,让入第一个HashEntry.
//尝试去创建table这个格子中的第一个HashEntry,将重试次数设置为0.继续第二轮尝试。
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
//如果first节点不为null,并且first节点就是我们传入的key的节点,将重试次数设置为0,继续第二轮尝试。
else if (key.equals(e.key))
retries = 0;
//如果任然没找到,将e指向后面一个HashEntry。
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
//第二步:
//这里的逻辑是:既然到这儿了,那么表示又是一次重试了,所以++retries,重试次数+1。
//如果重试的次数还没超过64次。继续走到下面的分支。
//如果重试的次数大于了最大扫描重试次数64次,直接使用lock方法,阻塞等待其他线程释放这个Segment的锁,然后跳出循环。既尝试了64次后,我一定要得到这个锁才走。
lock();
break;
}
//第三步:
//如果重试的次数是偶数的时候,再次去那table这个格子里面的值,如果发现头结点已经不是我们刚才拿出来的那个节点了(因为我们锁失败了嘛,说明其他线程在占用此segment,这个线程有可能删除了我们刚才找到的头节点),那就更新数据后重来第一步。
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
Segment的remove方法是ConcurrentHashMap的2个remove方法的实际执行者。
final V remove(Object key, int hash, Object value) {
//和put方法一样首先尝试去获得锁,失败则调用scanAndLock方法。采用tryLock不停的去尝试获得锁,类似scanAndLockForPut,但是更简单??????
if (!tryLock())
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V>[] tab = table;
//通过hash值得到table中相应的格子index
int index = (tab.length - 1) & hash;
//得到table此index上的第一个HashEntry
HashEntry<K,V> e = entryAt(tab, index);
HashEntry<K,V> pred = null;
while (e != null) {//如果e不为null,进入循环,如果是null事就了了,表示找遍了都没有,说明没有对应的key呗。
K k;
//先拿到当前节点的下一节点的引用
HashEntry<K,V> next = e.next;
//如果e就是我们要找的节点,并且传入的value是null,或者value等于这个节点的value值,则删除这个节点
if ((k = e.key) == key ||
(e.hash == hash && key.equals(k))) {
V v = e.value;
if (value == null || value == v || value.equals(v)) {
if (pred == null)
//pred==null说明这个被删除的节点是头结点,要把下一个节点放入table
setEntryAt(tab, index, next);
else
//否则只需直接将前一个节点的next改变即可.
pred.setNext(next);
++modCount;//修改次数+1
--count;//Segment中包含的HashEntry总数减一
oldValue = v;//返回被删除的key对应的旧value
}
//删除成功,跳出循环
break;
}
//e没匹配上,e往后移动一格
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}
scanAndLock
Segment的2个replace方法就更简单了,不再赘述。
Segment的clear方法就是死等锁,然后遍历Segment的table,将每个格子置为null,修改对应的modCount和count,最后释放锁。
final void clear() {
lock();
try {
HashEntry<K,V>[] tab = table;
for (int i = 0; i < tab.length ; i++)
setEntryAt(tab, i, null);
++modCount;
count = 0;
} finally {
unlock();
}
}
Segment的hash方法
在put方法时,超过了threshold被触发。
网友评论