目录
一、简单介绍
ConcurrentHashMap是通过数组和链表来实现的,但是和HashMap不一样的是,其中有两个数组,一个是Segment<K,V>数组,用于存放Segment对象,一个是HashEntry<K,V>数组。
基本的存储结构就像上图这样,Segment数组存放Segment对象。在其对象下面存放的是hashentry数组,而这个hashentry数组相当于一个hashmap一样的存储结构,数组加链表。Segment数组的默认长度是为16,hashentry数组默认长度等于,初始化长度/并发级别=1,这两个数据的默认值都是16,即在每个segment对象下面都含有一个hashentry数组大小为1(默认情况下)。
二、方法分析
主要从构造方法、pu和扩容这两个方法来分析
1、构造方法
①全参构造器
传入的是三个参数initialCapacity:初始化容量、loadFactor:负载因子、concurrencyLevel:并发级别三个参数,先会进行参数的检验,防止负载因子和初始化容量小于0和并发级别小于等于0,成立即抛出异常。然后判断并发级别于最大值大小;经过while循环去计算需要初始化的Segment数组的大小,于并发级别相比,则并发级别是与Segment的大小是关联上的,ssize <<= 1,左移一位扩大两倍,为2的幂次方数。
何为并发级别呢?即在多线程环境下,可以同时有多少个线程同时进行操作同一个ConcurrentHashMap对象。
segmentShift这个参数后面需要用,是移位操作。segmentMask在计算传入的key所要放入的Segment对象在数组中的位置所需要的,即计算索引的时候。然后判断初始化容量于最大值的大小;c * ssize < initialCapacity判断这个,是为了向上取整,防止溢出,即为最小的hashentry容量;cap即为我说的hashentry数组的容量大小,默认为2,判断cap和c的大小,满足就左移扩大两倍,直到不满足为止,数组大小为2的幂次方数;然后定义一个Segment()对象S0,并初始化Segment数组大小为ssize;使用UNSAFE中的方法,原子性,有序写入S0,防止多线程下的不安全性。
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
if (concurrencyLevel > MAX_SEGMENTS)
concurrencyLevel = MAX_SEGMENTS;
// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
++sshift;
ssize <<= 1;
}
this.segmentShift = 32 - sshift;
this.segmentMask = ssize - 1;
if (initialCapacity > MAXIMUM_CAPACITY)
initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
++c;
int cap = MIN_SEGMENT_TABLE_CAPACITY;
while (cap < c)
cap <<= 1;
// create segments and segments[0]
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;
}
②其他构造器
都会去调用全参构造器,只是把参数改变了而已,传入的参数替换默认的参数。
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
}
/**
* Creates a new, empty map with the specified initial capacity,
* and with default load factor (0.75) and concurrencyLevel (16).
*
* @param initialCapacity the initial capacity. The implementation
* performs internal sizing to accommodate this many elements.
* @throws IllegalArgumentException if the initial capacity of
* elements is negative.
*/
public ConcurrentHashMap(int initialCapacity) {
this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
/**
* Creates a new, empty map with a default initial capacity (16),
* load factor (0.75) and concurrencyLevel (16).
*/
public ConcurrentHashMap() {
this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
}
2、put方法
四个参数,出了最后一个,其他三个应该都知道什么意思,最后一个onlyIfAbsent是标记,即在如果传入的key在其中是存在的,那么这个标记是为了判断是否替换其中的value值。
第一步是拿锁tryLock(),因为Segment类不类继承了ReentrantLock所以可以使用其中的锁,我们先看拿到锁的语句,获取到table数组,计算传入的key在hashentry中的位置,(tab.length - 1) & hash和hashmap中计算位置是一样的计算方法,调用entryAt()拿到头节点,然后遍历这个数组下的链表;先判断头节点是否为null,不为null,在进行判断传入的key和该节点的key比较是否相等,oldValue记录该节点的value值,然后通过onlyIfAbsent来判断是否需要覆盖value值,需要覆盖记录一次modCount++,modCount为操作次数;如果该节点是没有值的时候,执行下面语句,然后判断node是否为null,满足即使用头插法,把传入的值改为新的头节点,其next节点为原来的frist节点;为null就new一个node作为头节点,把frist作为next节点,c = count + 1记录对象的个数。然后判断c与阈值比较,还要比较数组长度是否小于最大值,同事满足就进行扩容操作,不满足就直接插入即可,oldValue设置为null,然后break,最后释放锁,返回oldValue。
final V put(K key, int hash, V value, boolean onlyIfAbsent) {
HashEntry<K,V> node = tryLock() ? null :
scanAndLockForPut(key, hash, value);
V oldValue;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> first = entryAt(tab, index);
for (HashEntry<K,V> e = first;;) {
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;
}
else {
if (node != null)
node.setNext(first);
else
node = new HashEntry<K,V>(hash, key, value, first);
int c = count + 1;
if (c > threshold && tab.length < MAXIMUM_CAPACITY)
rehash(node);
else
setEntryAt(tab, index, node);
++modCount;
count = c;
oldValue = null;
break;
}
}
} finally {
unlock();
}
return oldValue;
}
在来看没有拿到锁的情况,同样先获取得到头节点,进行while循环tryLock()拿不到锁进循环,判断retries是否小于0,然后判断e是否为null,即头节点,满足在进行判断node是否为null,满足就会new一个node,把传入的值设置进去,将retries = 0,然后判断是否在这个链表中是否存在与传入的key相同的key,有也会 retries = 0;判断retries是否大于MAX_SCAN_RETRIES,成立就会调用lock()进行阻塞在这里。然后下面判断(retries & 1) == 0即为偶数成立,f = entryForHash(this, hash)) != first判断头节点是否发生改变,改变了就会再一次进入遍历链表。最后是返回node,
private HashEntry<K,V> scanAndLockForPut(K key, int hash, V value) {
HashEntry<K,V> first = entryForHash(this, hash);
HashEntry<K,V> e = first;
HashEntry<K,V> node = null;
int retries = -1; // negative while locating node
while (!tryLock()) {
HashEntry<K,V> f; // to recheck first below
if (retries < 0) {
if (e == null) {
if (node == null) // speculatively create node
node = new HashEntry<K,V>(hash, key, value, null);
retries = 0;
}
else if (key.equals(e.key))
retries = 0;
else
e = e.next;
}
else if (++retries > MAX_SCAN_RETRIES) {
lock();
break;
}
else if ((retries & 1) == 0 &&
(f = entryForHash(this, hash)) != first) {
e = first = f; // re-traverse if entry changed
retries = -1;
}
}
return node;
}
为什么要进行这些操作呢?①为了进行预热,提前准备好需要的操作;②使得循环变慢;③必须让其得到锁
3、rehash方法(扩容)
扩容操作其实和hashmap的扩容有相同之处,先获得hashentry数组及其长度,新的数组长度的原有长度左移一位扩大两倍,阈值的新的数组长度乘以负载因子,new一个新的hashentry数组,for循环原来的数组,判断e是否为null,不为null就获取下一个节点,计算e在新的数组中的位置e.hash & sizeMask,然后判断next是否为null,为null那么把这个值e移到新数组中;不为next不为null,记录idx和e,然后在此for循环,计算e.next在新的数组中的位置,并且判断是否与上一个节点在新的数组中的位置是否相同,不相同就在此记录位置,其实这个for循环就是找到最后在新的数组中位置相同的对象,以方便把位置相同的node直接移动到新的数组中,下面的for循环其实就是将没有移动的node进行复制,然后进入下一此的外层for循环。外层循环结束后,执行最后的语句,计算传入的key值在新的数组中的位置,并把传入的node插入新的数组中的链表中。
private void rehash(HashEntry<K,V> node) {
HashEntry<K,V>[] oldTable = table;
int oldCapacity = oldTable.length;
int newCapacity = oldCapacity << 1;
threshold = (int)(newCapacity * loadFactor);
HashEntry<K,V>[] newTable =
(HashEntry<K,V>[]) new HashEntry[newCapacity];
int sizeMask = newCapacity - 1;
for (int i = 0; i < oldCapacity ; i++) {
HashEntry<K,V> e = oldTable[i];
if (e != null) {
HashEntry<K,V> next = e.next;
int idx = e.hash & sizeMask;
if (next == null) // Single node on list
newTable[idx] = e;
else { // Reuse consecutive sequence at same slot
HashEntry<K,V> lastRun = e;
int lastIdx = idx;
for (HashEntry<K,V> last = next;
last != null;
last = last.next) {
int k = last.hash & sizeMask;
if (k != lastIdx) {
lastIdx = k;
lastRun = last;
}
}
newTable[lastIdx] = lastRun;
// Clone remaining nodes
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
V v = p.value;
int h = p.hash;
int k = h & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(h, p.key, v, n);
}
}
}
}
int nodeIndex = node.hash & sizeMask; // add the new node
node.setNext(newTable[nodeIndex]);
newTable[nodeIndex] = node;
table = newTable;
}
4、get方法
Segment<K,V> s这个操作主要是为了减少开销,获取hashentry数组tab,计算key的hashcode值,u计算其在segment中的位置,找到u所在的segment的对象进行判断是否为null并给s,还要判断s下的hashentry数组是否为null,同时不为null是才执行for循环,判断传入的key和找到的key进行比较,相等就返回这个key的value值,没有相同的key值的话,即没有找到,就返回null.
public V get(Object key) {
Segment<K,V> s; // manually integrate access methods to reduce overhead
HashEntry<K,V>[] tab;
int h = hash(key);
long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
(tab = s.table) != null) {
for (HashEntry<K,V> e = (HashEntry<K,V>) UNSAFE.getObjectVolatile
(tab, ((long)(((tab.length - 1) & h)) << TSHIFT) + TBASE);
e != null; e = e.next) {
K k;
if ((k = e.key) == key || (e.hash == h && key.equals(k)))
return e.value;
}
}
return null;
}
5、remove方法
删除方法,其实和put是一样的,只是少了new node,其他的都是一样的,put懂了,这个就会了,返回oldValue,
final V remove(Object key, int hash, Object value) {
if (!tryLock())
scanAndLock(key, hash);
V oldValue = null;
try {
HashEntry<K,V>[] tab = table;
int index = (tab.length - 1) & hash;
HashEntry<K,V> e = entryAt(tab, index);
HashEntry<K,V> pred = null;
while (e != null) {
K k;
HashEntry<K,V> next = e.next;
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)
setEntryAt(tab, index, next);
else
pred.setNext(next);
++modCount;
--count;
oldValue = v;
}
break;
}
pred = e;
e = next;
}
} finally {
unlock();
}
return oldValue;
}
三、总结
其实concurrenthashmap是在hashmap的基础上增加了一些锁的操作,当你hashmap懂了之后在看这个集合会更加轻松。可能我的理解不是很全面,而且页没有在代码中写注释,但是我写的其实就是代码的解析,读懂了就明白基本的方法功能了,也没有把所有方法给分析,我只是把主要的方法分析了,其实其他的方法可以自己去研究研究,会加深你对concurrenthashmap的理解,谢谢阅读!