java并发编程——ConcurrentHashMap(1.7)

先分析1.7的ConcurrentHashMap,再分析1.8,最后进行对比。

ConcurrentHashMap(1.7)

特点:

  • 线程安全
    在并发中使用HashMap存储k/v数据结构时,多个线程并发put操作会导致HashMap底层链表形成环路,导致Entry的next永远不为空,导致死循环。而ConcurrentHashMap保证了线程安全

  • 高效
    相比于其他Hash结构的线程安全容器,ConcurrentHashMap有着更高的效率。HashTable的实现通过synchronized方法保证线程安全,但是多个线程必须访问同一个HashTable实例,同一时刻只允许一个线程获取锁进行方法,因此在高并发情况下产生了锁等待的低效率。
    Collections.synchronizedMap(new HashMap())
    也是类似,使用了synchronized块锁住当前SynchronizedMap对象,同样的所有并发访问都必须要等待一把锁。

什么是分段锁
ConcurrentHashMap使用分段锁(Segment)技术:一个Segment本身就是ReentrantLock的子类(也就是一把重入锁),另外每个Segment中都组合了一个HashEntry[]数组。

当访问某个数据时只需要锁住这个数据所在的Segement内组合的HashEntry[]数组即可,在其他Segment中的数据依旧可以正常方法。

也就是说,ConcurrentHashMap中,将原本一个大的Hash表(hash桶,也就是全部的HashEntry[]数组),分成n份,每一份都组合到一个Segment对象中,并且每一份使用这一份对应的Segment锁保护。

在最大程度上隔离了数据,并且把隔离的数据用Segment锁保护,达到不同线程互斥的访问。

static final class Segment<K,V> extends ReentrantLock implements Serializable {

        Segment(float lf, int threshold, HashEntry<K,V>[] tab) {
            this.loadFactor = lf;
            this.threshold = threshold;
            this.table = tab;
        }
....

}

接下来,从jdk1.7源码具体分析,对于jdk1.8的优化后续分析

初始化

一个ConcurrentHashMap包含一个 Segement[]数组,
这个数组其中一个Segement对象中又包含一个HashEntry[]数组,每个HashEntry对象代表一个链表结构。其实每个Segment对象只是对整个Hash表的一部分做了特定的包装而已

如果熟悉HashMap(1.7)结构的话,这里的每个Segement对象类似一个Concurret.Synchronized(new HashMap())。

这里写图片描述

这里写图片描述

public ConcurrentHashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    }

    public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)//参数校验
            throw new IllegalArgumentException();
        if (concurrencyLevel > MAX_SEGMENTS)//并发数大于等于MAX_SEGMENTS,2的16次方(1 << 16==65536)
            concurrencyLevel = MAX_SEGMENTS;
        // Find power-of-two sizes best matching arguments
        int sshift = 0;
        int ssize = 1;
        //获取合适的ssize:小于或等于concurrentLevel的2的N次幂
        //假设N==sshift.N也就是ssize左移的次数,一次左移位运算就是乘2一次。
        while (ssize < concurrencyLevel) {//ssize最大为2的16次方.也就是最大循环16次
            ++sshift;
            ssize <<= 1;//ssize=ssize*2
        }
        this.segmentShift = 32 - sshift;
        this.segmentMask = ssize - 1;
        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;//1 << 30,2的30次方
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)//initialCapacity/ssize有余数,则除法结果+1
            ++c;
        int cap = MIN_SEGMENT_TABLE_CAPACITY;//2
        while (cap < c)//从0开始算,最接近c的最大的2的N次冪
            cap <<= 1;
        //cap值也就是每个segment中HashEntry[]数组的size.
        //可以这么理解:把initialCapacity个数的元素放入ssize个数的segment中,每个segement需要放几个元素?如果是c个,
        //那么segement中容器的容量必须是不小于c的2次幂.initialCapacity/ssize有余数时,c会增加一个2次幂以保证容量充足
        // create segments and segments[0]
        Segment<K,V> s0 =
            new Segment<K,V>(loadFactor, (int)(cap * loadFactor),
                             (HashEntry<K,V>[])new HashEntry[cap]);//segment容量阈值threshold=cap * loadFactor
        Segment<K,V>[] ss = (Segment<K,V>[])new Segment[ssize];
        UNSAFE.putOrderedObject(ss, SBASE, s0); // ordered write of segments[0]
        this.segments = ss;
    }

初始化,其实是初始化Segement[0]与 Segement[0]对应的Segment对象中的HashEntry[],根据int initialCapacity,float loadFactor, int concurrencyLevel获取到segement[]size(ssize),HashEntry[]size(cap)。

注意:segments.length一旦生产不会改变,rehash只是针对Segement中的hashtable而言!

get()

    public V get(Object key) {// key由equals()确定唯一性
        Segment<K, V> s; // manually integrate access methods to reduce overhead
        HashEntry<K, V>[] tab;
        int h = hash(key);//h是key的hashcode二次散列值。 根据key的hashcode再做散列函数运算
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;//散列算法定位segement,u就是Segement数组的索引, Segment的散列运算,为了将不同key分散在不同segement.根据h获取segement的index
        if ((s = (Segment<K, V>) UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) {// 如果u对应的segement存在,且segement中的table也存在,则获取table中的value
            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)))// 查询到对象相同或者equals相等的key,
                                                                            // 则返回对应的value.会不会一个key,存了多个value?这里只取第一个匹配的?还是只存一个,多的value被覆盖
                    return e.value;
            }
        }
        return null;
    }

/**
     * 使用一个补充hash算法,在原有k的hashcode基础上再次进行hash运算。
     * 这个二次hash函数很重要,提高容器的存取效率,避免key的hash值冲突(散列沖突)导致segement上的存储不均衡。
     */
    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);
    }

这里写图片描述

Concurrent.hash(Object k)的作用

对key的hashcode进行再次hash运算,为了保证hash冲突最少,元素可以均衡的存放,从而保证了存取效率。如果没有这个hash函数,可能存在很多hash冲突,极端情况下导致所有元素都在一个segement中,这样存取都要获取一把segement锁,分段锁根本无效。

test:

        /*
         * 定位segment算法:
         * u就是Segment index,
         * int h = hash(key); long u = (((h >>> segmentShift) & segmentMask) <<
         * SSHIFT) + SBASE;
         */
        // 十进制
        System.out.println(Integer.valueOf("00001111", 2));// 15
        System.out.println(Integer.valueOf("00011111", 2));// 31
        System.out.println(Integer.valueOf("00111111", 2));// 63
        System.out.println(Integer.valueOf("01111111", 2));// 127
        // 使用散列算法定位出segementIndex
        System.out.println("15  segementIndex: " + ((((15 >>> 28) & 15) << 10) + 11));// 15
        System.out.println("31  segementIndex: " + ((((31 >>> 28) & 15) << 10) + 11));// 31
        System.out.println("63  segementIndex: " + ((((63 >>> 28) & 15) << 10) + 11));// 63
        System.out.println("127 segementIndex: " + ((((127 >>> 28) & 15) << 10) + 11));// 127
        // 二次hash后使用散列算法定位出segementIndex
        System.out.println("15  segementIndex: " + ((((hash(15) >>> 28) & 15) << 10) + 11));// 15
        System.out.println("31  segementIndex: " + ((((hash(31) >>> 28) & 15) << 10) + 11));// 31
        System.out.println("63  segementIndex: " + ((((hash(63) >>> 28) & 15) << 10) + 11));// 63
        System.out.println("127 segementIndex: " + ((((hash(127) >>> 28) & 15) << 10) + 11));// 127

outpu:

15  segementIndex: 11
31  segementIndex: 11
63  segementIndex: 11
127 segementIndex: 11

15  segementIndex: 11275
31  segementIndex: 1035
63  segementIndex: 12299
127 segementIndex: 8203

可以很明显的看出,经过hash函数后的Segement index明显被分散了。因为这个hash通过移位预算保证二进制的每一位都参与预算,否则低位一样,无论高位是什么,计算后的值都会冲突。

如何保证get()可见性?

HashTable中通过 synchronized get()方法保证了线程安全,但是低效。我们看看ConcurrentHashMap怎么做的:

保证读的安全性,就是要保证读取变量的原子性与可见性。

s = (Segment<K, V>) UNSAFE.getObjectVolatile(segments, u)) 
UNSAFE.getObjectVolatile(tab,((long) (((tab.length - 1) & h)) << TSHIFT) + TBASE)

保证了segement元素读取,以及(segement中)HashEntry[]数组中的元素读取的原子性、可见性。也就是通过这个方法保证了segement[]这个数组\HashEntry[]数组 读的volatile语义。通常一个数组即使声明了volatile,但也无法保证volatile语义,这里使用UNSAFE方法在操作系统层面保证线程安全。

当线程安全的读到了HashEntry元素,通过volatile声明保证其中value、next的volatile语义,从而保证线程安全:

        volatile V value;
        volatile HashEntry<K, V> next;

Put

这里写图片描述

    public V put(K key, V value) {
        Segment<K, V> s;
        if (value == null)
            throw new NullPointerException();
        int hash = hash(key);
        int j = (hash >>> segmentShift) & segmentMask;
        if ((s = (Segment<K, V>) UNSAFE.getObject //这里并不需要保证volatile语义,因为如果null则在ensureSegment保证;
                                                  //如果不为null,写操作会使用CAS保证
                                                    //nonvolatile; recheck
        (segments, (j << SSHIFT) + SBASE)) == null) // in ensureSegment
            s = ensureSegment(j);// 获取到新初始化的segment
        return s.put(key, hash, value, false);
    }
    /**
     * 根据segementIndex获取对应Segement,如果这个segem不存在,则创建一个
     * @param k
     *            the index
     * @return the segment
     */
    @SuppressWarnings("unchecked")
    private Segment<K, V> ensureSegment(int k) {
        final Segment<K, V>[] ss = this.segments;
        long u = (k << SSHIFT) + SBASE; // 计算segment对应index==u
        Segment<K, V> seg;
        if ((seg = (Segment<K, V>) UNSAFE.getObjectVolatile(ss, u)) == null) {// 原子性检查是否未被初始化
            Segment<K, V> proto = ss[0]; // 使用ss[0]作为模板,并复制产生新的segment对象
            int cap = proto.table.length;// 获取模板HashEntry[].length
            float lf = proto.loadFactor;// 获取模板加载因子
            int threshold = (int) (cap * lf);// 获取模板阈值
            HashEntry<K, V>[] tab = (HashEntry<K, V>[]) new HashEntry[cap];// 初始化新的HashEntry[]数组
            if ((seg = (Segment<K, V>) UNSAFE.getObjectVolatile(ss, u)) == null) { // recheck,防止已经被初始化;否则直接返回即可
                Segment<K, V> s = new Segment<K, V>(lf, threshold, tab);// 初始化新的segment
                // 自旋,直到初始化成功.通过CAS\getObjectVolatile保证原子性、可见性
                while ((seg = (Segment<K, V>) UNSAFE.getObjectVolatile(ss, u)) == null) {
                    if (UNSAFE.compareAndSwapObject(ss, u, null, seg = s))
                        break;
                }
            }
        }
        return seg;
    }

注意:这个put方法属于内部类
static final class Segment

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            // 没有获取到锁执行scanAndLockForPut
            HashEntry<K, V> node = tryLock() ? null : scanAndLockForPut(key, hash, value);
            V oldValue;
            // 获取到锁,保证当前segment只能被一个线程使用
            try {
                HashEntry<K, V>[] tab = table;
                int index = (tab.length - 1) & hash;
                HashEntry<K, V> first = entryAt(tab, index);// 获取HashEntry[]对应的HashEntry元素
                for (HashEntry<K, V> e = first;;) {
                    if (e != null) {// 表示HashEntry[]中存在key对应的HashEntry,按onlyIfAbsent规则进行覆盖
                        K k;
                        if ((k = e.key) == key || (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;// put结构化修改操作,modCount+1
                            }
                            break;
                        }
                        e = e.next;
                    } else {// 如果e==null,则这个key没有对应的HashEntry,则容器首次遇见这个key,初始化一个HashEntry
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K, V>(hash, key, value, first);
                        int c = count + 1;// segment中的HashEntry数组length+1.count表示HashEntry对象个数
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);
                        else
                            setEntryAt(tab, index, node);
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {
                unlock();
            }
            return oldValue;
        }
/**
         * 返回时保证锁已经被成功获取 
         * @return a new node if key not found, else null
         */
        private HashEntry<K, V> scanAndLockForPut(K key, int hash, V value) {
            HashEntry<K, V> first = entryForHash(this, hash);// 根据key的hash值读取所在的HashEntry(头Entry)
            HashEntry<K, V> e = first;
            HashEntry<K, V> node = null;
            int retries = -1; // negative while locating node
            while (!tryLock()) {
                HashEntry<K, V> f; // to recheck first below
                if (retries < 0) {
                    // 当key对应的HashEntry链不存在,则创建一个,之后实际只会执行tryLock,直到超过最大次数限制
                    if (e == null) {
                        if (node == null) // speculatively create node
                            node = new HashEntry<K, V>(hash, key, value, null);
                        retries = 0;
                    } else if (key.equals(e.key))
                        retries = 0;
                    else
                        e = e.next;
                } else if (++retries > MAX_SCAN_RETRIES) {// 如果tryLock()的执行始终返回false,超过最大次数
                    lock();// 使用lock锁,当前线程有可能进入等待队列
                    break;
                } else if ((retries & 1) == 0 && (f = entryForHash(this, hash)) != first) {
                    e = first = f; // 如果发现hash对应的HashEntry变化,也就是发生了resize,改变HashEntry
                    retries = -1;
                }
            }
            return node;
        }

rehash针对的是当前这个Segment对象中的 HashEntry[]做扩容,并且在上边的代码中对这个Segement对象做了锁。


private void rehash(HashEntry<K, V> node) {
            /*

             * 重新分配每個HashEntry[]中的元素,因为我们使用了2的左移(<<2)膨胀,所以之前每个元素要保持原有的index,或者移动2的指数位
             * 
             * 如果原有的HashEntry元素可以使用,我們要消除的HashEntry的重复创建.
             * 因为原有HashEntry的后继结点并不会被改变
             * 
             * 经过统计,在double容量时,使用默认的阈值,只有1/6的HashEntry元素需要复制。

             * 替换掉的HashEntry元素当不再被引用时会尽快回收。也许其他线程遍历老的table时,在table遍历到一半时就会发生gc.
             * HashEntry的訪問使用普通的數組索引,因为他们在table
             * volatile语义的写之后发生
             */
            HashEntry<K, V>[] oldTable = table;// oldTable指向原有table表
            int oldCapacity = oldTable.length;
            int newCapacity = oldCapacity << 1;// newCapacity==oldCapacity*2
            threshold = (int) (newCapacity * loadFactor);
            HashEntry<K, V>[] newTable = (HashEntry<K, V>[]) new HashEntry[newCapacity];//初始化一個double length的 HashEntry[]
            int sizeMask = newCapacity - 1;//新的sizeMNask,sizeMask总是length-1
            for (int i = 0; i < oldCapacity; i++) {//遍历老的table(HashEntry[])
                //因为segement的 HashEntry<K, V>[] table使用了volatile修饰,
                //并且其中value、next变量也是用了volatile修饰,并且结合Unsafe.此处可以直接使用数组索引方式读。可见性有保证
                HashEntry<K, V> e = oldTable[i];
                if (e != null) {
                    HashEntry<K, V> next = e.next;
                    int idx = e.hash & sizeMask;
                    if (next == null) //如果這個HashEntry只存儲了一个k\v隐射,也就是只有一个结点
                        newTable[idx] = e;//HashEntry放入新的Entry数组中
                    else { //当这个HashEntry中存储的k\v隐射不止一个(每个kev的hash一直)
                        HashEntry<K, V> lastRun = e;
                        int lastIdx = idx;//e新的HashEntry Index
                        //从头遍历e的链表,找到最后一个需要更新HashEntryIndex的元素 lastRun
                        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;//把这个元素lastRun之前的元素构成的链表直接引用,后边会被gc自動回收
                        // Clone remaining nodes
                        //把这个lastRun元素之前的所有元素重新初始化,当然每个HashEntry[]Index的位置都可能不同,放入新的数组位置中。
                        for (HashEntry<K, V> p = e; p != lastRun; p = p.next) {//从头到位遍历HashEntry结点,并放入新的table中
                            V v = p.value;
                            int h = p.hash;
                            int k = h & sizeMask;
                            HashEntry<K, V> n = newTable[k];
                            newTable[k] = new HashEntry<K, V>(h, p.key, v, n);
                        }
                    }
                }
            }
            int nodeIndex = node.hash & sizeMask; // add the new node
            node.setNext(newTable[nodeIndex]);
            newTable[nodeIndex] = node;
            table = newTable;
        }

可以看到rehash是针对某个segment做的扩容,而非对所有Segement。并且是先判断在扩容,然后插入新的元素。这点相对HashMap的扩容更合理,HashMap插入后再去判断扩容,有可能扩容后并不会有新元素进来,也就是失去了扩容的意义。

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

size方法的实现是遍历所有的segment,统计累计所有segment里元素的个数。

这个遍历会做两次,如果两次计算中结构性变化的次数(modCount)相等,那么就直接完成返回。如果第一次遍历统计与第二次遍历统计直接发生了结构化操作(比如、put被另一个线程执行了),那么再尝试一次,如果第二次与第三次统计的结果依旧不相等,则动用Lock锁,锁住所有的segment(其他线程的put、clear等操作被阻塞)。接下来,再次统计两次,这次由于锁保持了互斥,所以不会被其他线程干扰,modCount不会改变,sum == last,返回统计值,并且要释放所有Segement锁。

前三次计算size,是无锁的操作。以提高性能。其实这里有个优化点,第一次的size并不用真的计算出来。可以只计算后两次可能会真实使用的size即可。

当然这样的锁是非常低效的,通常并发的场景中不会发生。

在1.8中size方法是无锁的设计。每次做结构性变化时(put、remove)会通过addCount方法维护一个全局变量baseCount。size通过volatile修饰、UnSafe.CAS来保证可见性与原子性

ConcurrentHashMap(1.8)

推荐阅读(JDK1.8实现):

http://blog.csdn.net/lemon89/article/details/76212739

  • 2
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值