龙哥网

龙哥网

java中HashMap和ConcurrentHashMap实现原理(hashmap concurrenthashmap原理)
2022-03-01

在java中,HashMap底层是通过 数组 + 链表实现的,如果当某个链表元素个数超过8个,则链表转化为红黑树。

其底层类似上述结构,一层采用数组结构,每个数组里面都是一个链表,当我们进行put的时候,计算key的hash值,和数组的长度-1取余得到待添加元素在数组中的位置,然后看该位置是否有元素,如果没有元素,那么作为链表的头加入,如果有,加入到已有的链表中,如果链表的元素超过8个,这时候将链表转换为红黑树。

 transient Node<K,V>[] table;

static final class TreeNode<K,V> extends LinkedHashMap.Entry<K,V> { 
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;
        TreeNode(int hash, K key, V val, Node<K,V> next) { 
            super(hash, key, val, next);
        }
        .....
}
static class Node<K,V> implements Map.Entry<K,V> { 
        final int hash;
        final K key;
        V value;
        Node<K,V> next;

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

如上,Node代表的是普通链表,而TreeNode则是HashMap中红黑树的实现。
另外在HashMap中有一个比较重要的属性就是扩充因子,表示的是当Map中的容量到达初始容量的多少进行扩容,默认情况下,扩充因子是0.75,初始大小是16 ,当map大小超过容量的0.75时,会进行扩容,每次对之前容量*2 进行扩容,即: 16 ,32,64
如下是HashMap添加元素的核心方法:

final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
                   boolean evict) { 
        Node<K,V>[] tab; Node<K,V> p; int n, i;
        if ((tab = table) == null || (n = tab.length) == 0)
        	// 初始化数组
            n = (tab = resize()).length;
        if ((p = tab[i = (n - 1) & hash]) == null)
        	// 对应位置上没有元素,直接加入
            tab[i] = newNode(hash, key, value, null);
        else { 
            Node<K,V> e; K k;
            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 { 
            	// 普通链表插入,插入完之后判断链表是否达到转红黑树条件,如果达到了,转红黑树
                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;
                    }
                    if (e.hash == hash &&
                        ((k = e.key) == key || (key != null && key.equals(k))))
                        break;
                    p = e;
                }
            }
            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;
    }

当使用红黑树进行插入的时候,我们知道红黑树是有序的,默认是使用hashCode来进行排序的,如果hashCode相同的话,那么还会判断Key是否是Comparable,如果不是,那么会判断二者是否是同一个类型,如果还不行,则调用System.identityHashCode来进行判断

JDK8中的HashMap采用了红黑树,避免了之前JDK7中使用普通链表当元素碰撞导致链表过长查询慢问题

HashMap是非线程安全的,当多线程访问的时候,会出现问题,基于此java提供了ConcurrentHashMap可以并发操作的Map。java本身也提供了HashTable,是一个线程安全的Map,但是HashTable在并发的情况下加锁的粒度比较大是整个Map,并发量会下降。

而ConcurrentHashMap则是根据HashMap底层数据结构的特点采用了分段上锁的逻辑,我们知道,当多个线程进行put的时候,每个key最终会落到数组其中一个元素上,如果多个线程每个操作的都是不同的数组元素,那么不存在竞争关系,那么只要每次操作对单个数组元素加锁,如果需要扩充Map,那么全局加锁即可。
ConcurrentHashMap的处理逻辑大致如上,上锁是结合CAS+Synchronized锁机制。

final V putVal(K key, V value, boolean onlyIfAbsent) { 
        if (key == null || value == null) throw new NullPointerException();
        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) { 
            	// 数组位置没有元素,通过cas设置头结点
                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)
            	// map在扩容,这里也会帮助扩容,思路和put差不多,只处理单个数组元素
                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) { 
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

可以看到,在对应位置没有元素时是通过cas来设置,但是存在元素插入的时候,则是通过synchronized的,这主要是,没有元素的时候设置是一个比较快的过程,这时候如果存在并发,通过cas,并发线程不会自旋等待很久,但是如果有元素进行插入的时候,尤其如果是红黑树还需要进行旋转,这时候如果继续cas空转等待的话,浪费cpu和时间,所以直接通过synchronized来上锁,如果等待时间比较长,锁升级为重量级锁,线程直接阻塞出让CPU。

对于初始化数组,直接通过cas来进行设置:

private final Node<K,V>[] initTable() { 
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) { 
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { 
                try { 
                    if ((tab = table) == null || tab.length == 0) { 
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally { 
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }
免责声明
本站部分资源来源于互联网 如有侵权 请联系站长删除
龙哥网是优质的互联网科技创业资源_行业项目分享_网络知识引流变现方法的平台为广大网友提供学习互联网相关知识_内容变现的方法。