ConcurrentHashMap源码分析2(总结)

来源:互联网 发布:淘宝网风骑士俱乐部 编辑:程序博客网 时间:2024/06/05 03:47

一些问题总结:

  1. 最大的分段(segment)数为2的16次方,每一个segment的HashEntry[]的最大容量为2的30次方。
    默认的分段数和每个segment的HashEntry[]的初始容量均为16。segment的默认加载因子为0.75。

  2. 定位segment段需要用的两个参数:segmentMask,segmentShift。
    这两个参数在构造函数中进行处理,如下:

//ssize为经传入的参数concurrencyLevel计算得到的segment数组的大小,sshift为移位次数int sshift = 0;int ssize = 1;// segment数组的长度while (ssize < concurrencyLevel) {    ++sshift;    ssize <<= 1;}segmentShift = 32 - sshift;// eg.segmentShift==32-4=28 segmentMask = ssize - 1;// eg.segmentMask==16-1==15 
/**     * 根据给定的key的hash值定位到一个Segment     * @param hash     */    final Segment<K, V> segmentFor(int hash) {        return segments[(hash >>> segmentShift) & segmentMask];    }

3.Segment类(ConcurrentHashMap的内部类):继承ReentrantLock类
属性:
count(用变量volatile修饰),表示该Segment中的包含的所有HashEntry中的key-value的个数。
HashEntry[] table(用变量volatile修饰),表示该Segment中的链表数组。
modCount,并发标记。
threshold,元素个数超出了这个值就扩容 threshold==(int)(capacity * loadFactor),注意是当前segment进行扩容。

4.HashEntry节点的定义:

static final class HashEntry<K, V> {        final K key;// 键        final int hash;//hash值        volatile V value;// 实现线程可见性        final HashEntry<K, V> next;// 下一个HashEntry        .........}

注意的是:值value为volatile类型,其他均为final,前者意味着对其他线程可见,后者意味着next不可以修改,在删除操作时,若删除目标节点在链表非头节点位置,需要将其前面部分进行copy,倒序连接。copy之前的那一份交给垃圾回收器处理。
如1-2-3-4,删除3,将变成2-1-4。

5.三参构造函数: (int initialCapacity,float loadFactor,int concurrencyLevel)

/**     * 创建ConcurrentHashMap     * @param initialCapacity 用于计算Segment数组中的每一个segment的HashEntry[]的容量, 但是并不是每一个segment的HashEntry[]的容量     * @param loadFactor     * @param concurrencyLevel 用于计算Segment数组的大小(可以传入不是2的几次方的数,但是根据下边的计算,最终segment数组的大小ssize将是2的几次方的数)     *      * 步骤:     * 这里以默认的无参构造器参数为例,initialCapacity==16,loadFactor==0.75f,concurrencyLevel==16     * 1)检查各参数是否符合要求     * 2)根据concurrencyLevel(16),计算Segment[]的容量ssize(16)与扩容移位条件sshift(4)     * 3)根据sshift与ssize计算将来用于定位到相应Segment的参数segmentShift与segmentMask     * 4)根据ssize创建Segment[]数组,容量为ssize(16)     * 5)根据initialCapacity(16)与ssize计算用于计算HashEntry[]容量的参数c(1)     * 6)根据c计算HashEntry[]的容量cap(1)     * 7)根据cap与loadFactor(0.75)为每一个Segment[i]都实例化一个Segment     * 8)每一个Segment的实例化都做下面这些事儿:     * 8.1)为当前的Segment初始化其loadFactor为传入的loadFactor(0.75)     * 8.2)创建一个HashEntry[],容量为传入的cap(1)     * 8.3)根据创建出来的HashEntry的容量(1)和初始化的loadFactor(0.75),计算扩容因子threshold(0)     * 8.4)初始化Segment的table为刚刚创建出来的HashEntry     */    public ConcurrentHashMap(int initialCapacity,float loadFactor,int concurrencyLevel) {        // 检查参数情况        if (loadFactor <= 0f || initialCapacity < 0 || concurrencyLevel <= 0)            throw new IllegalArgumentException();        if (concurrencyLevel > MAX_SEGMENTS)            concurrencyLevel = MAX_SEGMENTS;        /**         * 找一个能够正好小于concurrencyLevel的数(这个数必须是2的几次方的数)         * eg.concurrencyLevel==16==>sshift==4,ssize==16         * 当然,如果concurrencyLevel==15也是上边这个结果         */        int sshift = 0;        int ssize = 1;// segment数组的长度        while (ssize < concurrencyLevel) {            ++sshift;            ssize <<= 1;// ssize=ssize*2        }        segmentShift = 32 - sshift;// eg.segmentShift==32-4=28 用于根据给定的key的hash值定位到一个Segment        segmentMask = ssize - 1;// eg.segmentMask==16-1==15 用于根据给定的key的hash值定位到一个Segment        this.segments = Segment.newArray(ssize);// 构造出了Segment[ssize]数组 eg.Segment[16]        /*         * 下面将为segment数组中添加Segment元素         */        if (initialCapacity > MAXIMUM_CAPACITY)            initialCapacity = MAXIMUM_CAPACITY;        int c = initialCapacity / ssize;// eg.initialCapacity==16,c==16/16==1        if (c * ssize < initialCapacity)// eg.initialCapacity==17,c==17/16=1,这时1*16<17,所以c=c+1==2            ++c;// 为了少执行这一句,最好将initialCapacity设置为2的几次方        int cap = 1;// 每一个Segment中的HashEntry[]的初始化容量        while (cap < c)            cap <<= 1;// 创建容量        for (int i = 0; i < this.segments.length; ++i)            // 这一块this.segments.length就是ssize,为了不去计算这个值,可以直接改成i<ssize            this.segments[i] = new Segment<K, V>(cap, loadFactor);    }

默认的构造函数:调用上面的构造函数

/**     * 创建ConcurrentHashMap     */    public ConcurrentHashMap() {        this(DEFAULT_INITIAL_CAPACITY, // 16                DEFAULT_LOAD_FACTOR, // 0.75f                DEFAULT_CONCURRENCY_LEVEL);// 16    }

注意:
(1)传入的concurrencyLevel只是用于计算Segment数组的大小(可以传入不是2的几次方的数,但是根据下边的计算,最终segment数组的大小ssize将是2的几次方的数),并非真正的Segment数组的大小。
(2)传入的initialCapacity只是用于计算Segment数组中的每一个segment的HashEntry[]的容量, 但是并不是每一个segment的HashEntry[]的容量,而每一个HashEntry[]的容量不是2的几次方。
(3)非常值得注意的是,在默认情况下,创建出的HashEntry[]数组的容量为1,并不是传入的initialCapacity(16),证实了上一点;而每一个Segment的扩容因子threshold,一开始算出来是0,即开始put第一个元素就要扩容,不太理解JDK为什么这样做。
(4)想要在初始化时扩大HashEntry[]的容量,可以指定initialCapacity参数,且指定时最好指定为2的几次方的一个数,这样的话,在代码执行中可能会少执行一句”c++”,具体参看三参构造器的注释。
(5)对于Concurrenthashmap的扩容而言,只会扩当前的Segment,而不是整个Concurrenthashmap中的所有Segment都扩。

6.put函数:put(Object key, Object value)

/**     * 将key-value放入map     * 注意:key和value都不可以为空     * 步骤:     * 1)计算key.hashCode()的hash值     * 2)根据hash值定位到某个Segment     * 3)调用Segment的put()方法     * Segment的put()方法:     * 1)上锁     * 2)从主内存中读取key-value对个数count     * 3)count+1如果大于threshold,执行rehash()     * 4)计算将要插入的HashEntry[]的下标index     * 5)获取HashEntry的头节点HashEntry[index]-->first     * 6)从头结点开始遍历整个HashEntry链表,     * 6.1)若找到与key和hash相同的节点,则判断onlyIfAbsent如果为false,新值覆盖旧值,返回旧值;如果为true,则直接返回旧值(相当于不添加重复key的元素)     * 6.2)若没有找到与key和hash相同的节点,则创建新节点HashEntry,并将之前的有节点作为新节点的next,即将新节点放入链头,然后将新节点赋值给HashEntry[index],将count强制写入主内存,最后返回null     */    public V put(K key, V value) {        if (key == null || value == null)            throw new NullPointerException();        int hash = hash(key.hashCode());//计算key.hashCode()的hash值        /**         * 根据hash值定位到某个Segment,调用Segment的put()方法         */        return segmentFor(hash).put(key, hash, value, false);    }

里面的segment的put源码如下:

/**         * 往当前segment中添加key-value         * 注意:         * 1)onlyIfAbsent-->false如果有旧值存在,新值覆盖旧值,返回旧值;true如果有旧值存在,则直接返回旧值,相当于不添加元素(不可添加重复key的元素)         * 2)ReentrantLock的用法         * 3)volatile只能配合锁去使用才能实现原子性         */        V put(K key, int hash, V value, boolean onlyIfAbsent) {            lock();//加锁:ReentrantLock            try {                int c = count;//当前Segment中的key-value对(注意:由于count是volatile型的,所以读的时候工作内存会从主内存重新加载count值)                if (c++ > threshold) // 需要扩容                    rehash();//扩容                HashEntry<K, V>[] tab = table;                int index = hash & (tab.length - 1);//按位与获取数组下标:与HashMap相同                HashEntry<K, V> first = tab[index];//获取相应的HashEntry[i]中的头节点                HashEntry<K, V> e = first;                //一直遍历到与插入节点的hash和key相同的节点e;若没有,最后e==null                while (e != null && (e.hash != hash || !key.equals(e.key)))                    e = e.next;                V oldValue;//旧值                if (e != null) {//table中已经有与将要插入节点相同hash和key的节点                    oldValue = e.value;//获取旧值                    if (!onlyIfAbsent)                        e.value = value;//false 覆盖旧值  true的话,就不添加元素了                } else {//table中没有与将要插入节点相同hash或key的节点                    oldValue = null;                    ++modCount;                    tab[index] = new HashEntry<K, V>(key, hash, first, value);//将头节点作为新节点的next,所以新加入的元素也是添加在链头                    count = c; //设置key-value对(注意:由于count是volatile型的,所以写的时候工作内存会立即向主内存重新写入count值)                }                return oldValue;            } finally {                unlock();//手工释放锁            }        }

注意:
(1)key和value都不可为null,这一点与HashMap不同。
(2)nlyIfAbsent–>false如果有旧值存在,新值覆盖旧值,返回旧值;true如果有旧值存在,则直接返回旧值,相当于不添加元素
(3)ReentrantLock的用法:必须手工释放锁。可实现Synchronized的效果,原子性。
(4)volatile需要配合锁去使用才能实现原子性,否则在多线程操作的情况下依然不够用,在程序中,count变量(当前Segment中的key-value对个数)通过volatile修饰,实现内存可见性。在有锁保证了原子性的情况下:
a. 当我们读取count变量的时候,会强制从主内存中读取count的最新值;
b. 当我们对count变量进行赋值之后,会强制将最新的count值刷到主内存中去。
通过以上两点,我们可以保证在高并发的情况下,执行这段流程的线程可以读取到最新值。
(5)ConcurrentHashMap基于concurrencyLevel划分出多个Segment来存储key-value,这样的话put的时候只锁住当前的Segment,可以避免put的时候锁住整个map,从而减少了并发时的阻塞现象。

7.rehash操作:在上一步put操作时,可能会引发rehash操作。(非jdk版,自行修改版)

/**         * 步骤:         * 需要注意的是:同一个桶下边的HashEntry链表中的每一个元素的hash值不一定相同,只是hash&(table.length-1)的结果相同         * 1)创建一个新的HashEntry数组,容量为旧数组的二倍         * 2)计算新的threshold         * 3)遍历旧数组的每一个元素,对于每一个元素(即一个链表)         * 3.1)获取头节点e         * 3.2)从头节点开始到最后一个节点(null之前的那个节点)的所有节点计算其将要存储的索引k,然后创建新节点,将新节点赋给newTable[k],并将之前newTable[k]上存在的节点作为新节点的下一节点         */        void rehash() {            HashEntry<K, V>[] oldTable = table;            int oldCapacity = oldTable.length;            if (oldCapacity >= MAXIMUM_CAPACITY)                return;            HashEntry<K, V>[] newTable = HashEntry.newArray(oldCapacity << 1);//扩容为原来二倍            threshold = (int) (newTable.length * loadFactor);//计算新的扩容临界值            int sizeMask = newTable.length - 1;            for (int i = 0; i < oldCapacity; i++) {//遍历每一个数组元素                // We need to guarantee that any existing reads of old Map can                // proceed. So we cannot yet null out each bin.                HashEntry<K, V> e = oldTable[i];//头节点                if (e != null) {                    for (HashEntry<K, V> p = e; p != null; p = p.next) {//遍历数组元素中的链表                        int k = p.hash & sizeMask;                        HashEntry<K, V> n = newTable[k];//获取newTable[k]已经存在的HashEntry,并将此HashEntry赋给n                        //创建新节点,并将之前的n作为新节点的下一节点                        newTable[k] = new HashEntry<K, V>(p.key, p.hash, n,p.value);                    }                }            }            table = newTable;        }

注意:同一个桶下边的HashEntry链表中的每一个元素的hash值不一定相同,只是index = hash&(table.length-1)的结果相同,当table.length发生变化时,同一个桶下各个HashEntry算出来的index会不同。

8.get操作
ConcurrentHashMap的get(Object key)

/**     * 根据key获取value     * 步骤:     * 1)根据key获取hash值     * 2)根据hash值找到相应的Segment     * 调用Segment的get(Object key, int hash)     * 3)根据hash值找出HashEntry数组中的索引index,并返回HashEntry[index]     * 4)遍历整个HashEntry[index]链表,找出hash和key与给定参数相等的HashEntry,例如e,     * 4.1)如没找到e,返回null     * 4.2)如找到e,获取e.value     * 4.2.1)如果e.value!=null,直接返回     * 4.2.2)如果e.value==null,则先加锁,等并发的put操作将value设置成功后,再返回value值     */    public V get(Object key) {        int hash = hash(key.hashCode());        return segmentFor(hash).get(key, hash);    }

Segment的get(Object key, int hash)

/**         * 根据key和hash值获取value         */        V get(Object key, int hash) {            if (count != 0) { // read-volatile                HashEntry<K, V> e = getFirst(hash);//找到HashEntry[index]                while (e != null) {//遍历整个链表                    if (e.hash == hash && key.equals(e.key)) {                        V v = e.value;                        if (v != null)                            return v;                        /*                         * 如果V等于null,有可能是当下的这个HashEntry刚刚被创建,value属性还没有设置成功,                         * 这时候我们读到是该HashEntry的value的默认值null,所以这里加锁,等待put结束后,返回value值                         */                        return readValueUnderLock(e);                     }                    e = e.next;                }            }            return null;        }
/**         * 根据hash值找出HashEntry数组中的索引index,并返回HashEntry[index]         */        HashEntry<K, V> getFirst(int hash) {            HashEntry<K, V>[] tab = table;            return tab[hash & (tab.length - 1)];        }
        V readValueUnderLock(HashEntry<K, V> e) {            lock();            try {                return e.value;            } finally {                unlock();            }        }

注意:get操作基本不用加锁。这个理解起来需要知道两点:
(1)第一步是访问count变量,这是一个volatile变量,由于所有的修改操作在进行结构修改时都会在最后一步写count 变量,通过这种机制保证get操作能够得到几乎最新的结构更新。对于非结构更新,也就是结点值的改变,由于HashEntry的value变量是 volatile的,也能保证读取到最新的值。
(2)对得到key相对应的值v是否为null的判断。如果v等于null,有可能是当下的这个HashEntry刚刚被创建,value属性还没有设置成功,这时候我们读到是该HashEntry的value的默认值null,所以这里加锁,等待put结束后,返回value值。

9.remove操作
ConcurrentHashMap的remove(Object key)

/**     * 删除指定key的元素     * 步骤:     * 1)根据key获取hash值     * 2)根据hash值获取Segment     * 调用Segment的remove(Object key, int hash, Object value)     * 1)count-1     * 2)获取将要删除的元素所在的HashEntry[index]     * 3)遍历链表,     * 3.1)若没有hash和key都与指定参数相同的节点e,返回null     * 3.2)若有e,删除指定节点e,并将e之前的节点重新排序后,将排序后的最后一个节点的下一个节点指定为e的下一个节点     * (很绕,不知道JDK为什么这样实现)     */    public V remove(Object key) {        int hash = hash(key.hashCode());        return segmentFor(hash).remove(key, hash, null);    }

Segment的remove(Object key, int hash, Object value)

V remove(Object key, int hash, Object value) {            lock();            try {                int c = count - 1;//key-value对个数-1                HashEntry<K, V>[] tab = table;                int index = hash & (tab.length - 1);                HashEntry<K, V> first = tab[index];//获取将要删除的元素所在的HashEntry[index]                HashEntry<K, V> e = first;                //从头节点遍历到最后,若未找到相关的HashEntry,e==null,否则,有                while (e != null && (e.hash != hash || !key.equals(e.key)))                    e = e.next;                V oldValue = null;                if (e != null) {//将要删除的节点e                    V v = e.value;                    if (value == null || value.equals(v)) {                        oldValue = v;                        // All entries following removed node can stay                        // in list, but all preceding ones need to be                        // cloned.                        ++modCount;                        HashEntry<K, V> newFirst = e.next;                        /*                         * 从头结点遍历到e节点,这里将e节点删除了,但是删除节点e的前边的节点会倒序                         * eg.原本的顺序:E3-->E2-->E1-->E0,删除E1节点后的顺序为:E2-->E3-->E0                         * E1前的节点倒序排列了                         */                        for (HashEntry<K, V> p = first; p != e; p = p.next)                            newFirst = new HashEntry<K, V>(p.key, p.hash, newFirst, p.value);                        tab[index] = newFirst;                        count = c; // write-volatile                    }                }                return oldValue;            } finally {                unlock();            }        }

注意:
(1)remove操作需要加锁
(2)之前介绍过HashEntry结构时,它的值value为volatile类型,其他均为final,前者意味着对其他线程可见,后者意味着next不可以修改,在删除操作时,若删除目标节点在链表非头节点位置,需要将其前面部分进行copy,倒序连接。copy之前的那一份交给垃圾回收器处理。
如1-2-3-4,删除3,将变成2-1-4。

10.containsKey(Object key)/keySet().iterator() 不加锁,比较简单,就不贴源码了。

11.size() (jdk1.7)

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;        int 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 (;;) {                //retry两次,RETRIES_BEFORE_LOCK=2                //当两次均是当前modCount与前一次不一致时,则进行加锁统计总数                if (retries++ == RETRIES_BEFORE_LOCK) {                    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;                    }                }                //若当前modCount总数和前一次modCount总数相同,表示没有其他操作,循环跳出                //否则继续进行下次retry                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;    }

注意:
(1)统计所有Segment里元素的大小然后求它们的和,如果直接将各个segment中的count(volatile修饰)相加,这是不可取的。volatile只能保证得到的count是最新的,但是在相加过程中,可能有其他的线程改变count,得到的是不精确的结果
(2)最安全的做法时把segement的put,remove等操作给全部锁住,但是这种方法很低效。
(3)因为在累加count操作过程中,之前累加过的count发生变化的概率太小,所以ConcurrentHashMap的做法是先尝试2次通过不锁住Segment的方式来统计各个Segment大小,如果统计的过程中,容器的count发生了变化,则再采用加锁的方式来统计所有Segment的大小。那么ConcurrentHashMap是如何判断在统计的时候容器是否发生了变化呢?使用modCount变量,在put , remove和clean方法里操作元素前都会将变量modCount进行加1,那么在统计size前后比较modCount是否发生变化,从而得知容器的大小是否发生变化。

总结
数据结构:一个指定个数的Segment数组,数组中的每一个元素Segment相当于一个HashTable。
加锁情况(锁分离技术):
(1)put
(2)get中找到了hash与key都与指定参数相同的HashEntry,但是value==null的情况
(3)remove
(4)size():两次尝试后,还未成功,遍历所有Segment,分别加锁(即建立全局锁)

http://www.cnblogs.com/java-zhao/p/5113317.html

原创粉丝点击