zoukankan      html  css  js  c++  java
  • ConcurrentHashMap源码分析

      看过hashMap源码之后一直意犹未尽的感觉,挡不住我看其他的源码了。HashMap在单线程中非常好用,也不会出现什么问题,但是一到多线程就gg了,变的不灵了。我们有HashTable可以运用在多线程程序中,但是HashTable效率太低下了,所有访问HashTable的线程都必须竞争同一把锁,当一个线程访问HashTable的同步方法时,其他线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程A使用put进行添加元素,线程B不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。那有没有什么类在这方面表现比较好的呢,这就是我们这次要说的ConcurrentHashMap了。ConcurrentHashMap是在jdk1.5开始有的,看了一下ConcurrentHashMap的源码,觉的很有意思,于是有了这篇文章。HashMap使用的是数组+链表的存储结构构,ConcurrentHashMap使用的是数组+数组的结构(锁分段)。

    什么是锁分段技术?

      容器里有多把锁,每一把锁用于锁容器其中一部分数据,多线程访问容器里不同数据段的数据时,线程间就不会存在锁竞争,从而可以有效的提高并发访问效率,这就是ConcurrentHashMap所使用的锁分段技术,首先将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。

    ConcurrentHashMap的类图

      由类图可以看出ConcurrentHashMap是由Segment数组结构和HashEntry数组结构组成。Segment继承了ReentrantLock,是一种可重入锁ReentrantLock(可重入锁,也叫做递归锁,指的是同一线程外层函数获得锁之后 ,内层递归函数仍然有获取该锁的代码,但不受影响。在JAVA环境下 ReentrantLock 和synchronized 都是可重入锁),在ConcurrentHashMap里扮演锁的角色,HashEntry则用于存储键值对数据。一个ConcurrentHashMap里包含一个Segment数组,Segment的结构和HashMap类似,是一种数组和链表结构, 一个Segment里包含一个HashEntry数组,每个HashEntry是一个链表结构的元素, 每个Segment守护着一个HashEntry数组里的元素,当对HashEntry数组的数据进行修改时,必须首先获得它对应的Segment锁。

    ConcurrentHashMap的结构

    ConcurrentHashMap初始化

    public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel) {
            if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)//如果参数为负数,直接抛出异常
                throw new IllegalArgumentException();
            if (concurrencyLevel > MAX_SEGMENTS)//segments数组大小不能超过最大值MAX_SEGMENTS=65536=1<<16
                concurrencyLevel = MAX_SEGMENTS;
            // Find power-of-two sizes best matching arguments
            int sshift = 0;
            int ssize = 1;
            while (ssize < concurrencyLevel) {//计算segments的长度,必须为2的N次方(power-0f-two)
                ++sshift;//大于或等于concurrencyLevel的最小的2的N次方值来作为segments数组的长度
                ssize <<= 1;//注意concurrencyLevel的最大大小是65535,意味着segments数组的长度最大为65536,对应的二进制是16位。
            }
            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;
        }

    ①-②的代码是初始化segmentShift和segmentMask:这两个全局变量在定位segment时的哈希算法里需要使用,sshift等于ssize从1向左移位的次数,在默认情况下concurrencyLevel等于16,1需要向左移位移动4次,所以sshift等于4。segmentShift用于定位参与hash运算的位数,segmentShift等于32减sshift,所以等于28,这里之所以用32是因为ConcurrentHashMap里的hash()方法输出的最大数是32位的,后面的测试中我们可以看到这点。segmentMask是哈希运算的掩码,等于ssize减1,即15,掩码的二进制各个位的值都是1。因为ssize的最大长度是65536,所以segmentShift最大值是16,segmentMask最大值是65535,对应的二进制是16位,每个位都是1。

    ②-③的代码是初始化Segment:输入参数initialCapacity是ConcurrentHashMap的初始化容量,loadfactor是每个segment的负载因子,在构造方法里需要通过这两个参数来初始化数组中的每个segment。代码中的变量cap就是segment里HashEntry数组的长度,它等于initialCapacity除以ssize的倍数c,如果c大于1,就会取大于等于c的2的N次方值,所以cap不是1,就是2的N次方。segment的容量threshold=(int)cap*loadFactor,默认情况下initialCapacity等于16,loadfactor等于0.75,通过运算cap等于1,threshold等于零。

    ConcurrentHashMap的get方法和Segment的定位

      ConcurrentHashMap跟hashMap的作用是一样的,在使用的过程中,包括插入元素和获取元素肯定也需要定位到分段锁Segment。那它是怎么定位的呢?我们通过分析get方法的过程中顺便把Segment的定位给分析完。

    public V get(Object key) {
            Segment<K,V> s; // manually integrate access methods to reduce overhead  先定义一个Segment
            HashEntry<K,V>[] tab;//定义一个HashEntry
            int h = hash(key);//hash定位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;
        }
     private int hash(Object k) {
            int h = hashSeed;
    
            if ((0 != h) && (k instanceof String)) {
                return sun.misc.Hashing.stringHash32((String) k);
            }
    
            h ^= k.hashCode();
    
            // Spread bits to regularize both segment and index locations,
            // using variant of single-word Wang/Jenkins hash.
            h += (h <<  15) ^ 0xffffcd7d;
            h ^= (h >>> 10);
            h += (h <<   3);
            h ^= (h >>>  6);
            h += (h <<   2) + (h << 14);
            return h ^ (h >>> 16);
        }

    ConcurrentHashMap的Put操作

        public V put(K key, V value) {
            Segment<K,V> s;
            if (value == null)//value不能为null
                throw new NullPointerException();
            int hash = hash(key);//获得hash位置
            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方法里需要对共享变量进行写入操作,所以为了线程安全,在操作共享变量时必须得加锁。Put方法首先定位到Segment,然后在Segment里进行插入操作。插入操作需要经历两个步骤,第一步判断是否需要对Segment里的HashEntry数组进行扩容,第二步定位添加元素的位置然后放在HashEntry数组里。

      是否需要扩容。在插入元素前会先判断Segment里的HashEntry数组是否超过容量(threshold),如果超过阀值,数组进行扩容。值得一提的是,Segment的扩容判断比HashMap更恰当,因为HashMap是在插入元素后判断元素是否已经到达容量的,如果到达了就进行扩容,但是很有可能扩容之后没有新元素插入,这时HashMap就进行了一次无效的扩容。

        private Segment<K,V> ensureSegment(int k) {
            final Segment<K,V>[] ss = this.segments;
            long u = (k << SSHIFT) + SBASE; // raw offset
            Segment<K,V> seg;
            if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u)) == null) {
                Segment<K,V> proto = ss[0]; // use segment 0 as prototype
                int cap = proto.table.length;
                float lf = proto.loadFactor;
                int threshold = (int)(cap * lf);
                HashEntry<K,V>[] tab = (HashEntry<K,V>[])new HashEntry[cap];
                if ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                    == null) { // recheck
                    Segment<K,V> s = new Segment<K,V>(lf, threshold, tab);
                    while ((seg = (Segment<K,V>)UNSAFE.getObjectVolatile(ss, u))
                           == null) {
                        if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                            break;
                    }
                }
            }
            return seg;
        }

      如何扩容。扩容的时候首先会创建一个两倍于原容量的数组,然后将原数组里的元素进行再hash后插入到新的数组里。为了高效ConcurrentHashMap不会对整个容器进行扩容,而只对某个segment进行扩容。

    ConcurrentHashMap的size操作

     public int size() {
            // Try a few times to get accurate count. On failure due to
            // continuous async changes in table, resort to locking.
            final Segment<K,V>[] segments = this.segments;//获得segments
            int size;//定义size
            boolean overflow; // true if size overflows 32 bits
            long sum;         // sum of modCounts
            long last = 0L;   // previous sum
            int retries = -1; // first iteration isn't retry
            try {
                for (;;) {//死循环
                    if (retries++ == RETRIES_BEFORE_LOCK) {// static final int RETRIES_BEFORE_LOCK = 2  2次没有锁住的情况
                        for (int j = 0; j < segments.length; ++j)
                            ensureSegment(j).lock(); // force creation
                    }
                    sum = 0L;
                    size = 0;
                    overflow = false;
                    for (int j = 0; j < segments.length; ++j) {
                        Segment<K,V> seg = segmentAt(segments, j);
                        if (seg != null) {
                            sum += seg.modCount;
                            int c = seg.count;
                            if (c < 0 || (size += c) < 0)
                                overflow = true;
                        }
                    }
                    if (sum == last)
                        break;
                    last = sum;
                }
            } finally {
                if (retries > RETRIES_BEFORE_LOCK) {
                    for (int j = 0; j < segments.length; ++j)
                        segmentAt(segments, j).unlock();
                }
            }
            return overflow ? Integer.MAX_VALUE : size;
        }

      要知道整个ConcurrentHashMap里元素的大小,就必须计算所有Segment里元素的大小然后求它们的和。Segment里的全局变量count是一个volatile变量,如果多线程场景下,我们是不是直接把所有Segment的count相加就可以拿到整个ConcurrentHashMap大小了?肯定不是这么简单的,虽然相加时可以获取每个Segment的count的最新值(volatile变量),但是拿到之后可能累加前使用的count发生了变化,那么统计结果就不一定准确了。所以最安全的做法,是在统计size的时候把所有Segment的put,remove和clean等方法全部锁住,这样就不会出现上面说的情况,但是这种做法显然非常低效。因为在累加count操作过程中,之前累加过的count发生变化的概率太小,所以ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?使用modCount变量,在put , remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。

  • 相关阅读:
    Mybatis多层嵌套查询
    UUID 唯一性实现原理
    oracle 多实例启动
    orcal启动多实例是报 ORA-00845: MEMORY_TARGET not supported onthis system
    java调用quartz 2.2.2方法总结。
    mybatis中like的使用(模糊查询)
    Orcal数据库实现主键ID自增
    spring cloud分布式关于熔断器
    spring cloud分布式健康检查
    spring cloud分布式整合zipkin的链路跟踪
  • 原文地址:https://www.cnblogs.com/liaoweipeng/p/6343674.html
Copyright © 2011-2022 走看看