Java 之HashMap、ConcurrentHashMap底层原理

JDK1.8中对HashMap的优化

由 数组+链表 的结构改为 数组+链表+红黑树。
在链表元素数量超过8时改为红黑树,少于6时改为链表,中间7不改是避免频繁转换降低性能。

扩容后,元素要么是在原位置,要么是在原位置再移动2次幂的位置,且链表顺序不变。
不需要重新计算hash,只需要根据原来hash值和容量进行与,0的话索引没变,1的话索引变为原索引加原来的数组长度。

用的尾插法所以新数组链表不会倒置,多线程下不会出现死循环。

数组的长度为什么必须为2^n

原因1:h & (lenght-1)等效 h%lenght 操作,等效的前提就是:length 必须是2的整数倍。
原因2:防止hash冲突,位置冲突。

扩容后下标变化有两种情况:

  • 下标不变:将原表下标的元素放到扩容表同样的位置。
  • 下标变化:将原表下标的元素加上增加的扩容量放到扩容表的位置。
/**
 * 若(e.hash & oldCap) == 0,下标不变,将原表某个下标的元素放到扩容表同样
 * 下标的位置上
 */
                         
/**
  * 若(e.hash & oldCap) != 0,将原表某个下标的元素放到扩容表中
  * [下标+增加的扩容量]的位置上
  */

红黑树

1、根节点为黑色
2、节点不是黑色,就是红色(非黑即红)

在这里插入图片描述

HashMap的数据结构

JDK7 中的 HashMap

数组+链表
HashMap 底层维护一个数据,数组中存放的是 Entry<K, V>。
Map中的key,value则以Entry的形式存放在数组中。

static class Entry<K,V> implements Map.Entry<K,V> {
        final K key;
        V value;
        Entry<K,V> next;
        int hash;
}

链表的数据结构是怎样的:
HashMap中,每个元素实际都是一个Node(定义在HashMap内)对象。
从Node的定义上也可以看到,其实是个单向链表。

static class Node<K,V> implements Map.Entry<K,V> {
    // 缓存key的hash值
    final int hash;
    final K key;    
    V value;
    // 下一个Node引用
    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;    
    }
    // equals 等方法
    ...
}    

通过计算 key 的 hash 值来决定放在数组的哪个位置,当 hash 值冲突时,用链表的方式来存储。
数组扩容是原来的一倍。
如果 key 为 null,会将这个元素存放到 table[0] 的位置。

JDK8 中的 HashMap

为了提⾼链表查询性能增加红⿊树,红黑树查询数据的性能大于链表。
1、当链表的长度大于8时。

  • 当前hash table的长度小于等于64,会扩容。
  • 当前hash table的长度大于64,会转化为一个红黑树。

HashMap初始化

在这里插入图片描述
参数解析:

  • initialCapacity:初始化容量,默认数组长度为16。
  • loadFactor:扩容负载因⼦,默认0.75f,0.75扩容性能最好,没人会改,取的是0.5-1的中间值。
  • threshold:扩容阈值,threshold = capacity * loadFactor,当map所容纳的数量达到threshold的值时,hashMap就会自动扩容。

总结就是:当table元素有值数量达到75%时,会进行扩容,扩容2倍。

static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // 16
static final float DEFAULT_LOAD_FACTOR = 0.75f; 
HashMap<Integer, Integer> map = new HashMap<>();
Class<?> mapType = map.getClass();

Method capacity = mapType.getDeclaredMethod("capacity");
capacity.setAccessible(true);
System.out.println("capacity : " + capacity.invoke(map));

Field size = mapType.getDeclaredField("size");
size.setAccessible(true);
System.out.println("size : " + size.get(map));

Field threshold = mapType.getDeclaredField("threshold");
threshold.setAccessible(true);
System.out.println("threshold : " + threshold.get(map));

Field loadFactor = mapType.getDeclaredField("loadFactor");
loadFactor.setAccessible(true);
System.out.println("loadFactor : " + loadFactor.get(map));

在这里插入图片描述

HashMap在put时扩容产生死循环

JDK1.7链表使用的是头插法。
JDK1.8链表使用的是尾插法。大大降低了减小发生死循环的概率。

HashMap只有在并发扩容操作的情况下会造成链表的死循环。
HashMap本来就不支持多线程使用,要并发就用ConcurrentHashMap。

在JDK1.8中对HashMap进行了优化,在发生hash碰撞,不再采用头插法方式,而是直接插入链表尾部,因此不会出现环形链表的情况,但是在多线程的情况下仍然不安全。

HashMap 在并发场景下存在的问题

数据丢失、数据重复、死循环。

数据丢失情况

出现两个线程同时判断 table[i]=null 时,此时两个线程都会去创建Entry,这样存入会出现数据丢失。

数据重复情况

如果有两个线程同时发现自己都 key不存在,而这两个线程的 key 实际是相同的,在向链表中写入的时候,第一个线程将e设置为了自己的Entry,而第二个线程就会执行到e.next,此时拿到的是最后一个节点,依然会将自己持有的数据插入到链表中,这样就出现了数据 重复。

死循环

主要是因为hashMap在 resize 扩容过程中对链表进行了一次倒序处理。
假设两个线程同时进行resize, A->B 第一个线程在处理过程中比较慢,第二个线程已经完成了倒序编程了B-A 那么就出现了循环,B->A->B。就会出现CPU使用率飙升。
之所以出现死循环,主要还是在于对于链表对倒序处理,在Java 8中,已经不在使用倒序列表,解决了死循环问题。

JDK1.8中对hash算法和寻址算法是如何优化的

hash算法优化:用高16异或低16位,使新的数字具备高低16位的特性,这样更加随机不容易冲突。

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

hashCode 值是一个 int 类型(4个字节,32位)。
h >>> 16,对32位二进制向右位移16位。
(h = key.hashCode()) ^ (h >>> 16),对位移前和位移后的二进制进行异或(异或口诀:相同取0,不同取1)。
相当于对高低16位都参与运算。具备高低16位的特征。避免了很多低16位相同hashcode冲突的概率。
有一个key的hash值:

1111 1111 1111 1111 1111 1010 0111 1100
0000 0000 0000 0000 1111 1111 1111 1111  向右位移16位
1111 1111 1111 1111 0000 0101 1000 0011  进行异或

寻址算法优化:用与运算替代取模,提升性能
(n - 1) & hash => 与运算效果是跟hash对n取模,效果是一样的,但是与运算的性能要比hash对n取模要高很多。数学问题,如果数组长度是2的n次方,与运算和取模运算的值是一样的,hashmap数组的长度会一直是2的n次方。
与:两个同时为1,结果为1,否则为0

//通过如下代码寻址。其中n为hash table的长度(不是HashMap的size)
i = (n - 1) & hash  // 得到数组里的一个位置
1111 1111 1111 1111 0000 0101 1000 0011(经过扰动函数计算的新hash值)
0000 0000 0000 0000 0000 0000 0000 1111 数组的长度,默认是15(从0开始)
0000 0000 0000 0000 0000 0000 0000 0011 进行与运算后的值,转成十进制是3,3表示数组的位置

ConcurrentHashMap基本原理

1、内部持有一个Node<K,V>[],用来存放key,value。
这个数组的默认长度是16,并且只会在第一次put的时候才会初始化(lazy init)。

2、put 的时候要通过运算得到应存放的数组下标,然后根据不同的情况决定初始化数组、插入链表、插入红黑树或者协助扩容。

  • 先进行hash扰动。
  • 数组如果还未进行初始化,则先进行初始化。初始化默认大小为16,如果指定了初始化大小,则会计算一个>=指定值,且为2的N次幂的数字,且最接近当前参数的数字作为初始长度。
  • 当前位置==null,则直接通过CAS插入数据。
  • 如果当前数组正在进行扩容,则协助扩容。
  • 当前位置!=null。如果当前节点是红黑树,则直接插入树中。否则作为链表插入链表插入或者更新。
  • 插入成功后,如果是链表,则检查是否需要转成红黑树。转换条件是链表节点数>=8,且数组长度>64。
  • 最后更新size的值,并且检查是否需要扩容。

3、get的时候同样通过运算得到应存放的数组下标,然后进行遍历。

  • 先进行hash扰动,使用hash&(n-1)得到数组索引。
  • 取索引对应的数据进行遍历。可能是链表、红黑树,也可能是FWD节点。

put()

1、hash扰动
2、死循环put,直到成功

  • 数组未初始化,则进行初始化
  • 元素为空,则进行CAS插入
  • 元素正在转移,则协助转移
  • 存在hash冲突,则锁住头节点,进行插入
  • 超过阈值,链表转红黑树

3、size+1&检查扩容

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    hash扰乱
    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();
        所属节点不为空,则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;                   // no lock when adding to empty bin
        }
        帮助扩容
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            存在hash冲突,则锁住头节点
            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;
            }
        }
    }
    size+1&检查是否需要扩容
    addCount(1L, binCount);
    return null;
}

ConcurrentHashMap 线程安全原理

JDK7

使用分段锁,分割成一个个Segment数组,都继承了ReentrantLock类,也就是说每个Segment类本身就是一个锁。

JDK8

使用了CAS+Synchronized。
插入数据时,当前table位置为空时,则直接通过CAS插入数据。
插入数据时,当前table位置不为空时,使用Synchronized锁住当前元素,再插入数据。

ConcurrentHashMap 1.8为什么要使用CAS+Synchronized取代Segment+ReentrantLock

1.8以前的ConcurrentHashMap是怎么保证线程并发的,首先在初始化ConcurrentHashMap的时候,会初始化一个Segment数组,容量为16,而每个Segment呢,都继承了ReentrantLock类,也就是说每个Segment类本身就是一个锁,之后Segment内部又有一个table数组,而每个table数组里的索引数据呢,又对应着一个Node链表。

当我们使用put方法的时候,是对我们的key进行hash拿到一个整型,然后将整型对16取模,拿到对应的Segment,之后调用Segment的put方法,然后上锁,请注意,这里lock()的时候其实是this.lock(),也就是说,每个Segment的锁是分开的。

其中一个上锁不会影响另一个,此时也就代表了我可以有十六个线程进来,而ReentrantLock上锁的时候如果只有一个线程进来,是不会有线程挂起的操作的,也就是说只需要在AQS里使用CAS改变一个state的值为1,此时就能对代码进行操作,这样一来,我们等于将并发量/16了。

请注意Synchronized上锁的对象,请记住,Synchronized是靠对象的对象头和此对象对应的monitor来保证上锁的,也就是对象头里的重量级锁标志指向了monitor,而monitor呢,内部则保存了一个当前线程,也就是抢到了锁的线程。

那么这里的这个f是什么呢?它是Node链表里的每一个Node,也就是说,Synchronized是将每一个Node对象作为了一个锁,这样做的好处是什么呢?将锁细化了,也就是说,除非两个线程同时操作一个Node,注意,是一个Node而不是一个Node链表哦,那么才会争抢同一把锁.

如果使用ReentrantLock其实也可以将锁细化成这样的,只要让Node类继承ReentrantLock就行了,这样的话调用f.lock()就能做到和Synchronized(f)同样的效果,但为什么不这样做呢?

请大家试想一下,锁已经被细化到这种程度了,那么出现并发争抢的可能性还高吗?还有就是,哪怕出现争抢了,只要线程可以在30到50次自旋里拿到锁,那么Synchronized就不会升级为重量级锁,而等待的线程也就不用被挂起,我们也就少了挂起和唤醒这个上下文切换的过程开销.

但如果是ReentrantLock呢?它则只有在线程没有抢到锁,然后新建Node节点后再尝试一次而已,不会自旋,而是直接被挂起,这样一来,我们就很容易会多出线程上下文开销的代价.当然,你也可以使用tryLock(),但是这样又出现了一个问题,你怎么知道tryLock的时间呢?在时间范围里还好,假如超过了呢?

所以,在锁被细化到如此程度上,使用Synchronized是最好的选择了.这里再补充一句,Synchronized和ReentrantLock他们的开销差距是在释放锁时唤醒线程的数量,Synchronized是唤醒锁池里所有的线程+刚好来访问的线程,而ReentrantLock则是当前线程后进来的第一个线程+刚好来访问的线程.

如果是线程并发量不大的情况下,那么Synchronized因为自旋锁,偏向锁,轻量级锁的原因,不用将等待线程挂起,偏向锁甚至不用自旋,所以在这种情况下要比ReentrantLock高效。

面试题

HashMap的loadFactor为什么是0.75

问题: 为什么是0.75 , 不是0.5或者1?
回答: 如果是0.5 , 那么每次达到容量的一半就进行扩容,默认容量是16, 达到8就扩容成32,达到16就扩容成64, 最终使用空间和未使用空间的差值会逐渐增加,空间利用率低下。 如果是1,那意味着每次空间使用完毕才扩容,在一定程度上会增加put时候的时间。

继续问: 为什么是0.75,不是0.6或者0.8?
继续回: 取中间值,因为0.75是 0.5 ~ 1的中间值。

还是继续问:
还是继续回: 好像是根据一个数学公式计算得来的,再具体就不清楚了。

JDK1.8中HashMap在出现hash碰撞时链表长度超过8一定会变成红黑树?

不一定。
实际上转换红黑树有个大前提,就是当前hash table的长度也就是HashMap的capacity(不是size)不能小于64,小于64就只是做个扩容。

使用HashMap时,需要注意什么

尽可能避免频繁扩容,数据量大时,初始化时手动指定HashMap大小。
尽可能避免hash碰撞,作为HashMap的key的对象的hashcode方法,要合理设计。
不要在多线程情况下使用,HashMap是同步的,多线程情况下,优先考虑ConcurrentHashMap。
Q1:为什么多线程环境下不能使用HashMap?
A:多线程环境下HashMap可能会导致链表闭环,造成CPU100%

为什么说数据量大时,最好手动指定HashMap大小

空间上的开销:
HashMap的底层是数组+链表/红黑树来实现的,当数组被使用了75%(按默认的负载因子0.75)时就会对数组进行扩容。
而数组的存储空间是连续的,频繁的扩容,导致HashMap需要不停的去申请越来越大的连续的内存空间,当在堆内存中没有足够大的空闲的连续空间时,就会不停的触发GC。
性能上的开销:
依旧是频繁的扩容导致的。
在扩容的时候,除了内存空间之外,每次扩容时,还需要将HashMap中所有元素按照扩容后的hash table大小,重新计算下位置,而红黑树也有可能因为扩容后,重新退化成链表。
这个计算量还是很大的。

ConcurrentHashMap面试题

HaspMap的数组长度为什么是2的N次方?

  • 为了避免hash冲突,尽可能的散列数据。
  • 提升性能,如果数组长度是2的n次方,与运算和取模运算的值是一样的,与运算性能要高于取模运算。

ConcurrentHaspMap是如何保证线程安全的?

  • JDK 1.8以前,多个数组,分段加锁,一个数组一个锁。
  • JDK 1.8以后,优化细粒度,一个数组,每个元素进行CAS,如果失败说明有人了,此时synchronized对数组元素加锁,链表+红黑树处理,对数组每个元素加锁。

ConcurrentHaspMap中数据存储的可能形式有哪些?
数组、链表、红黑树。

ConcurrentHaspMap的扩容机制是什么?
多线程通过CAS+synchronized并发扩容。

put 方法

流程:
1)通过hash(Object key)算法得到hash值;
2)判断table是否为null或者长度为0,如果是执行resize()进行扩容;
3)通过hash值以及table数组长度得到插入的数组索引i,判断数组table[i]是否为空或为null;
4)如果table[i] == null,直接新建节点添加,转向 8),如果table[i]不为空,转向 5);
5)判断table[i]的首个元素是否和key一样,如果相同直接覆盖value,这里的相同指的是hashCode以及equals,否则转向 6);
6)判断table[i] 是否为treeNode,即table[i] 是否是红黑树,如果是红黑树,则直接在树中插入键值对,否则转7);
7)遍历table[i],判断链表长度是否大于8,大于8的话,再判断容量是否大于64,小于64,进行扩容,大于64把链表转换为红黑树,在红黑树中执行插入操作,否则进行链表的插入操作;遍历过程中若发现key已经存在直接覆盖value即可;
8)插入成功后,判断实际存在的键值对数量size是否超多了最大容量threshold,如果超过,进行扩容。
在这里插入图片描述

putVal方法

通过putVal方法将传递的key-value对添加到数组table中。

final V putVal(int hash, K key, V value, boolean onlyIfAbsent, boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    /**
     * 如果当前HashMap的table数组还未定义或者还未初始化其长度,则先通过resize()进行扩容,
     * 返回扩容后的数组长度n
     */
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //通过数组长度与hash值做按位与&运算得到对应数组下标,若该位置没有元素,则new Node直接将新元素插入
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    //否则该位置已经有元素了,我们就需要进行一些其他操作
    else {
        Node<K,V> e; K k;
        //如果插入的key和原来的key相同,则替换一下就完事了
        if (p.hash == hash && ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        /**
         * 否则key不同的情况下,判断当前Node是否是TreeNode,如果是则执行putTreeVal将新的元素插入
         * 到红黑树上。
         */
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        //如果不是TreeNode,则进行链表遍历
        else {
            for (int binCount = 0; ; ++binCount) {
                /**
                 * 在链表最后一个节点之后并没有找到相同的元素,则进行下面的操作,直接new Node插入,
                 * 但条件判断有可能转化为红黑树
                 */
                if ((e = p.next) == null) {
                    //直接new了一个Node
                    p.next = newNode(hash, key, value, null);
                    /**
                     * TREEIFY_THRESHOLD=8,因为binCount从0开始,也即是链表长度超过8(包含)时,
                     * 转为红黑树。
                     */
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                    	// 转化时会判断数组容量是否大于64,小于64进行扩容,大于64转为红黑树
                        treeifyBin(tab, hash);
                    break;
                }
                /**
                 * 如果在链表的最后一个节点之前找到key值相同的(和上面的判断不冲突,上面是直接通过数组
                 * 下标判断key值是否相同),则替换
                 */
                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;
            //onlyIfAbsent为true时:当某个位置已经存在元素时不去覆盖
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    ++modCount;
    //最后判断临界值,是否扩容。
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

resize方法

HashMap通过resize()方法进行扩容,容量规则为2的幂次。

final Node<K,V>[] resize() {
    Node<K,V>[] oldTab = table;
    int oldCap = (oldTab == null) ? 0 : oldTab.length;
    int oldThr = threshold;
    int newCap, newThr = 0;
    //以前的容量大于0,也就是hashMap中已经有元素了,或者new对象的时候设置了初始容量
    if (oldCap > 0) {
        //如果以前的容量大于限制的最大容量1<<30,则设置临界值为int的最大值2^31-1
        if (oldCap >= MAXIMUM_CAPACITY) {
            threshold = Integer.MAX_VALUE;
            return oldTab;
        }
        /**
         * 如果以前容量的2倍小于限制的最大容量,同时大于或等于默认的容量16,则设置临界值为以前临界值的2
         * 倍,因为threshold = loadFactor*capacity,capacity扩大了2倍,loadFactor不变,
         * threshold自然也扩大2倍。
         */
        else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
                 oldCap >= DEFAULT_INITIAL_CAPACITY)
            newThr = oldThr << 1; // double threshold
    }
    /**
     * 在HashMap构造器Hash(int initialCapacity, float loadFactor)中有一句代码,this.threshold      
     * = tableSizeFor(initialCapacity), 表示在调用构造器时,默认是将初始容量暂时赋值给了
     * threshold临界值,因此此处相当于将上一次的初始容量赋值给了新的容量。什么情况下会执行到这句?当调用     
     * 了HashMap(int initialCapacity)构造器,还没有添加元素时
     */
    else if (oldThr > 0) 
        newCap = oldThr;
    /**
     * 调用了默认构造器,初始容量没有设置,因此使用默认容量DEFAULT_INITIAL_CAPACITY(16),临界值
     * 就是16*0.75
     */
    else {               
        newCap = DEFAULT_INITIAL_CAPACITY;
        newThr = (int)(DEFAULT_LOAD_FACTOR * DEFAULT_INITIAL_CAPACITY);
    }
    //对临界值做判断,确保其不为0,因为在上面第二种情况(oldThr > 0),并没有计算newThr
    if (newThr == 0) {
        float ft = (float)newCap * loadFactor;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?
                  (int)ft : Integer.MAX_VALUE);
    }
    threshold = newThr;
    
    @SuppressWarnings({"rawtypes","unchecked"})
    /**构造新表,初始化表中数据*/
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newCap];
    //将刚创建的新表赋值给table
    table = newTab;
    if (oldTab != null) {
        //遍历将原来table中的数据放到扩容后的新表中来
        for (int j = 0; j < oldCap; ++j) {
            Node<K,V> e;
            if ((e = oldTab[j]) != null) {
                oldTab[j] = null;
                //没有链表Node节点,直接放到新的table中下标为【e.hash & (newCap - 1)】位置即可
                if (e.next == null)
                    newTab[e.hash & (newCap - 1)] = e;
                //如果是treeNode节点,则树上的节点放到newTab中
                else if (e instanceof TreeNode)
                    ((TreeNode<K,V>)e).split(this, newTab, j, oldCap);
                //如果e后面还有链表节点,则遍历e所在的链表,
                else { // 保证顺序
                    Node<K,V> loHead = null, loTail = null;
                    Node<K,V> hiHead = null, hiTail = null;
                    Node<K,V> next;
                    do {
                        //记录下一个节点
                        next = e.next;
                        /**
                         * newTab的容量是以前旧表容量的两倍,因为数组table下标并不是根据循环逐步递增
                         * 的,而是通过(table.length-1)& hash计算得到,因此扩容后,存放的位置就
                         * 可能发生变化,那么到底发生怎样的变化呢,就是由下面的算法得到.
                         *
                         * 通过e.hash & oldCap来判断节点位置通过再次hash算法后,是否会发生改变,如
                         * 果为0表示不会发生改变,如果为1表示会发生改变。到底怎么理解呢,举个例子:
                         * e.hash = 13 二进制:0000 1101
                         * oldCap = 32 二进制:0001 0000
                         *  &运算:  0  二进制:0000 0000
                         * 结论:元素位置在扩容后不会发生改变
                         */
                        if ((e.hash & oldCap) == 0) {
                            if (loTail == null)
                                loHead = e;
                            else
                                loTail.next = e;
                            loTail = e;
                        }
                        /**
                         * e.hash = 18 二进制:0001 0010
                         * oldCap = 32 二进制:0001 0000
                         * &运算:  32 二进制:0001 0000
                         * 结论:元素位置在扩容后会发生改变,那么如何改变呢?
                         * newCap = 64 二进制:0010 0000
                         * 通过(newCap-1)&hash
                         * 即0001 1111 & 0001 0010 得0001 0010,32+2 = 34
                         */
                        else {
                            if (hiTail == null)
                                hiHead = e;
                            else
                                hiTail.next = e;
                            hiTail = e;
                        }
                    } while ((e = next) != null);
                    if (loTail != null) {
                        loTail.next = null;
                        /**
                         * 若(e.hash & oldCap) == 0,下标不变,将原表某个下标的元素放到扩容表同样
                         * 下标的位置上
                         */
                        newTab[j] = loHead;
                    }
                    if (hiTail != null) {
                        hiTail.next = null;
                        /**
                         * 若(e.hash & oldCap) != 0,将原表某个下标的元素放到扩容表中
                         * [下标+增加的扩容量]的位置上
                         */
                        newTab[j + oldCap] = hiHead;
                    }
                }
            }
        }
    }
    return newTab;
}

参考:
JDK源码-关于JDK1.8的HashMap的探究
HashMap原理 — 扩容机制及存取原理

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ConcurrentHashMapJava中线程安全的哈希表实现,它是通过分段锁(Segment)来实现并发访问的。下面是ConcurrentHashMap底层原理: 1. 数据结构:ConcurrentHashMap内部由一个Segment数组组成,每个Segment都是一个独立的哈希表,用于存储键值对。每个Segment维护了一个独立的锁。 2. 分段锁:ConcurrentHashMap使用分段锁来实现并发访问。每个Segment都可以独立地加锁,不同的线程可以同时访问不同的Segment,从而提高并发性能。 3. Hash算法:ConcurrentHashMap使用了与HashMap相同的Hash算法来确定元素在哪个Segment中存储。首先,根据键的hashCode计算出一个哈希值,然后通过哈希值与Segment数组长度进行位运算,得到该元素应该存储在哪个Segment中。 4. 锁粒度:ConcurrentHashMap的锁粒度是Segment级别的,即每个Segment都有一个独立的锁。这样,在多线程并发访问时,只有访问同一个Segment的线程需要竞争锁,而其他Segment的访问不会受到影响,从而提高了并发性能。 5. 扩容:当ConcurrentHashMap中的元素数量达到一定阈值时,会触发扩容操作。扩容时,会对每个Segment进行扩容,而不是对整个ConcurrentHashMap进行扩容。这样可以减小扩容的开销,并且不会影响其他Segment的并发访问。 总结起来,ConcurrentHashMap通过分段锁和哈希算法实现了线程安全的并发访问。每个Segment都是一个独立的哈希表,通过细粒度的锁控制并发访问,从而提高了并发性能。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值