<span style="background-color: rgb(255, 255, 255);"><span style="font-family:Times New Roman;font-size:18px;"></span></span><pre code_snippet_id="1941677" snippet_file_name="blog_20161021_1_3425809" name="code" class="java" style="font-weight: bold;"><span style="font-family: "Times New Roman";">一 HashMap HashTable ConcurrentHashMap 结构图</span>
<span style="background-color: rgb(255, 255, 255);"><span style="font-family:Times New Roman;font-size:18px;"></span></span><pre code_snippet_id="1941677" snippet_file_name="blog_20161021_1_3425809" name="code" class="java" style="font-weight: bold;"><span style="font-family: "Times New Roman";">一 HashMap HashTable ConcurrentHashMap 结构图</span>
1.1HashMap 结构
如果是jdk1.8 如果链表长度大于8会转成红黑树处理
1.2 HashTable 结构
1.3 ConcurrentHashMap 结构
JDK1.8之前
jdk1.8之后其实结构和HashMap差不多
这三者主要的区别是什么呢?
首先HashMap的性能是最好的,基于数组+链表的数据结构实现。
但是呢?在多线程的情况下,容易出现死循环,因为在扩容的时候,会进行rehash动作,会把以前的哈希桶内的数据然后移到新的桶里,在移动过程中,有可能某个线程已经完事了,但是另外一个线程刚开始,有可能导致该线程指向新的已经重组后的链表,从而导致死循环。
其次,HashTable是线程安全的,不过基于synchoronized实现的,这样的问题导致并发情况下,会竞争同一个锁,那么效率可想而知。
最后,ConcurrentHashMap就是为了在性能和安全做了一个综合的方案。但是他不是锁住整个哈希表,而是采用锁分离的技术,使用多个锁,控制不同segment,每一个段其实就是一个小的哈希桶数组或者叫哈希表。只要多个操作发生不同的segment上,他们就可以并行操作。同时还兼备了HashMap的一些优点。
HashMap允许键为null,只允许一个;ConcurrentHashMap则不允许,抛出空指针异常
二 Concurrent HashMap 重要的类
2.1 Segment
<span style="font-family:Times New Roman;font-size:18px;">static final class Segment<K, V> extends ReentrantLock implements Serializable {
transient volatile int count;
transient int modCount;
transient int threshold;
transient volatile HashEntry<K, V>[] table;
final float loadFactor;
}</span>
count:Segment中元素的数量
modCount:对table的大小造成影响的操作的数量(比如put或者remove操作)
threshold:阈值,Segment里面元素的数量超过这个值依旧就会对Segment进行扩容
table:链表数组,数组中的每一个元素代表了一个链表的头部
loadFactor:负载因子,用于确定threshold
2.2 HashEntry:
<span style="font-family:Times New Roman;font-size:18px;"><span style="font-family:SimSun;font-size:18px;">static final class HashEntry<K,V> {
final K key;
final int hash;
volatile V value;
final HashEntry<K,V> next;
}</span></span>
三 重要的方法
3.1 segmentFor(int hash) 方法
用于确定操作应该在哪一个segment中进行
3.2 rehash 方法
<span style="font-family:Times New Roman;font-size:18px;"><span style="font-family:SimSun;font-size:18px;">void rehash() {
HashEntry<K,V>[] oldTable = table;//
int oldCapacity = oldTable.length;//现在Segment里面HashEntry的数量
if (oldCapacity >= MAXIMUM_CAPACITY)//如果已经大于最大的了,那么就不管了,hash冲突就冲突吧
return;
//产生一个新的HashEntry数组,大小为之前的2倍(oldCapacity<<1)
HashEntry<K,V>[] newTable = HashEntry.newArray(oldCapacity<<1);
threshold = (int)(newTable.length * loadFactor);//重新计算threshold阀值
int sizeMask = newTable.length - 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;//计算新的下标3
if (next == null)
newTable[idx] = e;
else {
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;
// 把当前链表其他的元素转到新的
for (HashEntry<K,V> p = e; p != lastRun; p = p.next) {
int k = p.hash & sizeMask;
HashEntry<K,V> n = newTable[k];
newTable[k] = new HashEntry<K,V>(p.key, p.hash,
n, p.value);
}
}
}
}
table = newTable;
}</span></span>
3.3 put 方法
|
3.4 get 方法
<span style="font-family:Times New Roman;font-size:18px;"><span style="font-family:SimSun;font-size:18px;">public V get(Object key) {
int hash = hash(key.hashCode());
return segmentFor(hash).get(key, hash);
}
/*
* 为什么整个get方法不需要加锁
* 因为 count 和 HashEntry的value都是volatile的
* 也就说能够在线程间保持可见性,只要修改了,能偶被同时读到
*
*/
V get(Object key, int hash) {
if (count != 0) {
// 根據key的hashcode取得第一個Entry
HashEntry<K, V> e = getFirst(hash);
while (e != null) {
//然後根據key去比較key,如果相同返回
if (e.hash == hash && key.equals(e.key)) {
V v = e.value;
if (v != null)
return v;
return readValueUnderLock(e); // recheck
}
e = e.next;
}
}
return null;
}
//value为空锁住了Segment,为什么?因为可能其他的线程有可能正在改变值
V readValueUnderLock(HashEntry<K,V> e) {
lock();
try {
return e.value;
} finally {
unlock();
}
}</span></span>
四 JDK1.8 和 之前版本的ConcurrentHashMap比较
- 由于HashMap的改进,ConcurrentHashMap也基于此做了一些调整。
- JDK1.8之前,主要是基于Segment进行存取数据的,然后每一个Segment又包括一组HashEntry数组。锁分段也是基于Segment进行的;JDK.18之后,基本没用到Segment了,存取数据直接采用HashEntry数组,然后每一次对每一个HashEntry上锁,这样其实也实现了锁分离,和使用Segment有着相似的地方。 只是读取的操作需要CAS来保证。
- 将以前的Segment 数组+HashEntry数组+链表的结构转化为
- HashEntry数组+链表+红黑树的结构存储。这样查询的时间复杂度由O(N)降低为O(LogN)
- 新的ConcurrentHashMap很多地方用到CAS算法,保证变量的原子性。包括get和size,这时候并不像以前一样,可能还需要加锁,防止value正在改变。而现在count和 value都是volatile的,采用若一致性,只要put完成,get既可感知到。
- 老的ConcurrentHashMap上锁机制采用的是lock,而新的ConcurrentHashMap采用的是Synchronized.