JAVA并发包(二十):ConcurrentHashMap

ConcurrentHashMap也是并发环境中常见的Map,如果在高并发中没有排序等特别的需要,我们可以优先选择ConcurrenHashMap存储key-value键值对。

ConcurrentHashMap一般有两个版本的实现,jdk7(包括7)之前是Segment数组+Hash桶的数据结构,jdk8(包括8)之后是synchronized+cas+红黑树的数据结构。

下面我们先来比较下各种常见Map区别,然后研究两个版本的ConcurrentHashMap。

一、各种常见Map的比较

  1. HashMap:最基础的Map结构,在非并发场景下,优先使用。缺点是非线程安全的。
  2. HashTable:线程安全,内部方法都使用了synchronized加锁,操作比较耗性能。
  3. Collections.synchronizedMap():线程安全,与HashTable类型,都是使用synchronized对每个方法做了加锁,比较耗性能。
  4. ConcurrentHashMap:线程安全,操作效率较高。

二、JDK7版本的ConcurrentHashMap

1.Segment对象源码分析

JDK7以及之前版本的ConcurrentHashMap采用了分段锁Segment数组+Hash桶的数据结构,相当于是做了两次Hash。第一次Hash拿到分段锁,默认是16个Segment,每个Segment继承了ReentrantLock可以加锁,里面又维护了一个跟HashMap类似的Hash桶来保存key-value数据。结构图大致如下:
在这里插入图片描述

其核心在Segment类中,我们通过下面源码来分析。

可以看到Segment继承了重入锁ReentrantLock,所以它可以调用tryLock方法加锁。另外从其内部的代码结构可以看到,它也可以当成是一个HashMap。可以从put方法中看到,它就是调用了tryLock加锁,然后操作内部数据。可以理解是先加锁然后操作HashMap。

static final class Segment<K,V> extends ReentrantLock implements Serializable {
        // Segment的元素
        transient volatile HashEntry<K,V>[] table;
        // Segment中保存对象个数
        transient int count;    
        /** 扩容的阈值 */
        transient int threshold;
        /** 负载因子 */
        final float loadFactor;

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

        final V put(K key, int hash, V value, boolean onlyIfAbsent) {
            HashEntry<K,V> node = tryLock() ? null :
                scanAndLockForPut(key, hash, value);
            V oldValue;
            try {
               // 这里省略部分代码
               .......
            } finally {
                unlock();
            }
            return oldValue;
        }

2.put方法分析

put方法先是通过hash找到当前key所对应的Segment,然后调用这个Segment对象的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;
        /** 内部维护了一个Segment的数组,默认数组大小为16,
        ConcurrentHashMap初始化的时候会把下标为0的Segment放到数组中,
        后面根据j的值从Segment数组中找到对应下标的Segment,
        如果找不到就创建一个Segment,并放到数组中 */
        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);
    }

3.size方法

size方法会遍历每个segment,并对其加锁,然后把每个segment保存的对象数量相加,最后解锁。

4.get方法

get方法不做加锁,首先拿到Segment,然后再去Segment里面的HashEntry数组里找到value

三、JDK8版本的ConcurrentHashMap

JDK8版本的ConcurrentHashMap比较复杂,这里就根据put方法的实现来大概来分析下其结构

 public V put(K key, V value) {
        return putVal(key, value, false);
    }

    /** Implementation for put and putIfAbsent */
    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        // key对应的hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        // table是哈希桶,跟HashMap类似
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            // 找到hash对应的桶
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                // 桶中之前没值,把当前值插入成功后可直接退出轮询
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                // 对操作的节点加锁,这里直接使用了synchronized,可以猜测jdk8对synchronized做了很多优化,可以直接使用在并发类了
                synchronized (f) {
                	// 加锁之后就是构建链表或者红黑树,如果哈希桶中一个下标位置对应的节点数大于等于8就构建红黑树
                    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;
                                    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) {
                	// 链表数量大于等于8构建红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ConcurrentHashMapJava 并发中的一个高效线程安全的哈希表实现,主要用于多线程环境下的高并发场景。 在 JDK 1.7 和 1.8 中,ConcurrentHashMap 的实现有所不同,主要区别如下: 1. Segment 数量:在 JDK 1.7 中,ConcurrentHashMap 内部使用了 Segment 数组来维护数据,每个 Segment 中含一个 HashEntry 数组,而在 JDK 1.8 中,ConcurrentHashMap 则使用了 Node 数组来维护数据,不再使用 Segment,这样可以减少了内存消耗。 2. 锁的粒度:在 JDK 1.7 中,ConcurrentHashMap 使用了 Segment 来控制并发访问,每个 Segment 内部都使用了一个锁,这样会导致不同线程访问不同 Segment 时存在竞争,而在 JDK 1.8 中,ConcurrentHashMap 使用了 Node 数组来控制并发访问,每个 Node 上都有一个锁,这样可以减少锁的粒度,提高并发性能。 3. CAS 操作:在 JDK 1.8 中,ConcurrentHashMap 引入了 CAS 操作,可以在不加锁的情况下实现对数据的修改,这样可以提高并发性能。 4. 数据结构:在 JDK 1.7 中,ConcurrentHashMap 内部使用了 HashEntry 数组来存储数据,而在 JDK 1.8 中,则使用了 Node 数组和红黑树来存储数据,这样可以提高数据的查询效率。 5. 对象分配:在 JDK 1.8 中,ConcurrentHashMap 使用了 sun.misc.Unsafe 来进行对象分配,可以减少对象分配时的锁竞争,提高并发性能。 总之,JDK 1.8 中的 ConcurrentHashMap并发性能和内存消耗方面都有所提高,是一个更加高效的实现。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值