HashMap和ConcurrentHashMap 源码分析

注:本文所述为JDK1.8版本

HashMap和ConcurrentHashMap源码分析

1、HashMap

1.1 概述

HashMap底层是基于数组+链表+红黑树组成的。

数据结构示例图:

在这里插入图片描述

主要属性:

    static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

    static final int MAXIMUM_CAPACITY = 1 << 30;

    static final float DEFAULT_LOAD_FACTOR = 0.75f;

    // 用于判断是否需要将链表转换为红黑树的阈值
    static final int TREEIFY_THRESHOLD = 8;

    // JDK1.7中的HashEntry修改为Node
    transient Node<K,V>[] table;

    transient Set<Map.Entry<K,V>> entrySet;

    transient int size;

Node结构:

TreeNode源码:

1.2 put方法

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

    final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) {
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        //判断当前桶是否为空,空的话就需要初始化(resize中会判断是否需要初始化)
        if ((tab = table) == null || (n = tab.length) == 0)
            n = (tab = resize()).length;
        // 根据当前key的hashcode定位到具体的桶中并判断是否为空,为空表明没有Hash冲突就直接在当前位置创建一个新桶即可
        if ((p = tab[i = (n - 1) & hash]) == null)
            tab[i] = newNode(hash, key, value, null);
        else {
            Node<K,V> e; K k;
            // 如果当前桶有值(Hash冲突),那么就要比较当前桶中的key、key的hashcode与写入的key是否相等,相等就赋值给e,后面统一进行赋值及返回
            if (p.hash == hash &&
                ((k = p.key) == key || (key != null && key.equals(k))))
                e = p;
            // 如果当前桶为红黑树,按照红黑树的方式写入数据
            else if (p instanceof TreeNode)
                e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
            else {
                // 如果是个链表,就需要将当前的key、value封装成一个新节点写入当前桶的后面(采用尾插法)
                for (int binCount = 0; ; ++binCount) {
                    if ((e = p.next) == null) {
                        p.next = newNode(hash, key, value, null);
                        // 判断当前链表的大小是否大于预设的阈值,大于时就要转换为红黑树
                        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                            treeifyBin(tab, hash);
                        break;
                    }
                    // 如果在遍历链表的过程中,找到key相同时直接退出遍历
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            // 如果e != null就相当于存在相同的key,那就需要将值覆盖
            if (e != null) { // existing mapping for key
                V oldValue = e.value;
                if (!onlyIfAbsent || oldValue == null)
                    e.value = value;
                afterNodeAccess(e);
                return oldValue;
            }
        }
        ++modCount;
        // 最后判断是否需要进行扩容
        if (++size > threshold)
            resize();
        afterNodeInsertion(evict);
        return null;
    }

1.3 get方法

    public V get(Object key) {
        Node<K,V> e;
        return (e = getNode(hash(key), key)) == null ? null : e.value;
    }

    final Node<K,V> getNode(int hash, Object key) {
        Node<K,V>[] tab; Node<K,V> first, e; int n; K k;
        // 将key hash之后取得所定位的桶
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (first = tab[(n - 1) & hash]) != null) {
            // 如果桶不为空
            // 判断桶的第一个位置的key是否为查询的key,是就直接返回value
            if (first.hash == hash && 
                ((k = first.key) == key || (key != null && key.equals(k))))
                return first;
            // 如果第一个不匹配,则判断它的下一个是红黑树还是链表
            if ((e = first.next) != null) {
                // 红黑树就按照数的查找方式返回值
                if (first instanceof TreeNode)
                    return ((TreeNode<K,V>)first).getTreeNode(hash, key);
                // 链表就遍历匹配返回值
                do {
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        return e;
                } while ((e = e.next) != null);
            }
        }
        return null;
    }

链表修改为红黑树之后查询效率变为了O(logn)

1.4 hash算法

key为空,hash值为0;h为key的hashCode,结果就变为了 h^(h>>>16),即:高16位不变,低位高位异或运算。

1.5 HashMap存在的问题

在JDK1.7中,HashMap在并发执行put操作时会引起死循环,是因为多线程会导致HashMap的Entry链表形成环形数据结构,一旦形成环形数据结构,Entry的next节点永不为空,就会产生死循环获取Entry

原因就是JDK1.7链表插入新节点采用的是头插法,这样在线程一扩容迁移元素时,会将元素顺序改变,导致两个线程中出现元素的相互指向而形成循环链表,JDK1.8采用了尾插法,从根源上杜绝了这种情况的发生。

1.6 遍历方式

		Iterator<Map.Entry<String, Integer>> entryIterator = map.entrySet().iterator();
		while (entryIterator.hasNext()) {
			Map.Entry<String, Integer> next = entryIterator.next();
			System.out.println("key=" + next.getKey() + " value=" + next.getValue());
		}

		Iterator<String> iterator = map.keySet().iterator();
		while (iterator.hasNext()) {
			String key = iterator.next();
			System.out.println("key=" + key + " value=" + map.get(key));
		}

建议使用第一种EntrySet进行遍历,第一种可以把key和value同时取出,第二种还需要通过key取一次value,效率较低

2、ConcurrentHashMap

2.1概述

为什么要使用ConcurrentHashMap?

  • HashMap线程不安全
  • HashTable效率低下:HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为当一个线程访问HashTable的同步方法,其他线程也访问HashTable的同步方法时,会进入阻塞或轮询状态

结构图:

在这里插入图片描述

JDK1.8中抛弃了原有的Segment分段锁,而采用了CAS+synchronized来保证并发安全性。

Node源码:

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        volatile V val;
        volatile Node<K,V> next;

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

        public final K getKey()       { return key; }
        public final V getValue()     { return val; }
        public final int hashCode()   { return key.hashCode() ^ val.hashCode(); }
        public final String toString(){ return key + "=" + val; }
        public final V setValue(V value) {
            throw new UnsupportedOperationException();
        }

也将1.7中存放数据的HashEntry改为了Node,但作用都是相同的

其中的val和next都用了volatile修饰,保证了可见性

TreeNode源码:

2.2 put方法

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

    final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        // 根据key计算出hashcode	
        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();
            // f为当前key定位出的Node,如果为空表示当前位置可以写入数据,利用CAS尝试写入,失败则自旋保证成功
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                if (casTabAt(tab, i, null,
                             new Node<K,V>(hash, key, value, null)))
                    break;                   
            }
            // 如果当前位置的hashcode==MOVED==-1,则需要进行扩容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                // 如果都不满足,则利用synchronized锁写入数据
                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;
                                    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) {
                    // 如果数量大于TREEIFY_THRESHOLD则要转换为红黑树
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

2.3 get方法

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) {
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        return null;
    }

根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。如果是红黑树那就按照树的方式获取值。都不满足那就按照链表的方式遍历获取值

JDK1.8在1.7的数据结构上做了大的改动,采用红黑树之后可以保证查询效率O(logn),甚至取消了ReentrantLock改为了synchronized,这样可以看出在新版的JDK中对synchronized优化是很到位的

2.4 hash算法

ConcurrentHashMap中key不能为空,其中h参数为key的hashCode;与HashMap相比,是HashMap的hash结果再与上int的最大值作为ConcurrentHashMap的hash值。

0x7fffffff的意思

  1. 7fffffff是8位16进制
  2. 每个16进制代表4个bit
  3. 8✖4bit=32bit=4Byte
  4. f的二进制为:1111,7的二进制位0111
  5. int类型的长度位4Byte
  6. 左边起,第一位为符号位,0代表正数,1代表负数
  7. 0x7fffffff代表int的最大值

3、小结:HashMap和ConcurrentHashMap的异同

  • HashMap允许null值null键,而ConcurrentHashMap则不允许null值null键
  • HashMap是非线程安全的,而ConcurrentHashMap是线程安全的
  • HashMap和ConcurrentHashMap的底层都是数组+链表+红黑树
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
ConcurrentHashMapJava中的一个并发容器,用于在多线程环境中安全地存储和访问键值对。它使用了一些特殊的技术来提高其并发性能。 ConcurrentHashMap源码分析可以从几个关键点开始。首先,它使用了大量的CAS(Compare and Swap)操作来代替传统的重量级锁操作,从而提高了并发性能。只有在节点实际变动的过程中才会进行加锁操作,这样可以减少对整个容器的锁竞争。 其次,ConcurrentHashMap的数据结构是由多个Segment组成的,每个Segment又包含多个HashEntry。这样的设计使得在多线程环境下,不同的线程可以同时对不同的Segment进行操作,从而提高了并发性能。每个Segment都相当于一个独立的HashMap,有自己的锁来保证线程安全。 在JDK1.7版本中,ConcurrentHashMap采用了分段锁的设计,即每个Segment都有自己的锁。这样的设计可以在多线程环境下提供更好的并发性能,因为不同的线程可以同时对不同的Segment进行读写操作,从而减少了锁竞争。 总的来说,ConcurrentHashMap通过使用CAS操作、分段锁以及特定的数据结构来实现线程安全的并发访问。这使得它成为在多线程环境中高效地存储和访问键值对的选择。123 #### 引用[.reference_title] - *1* [ConcurrentHashMap 源码解析](https://blog.csdn.net/Vampirelzl/article/details/126548972)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] - *2* *3* [ConcurrentHashMap源码分析](https://blog.csdn.net/java123456111/article/details/124883950)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT0_1"}} ] [.reference_item] [ .reference_list ]
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

long-king

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值