JDK1.7 ConcurrentHashMap

<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 方法

<span style="font-family:Times New Roman;font-size:18px;"><span style="font-family:SimSun;font-size:18px;">V put(K key, int hash, V value, boolean onlyIfAbsent) {
    lock();//锁住当前segment
    try {
        int c = count;
        if (c++ > threshold) // 大于Segment HashEntry阀值,需要先扩容
            rehash();
        HashEntry<K,V>[] tab = table;
        int index = hash & (tab.length - 1);//计算下标
        HashEntry<K,V> first = tab[index];//取出当前哈希表该位置的第一个Entry
        HashEntry<K,V> e = first;
        //第一个元素不为空,且和传进来的key不是同一个,如果是同一个,直接返回否则Entry从头结点往后移动
        while (e != null && (e.hash != hash || !key.equals(e.key)))
            e = e.next;
 
        V oldValue;
        //表示找到相同的key,返回value
        if (e != null) {
            oldValue = e.value;
            if (!onlyIfAbsent)
                e.value = value;
        }
        //遍历完没有找到,说明不存在相同key,
        else {
            oldValue = null;
            ++modCount;
            //key成为first,之前的firts成为next
            tab[index] = new HashEntry<K,V>(key, hash, first, value);
            count = c; // 数量+1
        }
        return oldValue;
    } finally {
        unlock();//释放锁住的Segment
    }
}</span></span>


 

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.



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

莫言静好、

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值