zoukankan      html  css  js  c++  java
  • HashMap 、HashTable与ConcurrentHashMap

    • HashMap 就是哈希表, 它的底层数据结构是,数组加链表加红黑树。假设HashMap  的初始元素有16个,HashMap 通过put(k,v)来添加值,put 的过程是首先通过k算出一个hashcode,然后在放到对应的数组中,随着put的值增多时就会发生两个hashcode 的值相同,此时就发生了冲突,那么就在数组的同一个位置产生了链表,如果链表数量大于8的时候就会变成红黑树,如果小于6时,又会变成链表。

    1.哈希表的优点:对于查找,添加和删除时间复杂度都是O(1)。因为它有数组,数组的查询为O(1),还有链表,链表的添加和删除时间复杂度是O(1)。

    2.哈希表的缺点:线程不安全,在涉及到多线程并发的情况,进行get操作有可能会引起死循环,导致CPU利用率接近100%。

    final HashMap<String, String> map = new HashMap<String, String>(2);
    for (int i = 0; i < 10000; i++) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                map.put(UUID.randomUUID().toString(), "");
            }
        }).start();
    }

    解决方案有Hashtable和Collections.synchronizedMap(hashMap),不过这两个方案基本上是对读写进行加锁操作,一个线程在读写元素,其余线程必须等待,性能可想而知。

    • HashTable 虽然是线程安全,但它所有涉及多线程的操作都用synchronized锁住整个table表,不管是put还是get操作都用synchronized关键字,这显然是极大的浪费,效率低下,需要将锁的颗粒度降低。
    • ConcurrentHashMap 主要就是为了应对hashmap在并发环境下不安全而诞生的,ConcurrentHashMap的设计与实现非常精巧,大量的利用了volatile,final,CAS等lock-free技术来减少锁竞争对于性能的影响。

    ConcurrentHashMap避免了对全局加锁改成了局部加锁操作,这样就极大地提高了并发环境下的操作速度,由于ConcurrentHashMap在JDK1.7和1.8中的实现非常不同,接下来我们谈谈JDK在1.7和1.8中的区别。

    JDK1.7版本的CurrentHashMap的实现原理

    在JDK1.7中ConcurrentHashMap采用了数组+Segment+分段锁的方式实现。

    ConcurrentHashMap解决了什么问题?  解决了HashTable在高并发下效率低,等待时间长的问题。

    1.Segment(分段锁)

    ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)。直接上图看concurrentHashMap结构更清晰。

     ConcurrentHashMap采用了非常精妙的"分段锁"策略,ConcurrentHashMap的主干是个Segment数组。

     final Segment<K,V>[] segments;

    HashEntry是目前我们提到的最小的逻辑处理单元了。一个ConcurrentHashMap维护一个Segment数组,一个Segment维护一个HashEntry数组。

     transient volatile HashEntry<K,V>[] table;
    static final class HashEntry<K,V> {
            final int hash;
            final K key;
            volatile V value;
            volatile HashEntry<K,V> next;
            //其他省略
    }   
    

    我们说Segment类似哈希表,那么一些属性就跟我们之前提到的HashMap差不离,比如负载因子loadFactor,比如阈值threshold等等,看下Segment的构造方法

    Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
                this.loadFactor = lf;//负载因子
                this.threshold = threshold;//阈值
                this.table = tab;//主干数组即HashEntry数组
            }
    

      我们来看下ConcurrentHashMap的构造方法

    public ConcurrentHashMap(int initialCapacity,
                                   float loadFactor, int concurrencyLevel) {
              if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
                  throw new IllegalArgumentException();
              //MAX_SEGMENTS 为1<<16=65536,也就是最大并发数为65536
              if (concurrencyLevel > MAX_SEGMENTS)
                  concurrencyLevel = MAX_SEGMENTS;
              //2的sshif次方等于ssize,例:ssize=16,sshift=4;ssize=32,sshif=5
             int sshift = 0;
             //ssize 为segments数组长度,根据concurrentLevel计算得出
             int ssize = 1;
             while (ssize < concurrencyLevel) {
                 ++sshift;
                 ssize <<= 1;
             }
             //segmentShift和segmentMask这两个变量在定位segment时会用到,后面会详细讲
             this.segmentShift = 32 - sshift;
             this.segmentMask = ssize - 1;
             if (initialCapacity > MAXIMUM_CAPACITY)
                 initialCapacity = MAXIMUM_CAPACITY;
             //计算cap的大小,即Segment中HashEntry的数组长度,cap也一定为2的n次方.
             int c = initialCapacity / ssize;
             if (c * ssize < initialCapacity)
                 ++c;
             int cap = MIN_SEGMENT_TABLE_CAPACITY;
             while (cap < c)
                 cap <<= 1;
             //创建segments数组并初始化第一个Segment,其余的Segment延迟初始化
             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);
             this.segments = ss;
         }
    

     初始化方法有三个参数,如果用户不指定则会使用默认值,initialCapacity为16,loadFactor为0.75(负载因子,扩容时需要参考),concurrentLevel为16。从上面的代码可以看出来,Segment数组的大小ssize是由concurrentLevel来决定的,但是却不一定等于concurrentLevel。知道了segment的大小就可以知道HashEntry数组的大小,计算cap的大小,即Segment中HashEntry的数组长度,cap也一定为2的n次方。

     接下来,我们来看看ConcurrentHashMap的put方法

    public V put(K key, V value) {
            Segment<K,V> s;
            //concurrentHashMap不允许key/value为空
            if (value == null)
                throw new NullPointerException();
            //hash函数对key的hashCode重新散列,避免差劲的不合理的hashcode,保证散列均匀
            int hash = hash(key);
            //返回的hash值无符号右移segmentShift位与段掩码进行位运算,定位segment
            int j = (hash >>> segmentShift) & segmentMask;
            if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
                 (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
                s = ensureSegment(j);
            return s.put(key, hash, value, false);
        }
    

      从源码看出,put的主要逻辑也就两步:1.定位segment并确保定位的Segment已初始化 2.调用Segment的put方法。

      我们来看看ConcurrentHashMap的get方法

    public V get(Object key) {
            Segment<K,V> s; 
            HashEntry<K,V>[] tab;
            int h = hash(key);
            long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;        //先定位Segment,再定位HashEntry
            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;
        }
    

      get方法无需加锁,由于其中涉及到的共享变量都使用volatile修饰,volatile可以保证内存可见性,所以不会读取到过期数据。

    来看下concurrentHashMap代理到Segment上的put方法,Segment中的put方法是要加锁的。只不过是锁粒度细了而已。

    final V put(K key, int hash, V value, boolean onlyIfAbsent) {
                HashEntry<K,V> node = tryLock() ? null :
                    scanAndLockForPut(key, hash, value);//tryLock不成功时会遍历定位到的HashEnry位置的链表(遍历主要是为了使CPU缓存链表),若找不到,则创建HashEntry。
              //tryLock一定次数后(MAX_SCAN_RETRIES变量决定),则lock。若遍历过程中,由于其他线程的操作导致链表头结点变化,则需要重新遍历。 V oldValue; try { HashEntry<K,V>[] tab = table; int index = (tab.length - 1) & hash;//定位HashEntry,可以看到,这个hash值在定位Segment时和在Segment中定位HashEntry都会用到,
                      //只不过定位Segment时只用到高几位。 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;              //若c超出阈值threshold,需要扩容并rehash。扩容后的容量是当前容量的2倍。
                  //这样可以最大程度避免之前散列好的entry重新散列,具体在另一篇文章中有详细分析,不赘述。扩容并rehash的这个过程是比较消耗资源的。 if (c > threshold && tab.length < MAXIMUM_CAPACITY) rehash(node); else setEntryAt(tab, index, node); ++modCount; count = c; oldValue = null; break; } } } finally { unlock(); } return oldValue; }  

    JDK1.8版本的CurrentHashMap的实现原理

    1.8的实现已经抛弃了Segment分段锁机制,利用CAS+Synchronized来保证并发更新的安全,底层采用数组+链表+红黑树的存储结构。

     JDK8中彻底放弃了Segment转而采用的是Node,其设计思想也不再是JDK1.7中的分段锁思想。

    Node:保存key,value及key的hash值的数据结构。其中value和next都用volatile修饰,保证并发的可见性。

    <strong>class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;
        //... 省略部分代码
    } </strong>
    

      其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发,从JDK1.7版本的ReentrantLock+Segment+HashEntry,到JDK1.8版本中synchronized+CAS+HashEntry+红黑树

    参考

    作者: dreamcatcher-cx

    出处: <http://www.cnblogs.com/chengxiao/>

  • 相关阅读:
    webrtc 手机端视频旋转
    gstreamer 命令行一些demo
    git一些命令记录
    libnice的问题记录
    webrtc ice 协商一些记录
    linux 挂在windows目录
    leetcode Permutation Sequence
    gstreamer 接收rtsp存储为h264
    uva 10285
    AndroidStudio VS Eclipse快捷键
  • 原文地址:https://www.cnblogs.com/ScarecrowAnBird/p/13056806.html
Copyright © 2011-2022 走看看