Map之ConcurrentHashMap

image

一、ConcurrentHashMap源码解析(jdk1.7)

1.1 主要成员变量

    //默认容量
    static final int DEFAULT_INITIAL_CAPACITY = 16;

    //默认加载因子
    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    //Segment数组
    final Segment<K,V>[] segments;

1.2 Segment段

    static final class Segment<K,V> extends ReentrantLock implements Serializable {
    
        //链表数组
        transient volatile HashEntry<K,V>[] table;

        //Segment中元素的数量
        transient int count;

        //修改的次数
        transient int modCount;

        //阈值,段中元素的数量超过这个值就会对Segment进行扩容
        transient int threshold;

        //段的负载因子,其值等同于ConcurrentHashMap的负载因子
        final float loadFactor;
        
        ...
    }
    static final class HashEntry<K,V> {
        final int hash;
        final K key;
        volatile V value;
        volatile HashEntry<K,V> next;
    }

由此可见,ConcurrentHashMap是Segment对象数组,每个Segment都是一个hashmap,而这个Segment继承了可重入锁,ConcurrentHashMap采用了分段锁的思想来实现,对一个Segment的加锁不影响其他的Segment读写,HashEntry和value都用volatile修饰保证了内存可见性,读的时候都是最新的值,所以读不用枷锁,所以能进行高并发读写。

1.3 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          // nonvolatile; recheck
             (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
            s = ensureSegment(j);
        //根据key的hash确定是哪个Segment,再确定在Segment中的位置
        return s.put(key, hash, value, false);
    }
    
    final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            //先加锁
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
                HashEntry<K,V>[] tab = table;
                int index = (tab.length - 1) & hash;//确定索引
                HashEntry<K,V> first = entryAt(tab, index);
                for (HashEntry<K,V> e = first;;) {
                    if (e != null) {//数组中当前位置存在key
                        K k;
                        if ((k = e.key) == key ||
                            (e.hash == hash && key.equals(k))) {
                            oldValue = e.value;//key相等则直接覆盖
                            if (!onlyIfAbsent) {
                                e.value = value;
                                ++modCount;
                            }
                            break;
                        }
                        e = e.next;
                    }
                    else {//数组中当前位置不存在key
                        if (node != null)
                            node.setNext(first);
                        else
                            node = new HashEntry<K,V>(hash, key, value, first);
                        int c = count + 1;
                        if (c > threshold && tab.length < MAXIMUM_CAPACITY)
                            rehash(node);//如果超过阈值则扩容
                        else
                            setEntryAt(tab, index, node);//把node放在数组中
                        ++modCount;
                        count = c;
                        oldValue = null;
                        break;
                    }
                }
            } finally {//解锁
                unlock();
            }
            return oldValue;
        }

大概流程:先根据key的hash定位到Segment,然后对找到的Segment加锁而不是对所有Segment加锁,再在Segment中找到具体的位置,这段逻辑就和HashMap的差不多了,最后释放锁。

1.4 get操作

    public V get(Object key) {
        Segment<K,V> s; // manually integrate access methods to reduce overhead
        HashEntry<K,V>[] tab;
        int h = hash(key);
        long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
        //先找到Segment,然后遍历Segment里的数组
        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操作没有加锁。

1.5 size方法

    public int size() {
        
        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 (;;) {
                //如果重试次数大于2,那么对每一个segments加锁
                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;
    }

大致流程:

  1. 在一个死循环里遍历所有的segment。
  2. 把每一个segment的size加起来得到sum。
  3. 判断这次循环的sum和上次循环的sum是否相等,如果相等则说明在此期间size没有改变,直接退出循环;如果不相等则说明其他线程在统计的时候改变了size,那么继续循环重试,当重试次数超过了阈值则对所有segment加锁,这样就保证了统计的时候size不会再改变了。
  4. 如果加锁了则进行解锁,返回size。

从这里可以看到乐观锁的思想,就先乐观地假设统计的过程中size不会被其他线程修改,如果真发生了修改那么就多尝试几次,实在不行再加锁。

二、ConcurrentHashMap常见面试问题

2.1 ConcurrentHashMap的数据结构?

jdk1.7:

本质上,ConcurrentHashMap就是个存储Segment对象的数组,而这个Segment又是一个HashMap的结构,Segment继承了可重入锁,从而使得Segment对象能充当锁的角色,这样,每个Segment守护ConcurrentHashMap的若干个桶,其中每个桶又是由若干个HashEntry对象链接起来的链表。通过使用段(Segment)将ConcurrentHashMap分成多个部分,这样每一个部分都有一把锁,修改一个部分不影响其他部分同时修改,就是允许多个部分同时修改,这就是分段锁的核心思想。
image

jdk1.8:

取消了分段锁,结构上和HashMap1.8的结构相似,都是数组+链表+红黑树的结构,采用CAS操作和synchronized来保证并发安全。
image

2.2 jdk1.7和jdk1.8的put实现有什么不同?

jdk1.8put源码:

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();//key和value都不能是空
        int hash = spread(key.hashCode());
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // 当数组的下标位置为空时,进行不加锁的CAS操作赋值
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                synchronized (f) {//锁住链表的头节点
                    if (tabAt(tab, i) == f) {
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;//如果key相等,则直接覆盖掉value
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }
                        else if (f instanceof TreeBin) {//红黑树插入
                            Node<K,V> p;
                            binCount = 2;
                            if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                           value)) != null) {
                                oldVal = p.val;
                                if (!onlyIfAbsent)
                                    p.val = value;
                            }
                        }
                    }
                }
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

如果定位到数组上的node为空,则用CAS操作进行写入数据,如果有数据则用synchronized锁住链表的头节点进行写入。

2.3 ConcurrentHashMap和HashMap有什么区别?

  • ConcurrentHashMap线程安全,HashMap线程不安全。
  • ConcurrentHashMap不允许存储null键和null值,HashMap允许存储null键和null值。

2.4 ConcurrentHashMap的key和value为什么不能为null?

因为作者Doug Lea不喜欢null。无法分辨是key没找到的null还是有key值为null。

三、总结

在jdk1.8中ConcurrentHashMap放弃了Segment分段锁,采用CAS和synchronized来保证线程安全,这是一种乐观锁的思想,细化了锁的粒度。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值