赵计刚
每天进步一点点

注:在看这篇文章之前,如果对HashMap的层不清楚的话,建议先去看看HashMap源码解析。

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

1、对于ConcurrentHashMap需要掌握以下几点

  • Map的创建:ConcurrentHashMap()
  • 往Map中添加键值对:即put(Object key, Object value)方法
  • 获取Map中的单个对象:即get(Object key)方法
  • 删除Map中的对象:即remove(Object key)方法
  • 判断对象是否存在于Map中:containsKey(Object key)
  • 遍历Map中的对象:即keySet().iterator(),在实际中更常用的是增强型的for循环去做遍历

2、ConcurrentHashMap的创建

注:在往下看之前,心里先有这样一个映像:ConcurrentHashMap的数据结构:一个指定个数的Segment数组,数组中的每一个元素Segment相当于一个HashTable

2.1、使用方法:

Map<String, Object> map = new ConcurrentHashMap<String, Object>();

2.2、源代码:

 ConcurrentHashMap相关属性:

    /**
     * 用于分段
     */
    // 根据这个数来计算segment的个数,segment的个数是仅小于这个数且是2的几次方的一个数(ssize)
    static final int DEFAULT_CONCURRENCY_LEVEL = 16;
    // 最大的分段(segment)数(2的16次方)
    static final int MAX_SEGMENTS = 1 << 16;
    
    /**
     * 用于HashEntry
     */
    // 默认的用于计算Segment数组中的每一个segment的HashEntry[]的容量,但是并不是每一个segment的HashEntry[]的容量
    static final int DEFAULT_INITIAL_CAPACITY = 16;
    // 默认的加载因子(用于resize)
    static final float DEFAULT_LOAD_FACTOR = 0.75f;
    // 用于计算Segment数组中的每一个segment的HashEntry[]的最大容量(2的30次方)
    static final int MAXIMUM_CAPACITY = 1 << 30;

    /**
     * segments数组
     * 每一个segment元素都看做是一个HashTable
     */
    final Segment<K, V>[] segments;
    
    /**
     * 用于扩容
     */
    final int segmentMask;// 用于根据给定的key的hash值定位到一个Segment
    final int segmentShift;// 用于根据给定的key的hash值定位到一个Segment
View Code

Segment类(ConcurrentHashMap的内部类)

    /**
     * 一个特殊的HashTable
     */
    static final class Segment<K, V> extends ReentrantLock implements
            Serializable {

        private static final long serialVersionUID = 2249069246763182397L;

        transient volatile int count;// 该Segment中的包含的所有HashEntry中的key-value的个数
        transient int modCount;// 并发标记

        /*
         * 元素个数超出了这个值就扩容 threshold==(int)(capacity * loadFactor)
         * 值得注意的是,只是当前的Segment扩容,所以这是Segment自己的一个变量,而不是ConcurrentHashMap的
         */
        transient int threshold;
        transient volatile HashEntry<K, V>[] table;// 链表数组
        final float loadFactor;

        /**
         * 这里要注意一个很不好的编程习惯,就是小写l,容易与数字1混淆,所以最好不要用小写l,可以改为大写L
         */
        Segment(int initialCapacity, float lf) {
            loadFactor = lf;//每个Segment的加载因子
            setTable(HashEntry.<K, V> newArray(initialCapacity));
        }

        /**
         * 创建一个Segment数组,容量为i
         */
        @SuppressWarnings("unchecked")
        static final <K, V> Segment<K, V>[] newArray(int i) {
            return new Segment[i];
        }

        /**
         * Sets table to new HashEntry array. Call only while holding lock or in
         * constructor.
         */
        void setTable(HashEntry<K, V>[] newTable) {
            threshold = (int) (newTable.length * loadFactor);// 设置扩容值
            table = newTable;// 设置链表数组
        }
View Code

说明:只列出了Segement的全部属性和创建ConcurrentHashMap时所用到的方法。

HashEntry类(ConcurrentHashMap的内部类)

    /**
     * Segment中的HashEntry节点 类比HashMap中的Entry节点
     */
    static final class HashEntry<K, V> {
        final K key;//
        final int hash;//hash值
        volatile V value;// 实现线程可见性
        final HashEntry<K, V> next;// 下一个HashEntry

        HashEntry(K key, int hash, HashEntry<K, V> next, V value) {
            this.key = key;
            this.hash = hash;
            this.next = next;
            this.value = value;
        }

        /*
         * 创建HashEntry数组,容量为传入的i
         */
        @SuppressWarnings("unchecked")
        static final <K, V> HashEntry<K, V>[] newArray(int i) {
            return new HashEntry[i];
        }
    }
View Code

ConcurrentHashMap(int initialCapacity,float loadFactor,int concurrencyLevel)

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

注意:这个方法里边我在头部所写的注释非常重要,在这块注释写明了:

  • 每一个参数的作用
  • 整个ConcurrentHashMap的一个创建步骤(以默认的参数值为例)

ConcurrentHashMap()

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

该方法调用了上边的三参构造器。

五点注意:

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

两点改进:

在Concurrenthashmap的构造过程中,相对于JDK的代码,有两点改进:

  • 在遍历Segment数组为每一个数组元素实例化的时候,可以直接写作i<ssize,而不必在每次循环时都去计算this.segments.length,JDK代码如下,可以按照代码中的注释做优化
            for (int i = 0; i < this.segments.length; ++i)
                // 这一块this.segments.length就是ssize,为了不去计算这个值,可以直接改成i<ssize
                this.segments[i] = new Segment<K, V>(cap, loadFactor);
    View Code
  • 另外一个,就是在程序中,尽量少用小写"l",容易与数字"1"混淆,要么不用"l",若要用的话,就用大写"L",JDK代码如下,可参照注释进行优化:
            /**
             * 这里要注意一个很不好的编程习惯,就是小写l,容易与数字1混淆,所以最好不要用小写l,可以改为大写L
             */
            Segment(int initialCapacity, float lf) {
                loadFactor = lf;//每个Segment的加载因子
                setTable(HashEntry.<K, V> newArray(initialCapacity));
            }
    View Code

一个疑问:

  • 在默认情况下,创建出的HashEntry[]数组的容量为1,而每一个Segment的扩容因子threshold,一开始算出来是0,即开始put第一个元素就要扩容,不太理解JDK为什么这样做。在我们实际开发中,其实空间有的是,所以我们一般会采用"以适当的空间换取时间"的方式,所以我们会适当的扩大HashEntry[],以确保在put数据的时候尽量减少扩容才对,但是JDK这样做到底是为了什么?是为了减少空间吗?还是我本身的理解就有问题?求大神指点!!!
  • 注意我上边说的适当容量,是因为如果容量设的太大,可能会导致某个HashEntry[i]中的HashEntry链表过长,进而影响查询的效率,容量设的太小的话,有需要不断扩容,影响插入效率。

3、put(Object key, Object value)

上述方法,若添加已有key的key-value对,则新值覆盖旧值。

putIfAbsent(K key, V value):若添加已有key的key-value对,直接返回旧值,则新值相当于没有添加。

使用方法:

map.put("hello", "world");

源代码:

ConcurrentHashMap的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);
    }
View Code

 注意:

  • key和value都不可为null,这一点与HashMap不同,但是从代码来看,并没有判断key为空的情况,这一段代码在哪里呢?为了可读性,建议将判断的地方改为如下代码
            if (key == null || value == null)
                throw new NullPointerException();
    View Code
  • 注释部分写明了整个插入流程,详细的流程步骤见代码,这里列出大致流程
    • 根据key获取key.hashCode的hash值
    • 根据hash值算出将要插入的Segment
    • 根据hash值与Segment中的HashEntry的容量-1按位与获取将要插入的HashEntry的index
    • 若HashEntry[index]中的HashEntry链表有与插入元素相同的key和hash值,根据onlyIfAbsent决定是否替换旧值
    • 若没有相同的key和hash,直接返回将新节点插入链头,原来的头节点设为新节点的next(采用的方式与HashMap一致,都是HashEntry替换的方法)

 

Segment的put(K key, int hash, V value, boolean onlyIfAbsent)

        /**
         * 往当前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();//手工释放锁
            }
        }
View Code

注意:在注释中已经写明了,这里还是要写一下

  • onlyIfAbsent-->false如果有旧值存在,新值覆盖旧值,返回旧值;true如果有旧值存在,则直接返回旧值,相当于不添加元素
  • ReentrantLock的用法:必须手工释放锁。可实现Synchronized的效果,原子性。
  • volatile需要配合锁去使用才能实现原子性,否则在多线程操作的情况下依然不够用,在程序中,count变量(当前Segment中的key-value对个数)通过volatile修饰,实现内存可见性(关于内存可见性以后会仔细去记录,这里列出大概的一个流程)在有锁保证了原子性的情况下
    • 当我们读取count变量的时候,会强制从主内存中读取count的最新值
    • 当我们对count变量进行赋值之后,会强制将最新的count值刷到主内存中去
    • 通过以上两点,我们可以保证在高并发的情况下,执行这段流程的线程可以读取到最新值
  • 在这里的ReentrantLock与volatile结合的用法值得我们学习

 补:volatile的介绍见《附2 volatile》,链接如下:

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

hash(int h)

    /**
     * 对key.hashCode()进行hash计算
     * @param h key.hashCode()
     */
    private static int hash(int h) {
        // 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);
    }
View Code

segmentFor(int hash)

    /**
     * 根据给定的key的hash值定位到一个Segment
     * @param hash
     */
    final Segment<K, V> segmentFor(int hash) {
        return segments[(hash >>> segmentShift) & segmentMask];
    }
View Code

注意:hash(int h)与segmentFor(int hash)这两个方法应该会尽量将key的hash值打散,从而保证尽可能多的同时在多个Segment上进行put操作,而不是在同一个Segment上执行多个put操作,这样之后,在同一个Segment中,要尽可能的保证向HashEntry[]的不同元素上进行put,而不是向同一个元素上一直put,以上两个函数究竟是怎样保证实现这样的将hash打散的效果呢?求大神指点啊!!!

rehash()

JDK的实现代码:

        /**
         * 步骤:
         * 需要注意的是:同一个桶下边的HashEntry链表中的每一个元素的hash值不一定相同,只是hash&(table.length-1)的结果相同
         * 1)创建一个新的HashEntry数组,容量为旧数组的二倍
         * 2)计算新的threshold
         * 3)遍历旧数组的每一个元素,对于每一个元素
         * 3.1)根据头节点e重新计算将要存入的新数组的索引idx
         * 3.2)若整个链表只有一个节点e,则是直接将e赋给newTable[idx]即可
         * 3.3)若整个链表还有其他节点,先算出最后一个节点lastRun的位置lastIdx,并将最后一个节点赋值给newTable[lastIdx]
         * 3.4)最后将从头节点开始到最后一个节点之前的所有节点计算其将要存储的索引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) {
                    HashEntry<K, V> next = e.next;
                    int idx = e.hash & sizeMask;//重新按位与计算将要存放的新数组中的索引

                    
                    if (next == null)//如果是只有一个头节点,只需将头节点设置到newTable[idx]即可
                        newTable[idx] = e;
                    else {
                        // Reuse trailing consecutive sequence at same slot
                        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;//存放最后一个元素

                        // Clone all remaining nodes
                        for (HashEntry<K, V> p = e; p != lastRun; 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;
        }
View Code

个人感觉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;
        }
View Code

注意点:

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

总结:ConcurrentHashMap基于concurrencyLevel划分出多个Segment来存储key-value,这样的话put的时候只锁住当前的Segment,可以避免put的时候锁住整个map,从而减少了并发时的阻塞现象。

 

4、get(Object key)

使用方法:

map.get("hello");

源代码:

 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);
    }
View Code

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;
        }
View Code

Segment的getFirst(int hash)

        /**
         * 根据hash值找出HashEntry数组中的索引index,并返回HashEntry[index]
         */
        HashEntry<K, V> getFirst(int hash) {
            HashEntry<K, V>[] tab = table;
            return tab[hash & (tab.length - 1)];
        }
View Code

Segment的readValueUnderLock(HashEntry<K, V> e)

        V readValueUnderLock(HashEntry<K, V> e) {
            lock();
            try {
                return e.value;
            } finally {
                unlock();
            }
        }
View Code

注意点:

 

  • 注释很重要,一定要看
  • 注释已经写明了详细流程,这里说一下大致流程:
    • 根据key获取hash值
    • 根据hash值找到相应的Segment
    • 根据hash值找出Segment中的哪一个HashEntry[index]
    • 遍历整个HashEntry[index]链表,找出hash和key与给定参数相等的HashEntry,例如e
      • 如没找到e,返回null
      • 如找到e,获取e.value
        • 如果e.value!=null,直接返回
        • 如果e.value==null,则先加锁,等并发的put操作将value设置成功后,再返回value值
  • 对于get操作而言,基本没有锁,只有当找到了e且e.value等于null,有可能是当下的这个HashEntry刚刚被创建,value属性还没有设置成功,这时候我们读到是该HashEntry的value的默认值null,所以这里加锁,等待put结束后,返回value值
  • 据说,上边这一点还没有发生过

 

5、remove(Object key)

使用方法:

map.remove("hello");

源代码:

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);
    }
View Code

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();
            }
        }
View Code

注意:具体的实现方式看注释,个人感觉比较绕,所以有兴趣的朋友可以按照如下步骤实现了一遍:(实现的过程可以参照HashMap的remove(Object key))

  • 根据key获取hash值
  • 根据hash值获取Segment
  • 获取将要删除的元素所在的HashEntry[index]
  • 遍历链表
    • 若没有hash和key都与指定参数相同的节点e,返回null
    • 若有e,删除指定节点e,并将e的前一个节点的next指向e的下一个节点,之后count-1

 

6、containsKey(Object key)

使用方法:

map.containsKey("hello")
View Code

源代码:

 ConcurrentHashMap的containsKey(Object key)

    /**
     * 是否包含指定key的数据
     * 步骤:
     * 1)根据key计算hash值
     * 2)根据hash获取相应的Segment
     * 调用Segment的containsKey(Object key, int hash)
     * 3)根据hash值找出HashEntry数组中的索引index,并返回HashEntry[index]
     * 4)遍历整个HashEntry[index]链表,找出hash和key与给定参数相等的HashEntry,例如e,
     * 4.1)如找到e,返回true
     * 4.2)如没找到e,返回false
     */
    public boolean containsKey(Object key) {
        int hash = hash(key.hashCode());
        return segmentFor(hash).containsKey(key, hash);
    }
View Code

Segment的containsKey(Object key, int hash)

        boolean containsKey(Object key, int hash) {
            if (count != 0) { // read-volatile
                HashEntry<K, V> e = getFirst(hash);
                while (e != null) {
                    if (e.hash == hash && key.equals(e.key))
                        return true;
                    e = e.next;
                }
            }
            return false;
        }
View Code

说明:代码清晰简单,流程步骤查看注释即可

 

7、keySet().iterator()

使用方法:

        Map<String, Object> map = new ConcurrentHashMap<String, Object>();
        map.put("hello3", "world2");
        map.put("hello2", "world");
        for(String key : map.keySet()){
            System.out.println(map.get(key));
        }
View Code

源代码不写了。

流程:

遍历每个Segment中的HashEntry[],完成所有对象的读取,不加锁。

 

8、size()

源代码:

    /**
     * 计算map中的key-value对总数
     * 步骤:
     * 1)遍历所有段,计算总的count值sum,计算总的modCount值
     * 2)如果有数据的话(modCount!=0),再遍历所有段一遍,计算总的count值check,在这期间只要有一个段的modCount发生了变化,就再重复如上动作两次
     * 3)若三次后,还未成功,遍历所有Segment,分别加锁(即建立全局锁),然后计算,最后释放所有锁
     */
    public int size() {
        final Segment<K, V>[] segments = this.segments;
        long sum = 0;//总量
        long check = 0;//标志位
        int[] mc = new int[segments.length];//存放每个段的modCount
        
        
        for (int k = 0; k < RETRIES_BEFORE_LOCK; ++k) {
            check = 0;
            sum = 0;//总的count值
            int mcsum = 0;//总的modCount值
            for (int i = 0; i < segments.length; ++i) {//遍历所有段
                sum += segments[i].count;//计算总的count值
                mcsum += mc[i] = segments[i].modCount;//计算总的modCount值
            }
            if (mcsum != 0) {//有数据的话,再检查一遍
                for (int i = 0; i < segments.length; ++i) {
                    check += segments[i].count;//计算总的count
                    if (mc[i] != segments[i].modCount) {//只要有一个段发生了变化(在遍历期间发生了增删变化)
                        check = -1; 
                        break;//跳出所有循环
                    }
                }
            }
            if (check == sum)//成功
                break;
        }
        if (check != sum) { //以上三次都为成功的话
            sum = 0;
            //每一个段全部加锁(相当于加了一个全局锁)
            for (int i = 0; i < segments.length; ++i)
                segments[i].lock();
            //进行统计
            for (int i = 0; i < segments.length; ++i)
                sum += segments[i].count;
            //全部解锁
            for (int i = 0; i < segments.length; ++i)
                segments[i].unlock();
        }
        if (sum > Integer.MAX_VALUE)
            return Integer.MAX_VALUE;
        else
            return (int) sum;
    }
View Code

在不加锁的情况下遍历所有Segment,读取每个Segment的count和modCount,并进行统计;

完毕后,再遍历一遍所有Segment,比较modCount,是否发生了变化,若发生了变化,则再重复如上动作两次;

若三次后,还未成功,遍历所有Segment,分别加锁(即建立全局锁),然后计算,最后释放所有锁。

注:以如上的方式,大部分情况下,不需要加锁就可以获取size()

 

总结:

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

jdk1.8 concurrentHashMap的实现

posted on 2016-01-08 15:12  赵计刚  阅读(6813)  评论(3编辑  收藏  举报