Java集合源码分析(二):ConcurrentHashMap

ConcurrentHashMap 源码学习

建议先了解HashMap的底层源码,再来学习ConcurrentHashMap ,效率会快很多。
这里给出本人对于HashMap的一点理解。
《HashMap 源码学习》

特性

  • ConcurrentHashMap继承了AbstractMap类,实现了ConcurrentMap和Serializable接口
  • ConcurrentHashMap线程安全(jdk1.7:segment分段锁;jdk1.8:node+cas+synchronized)

七个重要参数

  • 数组最大长度 MAXIMUM_CAPACITY = 1 << 30
  • 数组初始化长度 DEFAULT_CAPACITY = 16
  • 默认并发级别 DEFAULT_CONCURRENCY_LEVEL = 16
  • 加载因子 LOAD_FACTOR = 0.75f
  • 树化阈值 TREEIFY_THRESHOLD = 8
  • 链表化阈值 UNTREEIFY_THRESHOLD = 6
  • 树化的最小数组长度 MIN_TREEIFY_CAPACITY = 64

几个重要方法

基于jdk1.8

put()

首先看put函数,内部调用了putVal
在这里插入图片描述

putVal()
final V putVal(K key, V value, boolean onlyIfAbsent) {
		// ConcurrentHashMap的键值都不能为空
        if (key == null || value == null) throw new NullPointerException();
        // 计算key的哈希值
        int hash = spread(key.hashCode());
        // binCount 用来记录 链表/树 中元素个数
        int binCount = 0;
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            // 若Node数组为空则先进行初始化
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
             // 计算key要存储在数组的下标,并且判断当前位置上是否存在元素
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            	// 若不存在元素,则通过cas操作添加元素
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   // no lock when adding to empty bin
            }
            // 若存在元素,且首元素的哈希值为MOVED(-1),则需要进行扩容
            else if ((fh = f.hash) == MOVED)
            	// 帮助数据迁移
                tab = helpTransfer(tab, f);
            // 若存在元素,并不需要扩容
            else {
                V oldVal = null;
                // 只对首元素加锁,这是ConcurrentHashMap的特性
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                    	// 首元素的哈希值>=0 说明这是一个链表结构
                        if (fh >= 0) {
                        	// binCount 用于记录链表长度
                            binCount = 1;
                            // 遍历链表
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                // 同HashMap,对比key是否是同一个对象
                                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;
                            }
                        }
                    }
                }
                // 当前节点的个数binCount 
                if (binCount != 0) {
                	// 节点个数大于等于TREEIFY_THRESHOLD(8)时,进行树化
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                    	// 如果有覆盖,则返回旧值
                        return oldVal;
                    break;
                }
            }
        }
        // 记录所有数组中的元素个数
        addCount(1L, binCount);
        return null;
}

这里可以了解下jdk1.7中的put操作

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
 	// 将当前 Segment 中的 table 通过 key 的 hashcode 定位到 HashEntry
    // 首先第一步的时候会尝试获取锁,如果获取失败肯定就有其他线程存在竞争,则利用 scanAndLockForPut() 自旋获取锁,如果重试的次数达到了 MAX_SCAN_RETRIES 则改为阻塞锁获取,保证能获取成功。
    // tryLock()方法尝试去获取锁
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        // 取数组 tab 的第一个元素 first
        HashEntry<K,V> first = entryAt(tab, index);
        // e 是遍历数组中的元素
        for (HashEntry<K,V> e = first;;) {
            // 若 e 不为空
            if (e != null) {
                K k;
// 遍历该 HashEntry,如果不为空则判断传入的 key 和当前遍历的 key 是否相等,相等则覆盖旧的 value。
                // 相等则覆盖,然后结束
                if ((k = e.key) == key ||
                    (e.hash == hash && key.equals(k))) {
                    oldValue = e.value;
                    if (!onlyIfAbsent) {
                        e.value = value;
                        ++modCount;
                    }
                    break;
                }
                e = e.next;
            }
            // 若 e 为空
            else {
         // node不为空则需要新建一个 HashEntry 并加入到 Segment 中,同时会先判断是否需要扩容。
                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);
                ++modCount;
                count = c;
                oldValue = null;
                break;
            }
        }
    } finally {
       //释放锁
        unlock();
    }
    return oldValue;
}
helpTransfer()

用于数据迁移

treeifyBin()

链表转化为红黑树

get()

public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        // 计算key的哈希值
        int h = spread(key.hashCode());
        // 当前数组不为空,长度>0,且根据key的哈希值与数组长度进行与运算后,得到数组下标的首元素不为空
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            // 若当前key与数组首元素的key是一个对象,则直接返回value
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            // 如果哈希值小于0,说明正在扩容,或者是红黑树
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            // 遍历链表
            while ((e = e.next) != null) {
            	// 一一比对key,是同一个对象就返回value
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        // 没有就返回null
        return null;
    }

五个注意点

1. ConcurrentHashMap的键值都不能为空(jdk1.8)

会返回空指针异常。
在这里插入图片描述

2. ConcurrentHashMap与HashMap的不同之处:

  1. 若当前index下的元素为空,则通过CAS写入数据,失败则自旋
  2. 若不为空,为链表的第一个元素加上synchronized锁,后面同HashMap

3. jdk1.8与jdk1.7的不同之处:

  • 添加了红黑树,提高了查询效率
  • 用synchronized内置锁取代了segment分段锁(继承ReentrantLock锁)
  • 用Node[] 数组替代了Segment数组和Entry数组
    • jdk1.7中put和 get 操作需要两次Hash才能到达指定的HashEntry,第一次hash到达Segment,第二次到达Segment里面的Entry,然后在遍历entry链表.
  • 使用cas操作put元素

4. 为什么jdk1.8使用内置锁synchronized来代替重入锁ReentrantLock

  • 因为锁粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
  • JVM开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然(synchronized在1.6中有了锁升级机制,性能得到优化)
  • 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据。而synchronized是java内置关键字。

5. 扩容机制

jdk1.7中,segment长度是固定的(初始值为16),不能扩容,扩容的是HashEntry数组;jdk1.8中,扩容的是Node数组。扩容机制同HashMap:当整体元素个数>数组长度 * 扩容因子数组长度>64时,两倍扩容。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值