集合容器面试题

序号内容
1基础面试题
2JVM面试题
3多线程面试题
4MySql面试题
5集合容器面试题
6设计模式面试题
7分布式面试题
8Spring面试题
9SpringBoot面试题
10SpringCloud面试题
11Redis面试题
12RabbitMQ面试题
13ES面试题
14Nginx、Cancal
15Mybatis面试题
16消息队列面试题
17网络面试题
18Linux、Kubenetes面试题
19Netty面试题

Itreator

是一个迭代器。可以遍历任何Collection接口。可以遍历map

hashCode方法

根据对象的内存地址,将这个内存地址转换为整数类型。

hashmap中的运算

(h = key.hashCode()) ^ (h > > > 16)。比较的时候,如果位数不同,则高位补齐。

%(取模):

^(异或运算): 两个数转为二进制,然后从高位比较,如果相同则为0,不同则为1。得到比较的结果在转换为10进制。

(> > >:无符号右移):

&(位语运算符):两个数都转为二进制,从高位比较,如果两个数都为1则为1,否则为0。

如何边遍历边移除元素

使用iterator。在while中 调用 it.remove()方法。

List和Set的区别

list:有序,可重复的数组。可以插入多个null元素。可以使用for循环,或者迭代器来遍历list。list可以动态的增长,查询时直接通过下标获取到对应的值,查询效率高。List的实现类ArrayList 插入时如果是通过add方法插入,是将元素追加到数组的最后面,这时插入时间复杂度为O(1)。如果调用的是add(int index,E element)方法,这种插入是插入到数组中间,需要移动插入点后的所有元素,以确保数组的连续性。这种移动操作导致相对高的时间复杂度。类似的,当删除ArrayList的元素时,需要指定元素的下标。删除当前元素后,后面的元素需要向前移动一个单位。也涉及到了元素的移动,删除的时间复杂度也是O(n)
List 的实现类LinkedList

set:无序,不可重复。无序性:无序性不等于随机性。HashSet存储的数据,在底层数组中并非是按照数组的索引(下标)的顺序添加的,而是按照数据的hash值添加的。只允许存一个null元素。set只能使用迭代器遍历,因为set是无序的,无法通过下标来取值。查询效率相对较低,删除和插入效率高,删除和插入不会引起元素位置的改变。

ArrayList底层原理

arrayList底层是通过数组实现的。JDK1.8创建ArrayList的时候无参的构造方法是创建了一个空的对象。在调用add方法时才会设置数组的长度为10,如果传入参数的大于10,则取传入的长度。下一步是判断当前数组的长度是否小于数组最小需要的长度,如果小于,则调用扩容的方法,先将数组的长度扩容为当前的1.5倍,如果扩容1.5倍后的长度仍然小于数组最小需要的长度,则扩容的长度去数组最小需要的长度。然后判断扩容的长度是否大于数组允许的最大值,如果大于,则取Inter的maxvalue。最后将数组所有的元素copy到扩容后的新数组中。

用于存储数据的动态数组,数组元素的类型为Object类型。

ArrayList是如何扩容的

1、添加新元素时,当元素个数等于list的容量时触发扩容操作
2、ArrayList创建一个新数据,容量是当前的1.5倍,用于存储扩容后的元素。如果扩容1.5倍后还不足以存储扩容后的元素,则继续扩容为当前元素的大小。
3、ArrayList会将原数组中的所有元素通过System.arraycopy()方法复制到新的数组中。
4、复制完成后,ArrayList会更新其内部的数组引用,使其指向新的数组,并丢弃旧的数组。
5、最后,可以将新的元素添加到扩容后的ArrayList中。

ArrayList和LinkedList区别

1、arrayList是基于数组实现的,它使用的是一块连续的存储空间。LinkedList是基于链表实现的,数据在节点中,节点包括前后指针和数据。
2、随机访问时,arrayList的效率更高,可以直接通过下标找到元素。linkedList则需要遍历,时间复杂度为O(n).
3、arrayList 和 linkedList都不是线程安全的。可以使用Collections.synchronizedList保证线程安全
4、arrayList 内存开销需要在list列表预留一定空间,当添加元素导致数组容量不足时,它会自动扩容,通常是将原数组的内存空间长度扩容至原来的1.5倍。LinkedList的主要内存开销在于需要存储节点信息以及节点指针信息。

CopyOnWriteArrayList

CopyOnWriteArrayList 是一个并发容器。在非复合场景下操作它是线程安全的。

CopyOnWriteArrayList(免锁容器)的好处之一是当多个迭代器同时遍历和修改这个列表时,不会抛出 ConcurrentModificationException。在CopyOnWriteArrayList 中,写入将导致创建整个底层数组的副本,而源数组将保留在原地,使得复制的数组在被修改时,读取操作可以安全地执行。

场景

就是合适读多写少的场景。

CopyOnWriteArrayList 的缺点

由于写操作的时候,需要拷贝数组,会消耗内存,如果原数组的内容比较多的情况下,可能导致 young gc 或者 full gc。
不能用于实时读的场景,像拷贝数组、新增元素都需要时间,所以调用一个 set 操作后,读取到数据可能还是旧的,虽然CopyOnWriteArrayList 能做到最终一致性,但是还是没法满足实时性要求。
由于实际使用中可能没法保证 CopyOnWriteArrayList 到底要放置多少数据,万一数据稍微有点多,每次 add/set 都要重新复制数组,这个代价实在太高昂了。在高性能的互联网应用中,这种操作分分钟引起故障。

CopyOnWriteArrayList 的设计思想

读写分离,读和写分开
最终一致性
使用另外开辟空间的思路,来解决并发冲突

线程安全的List

Collections.synchronizedList(new ArrayList);

CopyOnwriteArrayList

HashSet底层原理

HashSet的底层是通过HashMap实现的。HashSet调用add存的值都是存放到HashMap的key上,value则是统一的new Object()。

HashSet是如何保证数据不重复

HashSet底层add的方法是调用的HashMap的put的方法。add其实是在map中添加了key,map中可以是不重复的。HashMap中比较key是否相等hi先比较hashcode然后再比较equals。

HashMap底层实现原理

HashMap概述: HashMap是基于哈希表的Map接口的非同步实现。 允许使用null值和null键。HashMap不保证映射的顺序。

HashMap的数据结构:HashMap实际上是一个“链表散列”的数据结构,即数组、链表、红黑树的结合体。

HashMap 基于 Hash 算法实现的

put时,利用key的hashCode重新hash,计算出当前对象的元素在数组中的下标。
存储时,如果出现hash值 相同的key,如果key相同,则覆盖原始值;如果key不同(出现冲突),则将当前的key-value放入链表中。
获取时,直接找到hash值对应数组中的下标,在进一步判断key是否相同,从而找到对应的value。

HashMap 的get方法

对key的hashCode()做hash运算,计算index; 如果在bucket里的第一个节点里直接命中,则直接返回; 如果有冲突,则通过key.equals(k)去查找对应的Entry;

  • 若为树,则在树中通过key.equals(k)查找,O(logn);
  • 若为链表,则在链表中通过key.equals(k)查找,O(n)。

HashMap的putVal方法

1、判断数组是否为空,如果为空调用resize方法扩容。
2、计算要插入的key的桶在数组的哪个位置。使用p = tab[(n - 1) & hash] 确定元素存放在哪个桶中,判断这个桶是否为空,如果为空,调用newNode方法直接在这个位置插入一个Node。
3、如果当前key对应的桶中有数据。判断桶中的key的hash值和要插入的key的hash值是否相同,且桶中的key和要插入的key的值是否相等,如果hash值相同且key的值相等,则将当前key-value替换原来的key-value。
4、如果key的hash值不同,或者key的值不同。则判断这条链是否为红黑树,如果是红黑树,则调动putTreeVal方法将key插入到红黑树。
5、如果不是红黑树,则代表当前链是链表。通过自旋,将当前key插入到链表的最尾端。
6、自旋中,先判断当前桶§的的下一个节点是否为空,如果为空,当前桶的下一个节点 等于 newNode 方法插入一个节点。再判断链表的长度是否达到了转红黑树的临界值,如果达到了,调用treeifyBin方法将链表转为红黑树。结束循环。
7、自旋中,如果当前桶(p)的下一个节点不为空,判断要插入的key和当期节点的key的hash值是否相同,如果相同,则跳出循环。如果不同,则继续循环。
8、判断当前桶的下一个节点是否为空,如果不为空,则代表链表中有相同的key。当onlyIfAbsent为false或者旧值为null时,使用传入的value替换链表中节点的旧value。然后访问后回调,将旧value返回。
9、调用++modCount记录修改HashMap的次数。然后判断当前HashMap的大小是否大于阈值,如果大于,则需要调用resize方法进行扩容。最后执行插入后回调afterNodeInsertion。

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

static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
               boolean evict) {
    Node<K,V>[] tab; Node<K,V> p; int n, i;
    //1、判断数组是否为空,如果为空调用resize方法扩容。
    if ((tab = table) == null || (n = tab.length) == 0)
        n = (tab = resize()).length;
    //2、计算要插入的key的桶在数组的哪个位置。使用p = tab[(n - 1) & hash] 确定元素存放在哪个桶中,判断这个桶是否为空,如果为空,调用newNode方法直接在这个位置插入一个Node。
    if ((p = tab[i = (n - 1) & hash]) == null)
        tab[i] = newNode(hash, key, value, null);
    else {
        Node<K,V> e; K k;
        //3、如果当前key对应的桶中有数据。判断桶中的key的hash值和要插入的key的hash值是否相同,且桶中的key和要插入的key的值是否相等,如果hash值相同且key的值相等,则将当前key-value替换原来的key-value。
        if (p.hash == hash &&
            ((k = p.key) == key || (key != null && key.equals(k))))
            e = p;
        //4、如果key的hash值不同,或者key的值不同。则判断这条链是否为红黑树,如果是红黑树,则调动putTreeVal方法将key插入到红黑树。
        else if (p instanceof TreeNode)
            e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
        else {
            //5、如果不是红黑树,则代表当前链是链表。通过自旋,将当前key插入到链表的最尾端。
            for (int binCount = 0; ; ++binCount) {
                //6、自旋中,先判断当前桶(p)的的下一个节点是否为空,如果为空,当前桶的下一个节点 等于 newNode 方法插入一个节点。再判断链表的长度是否达到了转红黑树的临界值,如果达到了,调用treeifyBin方法将链表转为红黑树。结束循环。
                if ((e = p.next) == null) {
                    p.next = newNode(hash, key, value, null);
                    if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
                        treeifyBin(tab, hash);
                    break;
                }
                //7、自旋中,如果当前桶(p)的下一个节点不为空,判断要插入的key和当期节点的key的hash值是否相同,如果相同,则跳出循环。如果不同,则继续循环。
                if (e.hash == hash &&
                    ((k = e.key) == key || (key != null && key.equals(k))))
                    break;
                p = e;
            }
        }
        //8、判断当前桶的下一个节点是否为空,如果不为空,则代表链表中有相同的key。当onlyIfAbsent为false或者旧值为null时,使用传入的value替换链表中节点的旧value。然后访问后回调,将旧value返回。
        if (e != null) { // existing mapping for key
            V oldValue = e.value;
            if (!onlyIfAbsent || oldValue == null)
                e.value = value;
            afterNodeAccess(e);
            return oldValue;
        }
    }
    //9、调用++modCount记录修改HashMap的次数。然后判断当前HashMap的大小是否大于阈值,如果大于,则需要调用resize方法进行扩容。最后执行插入后回调afterNodeInsertion。
    ++modCount;
    if (++size > threshold)
        resize();
    afterNodeInsertion(evict);
    return null;
}

HashMap的resize扩容

条件:

如果bucket(桶)满了(超过load facto(加载因子 0.75)r*current capacity(当前数组大小:一般从16开始。默认创建是16)),就要resize。为了最大程度避免哈希冲突 。

步骤:

HashMap 每次扩容都是建立一个新的 table 数组,长度和容量阈值都变为原来的两倍,然后把原数组元素重新映射到新数组上,具体步骤如下:

  1. 首先会判断 table 数组长度,如果大于 0 说明已被初始化过,那么按当前 table 数组长度的 2 倍进行扩容,阈值也变为原来的 2 倍
  2. 若 table 数组未被初始化过,且 threshold(阈值)大于 0 说明调用了 HashMap(initialCapacity, loadFactor) 构造方法,那么就把数组大小设为 threshold
  3. 若 table 数组未被初始化,且 threshold 为 0 说明调用 HashMap() 构造方法,那么就把数组大小设为 16,threshold 设为 16*0.75
  4. 接着需要判断如果不是第一次初始化,那么扩容之后,要重新计算键值对的位置,并把它们移动到合适的位置上去,如果节点是红黑树类型的话则需要进行红黑树的拆分。
    这里有一个需要注意的点就是在 JDK1.8 HashMap 扩容阶段重新映射元素时不需要像 1.7 版本那样重新去一个个计算元素的 hash 值,而是通过 hash & oldCap 的值来判断,若为 0 则索引位置不变,不为 0 则新索引=原索引+旧数组长度,
    因为我们使用的是 2 次幂的扩展(指长度扩为原来 2 倍),所以,元素的位置要么是在原位置,要么是在原位置再移动 2 次幂的位置。因此,我们在扩充 HashMap 的时候,不需要像 JDK1.7 的实现那样重新计算 hash,只需要看看原来的 hash 值新增的那个 bit 是 1 还是 0 就好了,是 0 的话索引没变,是 1 的话索引变成“原索引 +oldCap

HashMap 的长度为什么是2的幂次方

为了能让 HashMap 存取高效,尽量较少碰撞,也就是要尽量把数据分配均匀,每个链表/红黑树长度大致相同。这个实现就是把数据存到哪个链表/红黑树中的算法。

HashMap 1.8中多线程扩容是如何避免死循环的

HashMap在多线程环境下,同时进行put操作,并且同时进行扩容时,会出现链表环,导致死循环。
因为jdk1.8之前采用的是头插法,新加入的冲突元素将会插到原有链表的头部。
扩容之后,链表上的元素顺序会反过来。这也是造成死循环的原因之一。

jdk1.8后是直接把节点放到扩容后原有链表的尾部,尾插法。但是这种操作也会出现死循环,链表转换树或者对树进行操作(重新平衡红黑树)的时候会出现死循环的问题。

多线程情况下使用hashMap 还会有 数据丢失的问题。

为什么链表的长度是8,且数组长度是64之后才会转为红黑树

为什么要有数组是64的限制

因为数组的查询效率更快,通过数组的下标可以直接定位到数据。所以数组短的时候,即使链表长度到8也不会转为红黑树。

为什么链表长度为8

在随机的hashCode 下,容器中节点的频率遵循泊松分布(Poisson:HashMap注释)。链表达到8个元素的时候,概率已经很低了(概率为亿分之六,注释:0.00000006)。此时树化,性价比会很高。

既不会因为链表太长(8)导致复杂度加大,也不会因为概率太高导致太多节点树化。

为什么数组是64

应该至少为4 * TREEIFY_THRESHOLD = 32,以避免大小调整和树化阈值之间发生冲突。(来自HashMap注释的翻译)

加载因子为什么是0.75

作为一般规则,默认负载因子(.75)在时间和空间成本之间提供了很好的折衷。更高的值会减少空间开销,但会增加查找成本(反映在HashMap类的大多数操作中,包括getput)。(来自HashMap注释的翻译)

红黑树什么时候会转为链表

在resize的时候才会根据 UN TREE IFY_THRESHOLD 进行转换。一般回答为红黑树节点为6个的时候。

HashMap是如何降低Hash冲突

当两个不同的输入值,根据同一哈希计算出相同的哈希值的现象,我们就把它叫做碰撞(哈希碰撞、冲突)。

链式寻址法:Hashmap就是用了单向链表的方式来解决哈希冲突。但会存在链表过长增加遍历时间。假如我们某个桶下对应的链表有n个元素,那么遍历时间复杂度就为O(n),为了针对这个问题,JDK1.8引入了红黑树,使得遍历复杂度降低至O(logn)。

再hash法:使用某个hash函数计算的可以存在冲突时,再用另一个hash函数对这个可以做hash,一直运算到不再产生冲突。但是会增加计算时间,影响性能。

建立公共溢出区:把hash表分为基本表和溢出表两个部分,凡是存在冲突的的元素,一律放入到溢出表中。

红黑树

原则:

  1. 节点是红色或黑色;
  2. 根节点是黑色;
  3. 所有叶子节点都是黑色;(叶子是NULL节点)
  4. 每个红色节点的两个子节点都是黑色;(从每个叶子到根的所有路径上不能有两个连续的红色节点)
  5. 从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点;

HashMap的key为null存的位置

档key 为null时,hashMap 会将 null key 存放在数组的 第0个下标的位置。因为null 没有内存地址,无法进行hashCode运算。

任何类作为 Map的key吗?

可以使用任何类作为HashMap的key(8种基本数据类型不行)。但是需要重写hashCode 和 equals 方法。避免造成内存泄漏。

如果类重写了 equals() 方法,也应该重写 hashCode() 方法。
类的所有实例需要遵循与 equals() 和 hashCode() 相关的规则。
如果一个类没有使用 equals(),不应该在 hashCode() 中使用它。
用户自定义 Key 类最佳实践是使之为不可变的,这样 hashCode() 值可以被缓存起来,拥有更好的性能。不可变的类也可以确保 hashCode() 和 equals() 在未来不会改变,这样就会解决与可变相关的问题了。

HashMap在并发编程环境下有什么问题

  • (1)多线程扩容,引起的死循环问题(1.8已经解决)
  • (2)多线程put的时候可能导致元素丢失
  • (3)put非null元素后get出来的却是null

String Integer适合作为Key吗

String、Integer等包装类的特性能够保证Hash值的不可更改性和计算准确性,能够有效的减少Hash碰撞的几率。但是,8种基本数据类型不能做为key。因为key,value是一个对象。

  1. 都是final类型,即不可变性,保证key的不可更改性,不会存在获取hash值不同的情况
  2. 内部已重写了equals()hashCode()等方法,遵守了HashMap内部的规范,不容易出现Hash值计算错误的情况;

HashMap与HashTable

HashMap:线程不安全,允许存放一个key为null。不指定容量大小put的时候,数组的容量为16,每次扩容为原来的2倍。当链表长度大于8和数组长度大于等于64的时候,链表会转换为红黑树

HashMTable:线程安全,不允许key 为 null。不指定容量大小的话,初始容量为11。每次扩容为原来的2n+1。

ConcurrentHashMap的key和value为什么不允许为null

是为了避免在多线程场景下的歧义问题,也就是说当一个线程从concurrentHashMap中获取key 的时候如果返回的结果是null,那么线程是无法确认这个null表示的确实是不存在这个key,还是说存在这个key,但是value为空。这种不确定性会造成线程安全的问题,concurrentHashMap本身是线程安全的,所以才这样设计。hashmap本身是线程不安全的,所以可以运行key为null。

ConcurrentHashMap如何保证线程安全

1、在ConcurrentHashMap中,关键字段使用volatile修饰,这保证了可见性,使得读操作不需要加锁。对于写操作,它使用CAS(Compare and Swap)操作来确保线程安全。
2、ConcurrentHashMap使用了一种特殊的哈希表结构,即数组+链表+红黑树的结合体。在JDK 1.8版本中,当链表长度大于8且数组长度大于64时,链表会升级为红黑树的结构。
3、ConcurrentHashMap取消了JDK1.7的Segment分段锁,而是对首个节点(table[hash(key)]取得的链接或红黑树的首个节点)进行(synchronized )加锁操作。这种设计进一步降低了锁的粒度,提高了并发性能。

ConcurrentHashMap实现原理

使用volatile 修饰Node,保证全局变量Node的可见性,和禁止指令重排。

采用Node + CAS + Synchronized来保证并发安全进行实现,synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发,效率又提升N倍。

final V putVal(K key, V value, boolean onlyIfAbsent) {
    //ConcurrentHashMap 的key 和value 不能为空。
    if (key == null || value == null) throw new NullPointerException();
    //使用散列算法计算key的hash值。
    int hash = spread(key.hashCode());
    int binCount = 0;
    //自旋。
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //如果当前concurrentHashMap为空,则初始化一个Node
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        //通过 (n - 1) & hash 计算当前位置的桶是否存在值,如果为空
        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。如果当前位置的hash值为-1,代表这个桶这在扩容
        else if ((fh = f.hash) == MOVED)
            //正在扩容的时候,helpTransfer方法有助于扩容。
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            //对当前桶进行加锁。
            synchronized (f) {
                //i = (n - 1) & hash。还是当前位置,这里是判断当前位置的数据是否被修改
                if (tabAt(tab, i) == f) {
                    //fh = f.hash。当前位置的hash值大于0,代表当前位置是链表,否则是红黑树
                    if (fh >= 0) {
                        binCount = 1;
                        //开始自旋
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //判断当前位置的key的hash值是否和要添加的key的hash值相同
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                //e:当前Node,oldVal = 当前桶的value
                                oldVal = e.val;
                                //根据onlyIfAbsent选择是否覆盖旧值
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            //如果链表中没有hash值相同的数据,就一直自旋,
                            //直到循环到当前节点的下一个节点为空的时候,将要新增的key-value放到一个新的Node中,并将当前节点的下一个节点指向这个Node
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    //判断当前位置是否为红黑树。使用的是TreeBin不是TreeNode。
                    //TreeBin持有红黑树引用,并且会加锁,保证线程安全。
                    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不为空,并且oldVal有值的情况下,说明已经新增成功了。
            if (binCount != 0) {
                //判断TREEIFY_THRESHOLD:8,判断长度是否大于8,大于8就转为红黑树。
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    //判断容器是否需要扩容,如果需要扩容,则调用 transfer 方法进行扩容。
    //如果已经在扩容中了,判断是否完成扩容。
    addCount(1L, binCount);
    return null;
}

散列算法

定义:就是HashMap、ConcurrentHashMap如何基于key进行运算,并将key-value存储到数组的某一个节点上,或者是挂载到下面的链表或者红黑树上。

ConcurrentHashMap 的key 和value 是不可以存null的,使用散列算法,可以保证ConcurrentHashMap 的key为正数。

HashMap是key 和 value是可以存null的,对key做hash运算不需要散列算法保证key为正数。hashmap的key为null,数组的下标为0。

运算符(^,&,|,~)

^运算

符号左右两边的二进制数,相应的位相同为0,不相同为1。

例如:1000^1010 = 0010 。第一位: 1 和 1 相同,则为0。第二位:0 和 0 相同,则为0。第三位:0 和 1 不同,则为1。第四位:0 和 0相同,则为0。

&运算

符号两边都为1,才为1。

|运算

符号两边一个为1 ,就为1。

~运算

将该数的所有二进制位全取反。

例:h = 01010 , ~h 后 为 1 0101。

右移运算

逻辑右移,即若该数为正,则高位补0,而若该数为负数,则右移后高位同样补0。将32的高16位补为0,被补成0的高16位变成低16位。

例:h = 01010101 11111111 10101010 0000

​ h >>> 16 后的值是 00000000 00000000 01010101 11111111

//concurrentHashMap的散列算法
int hash = spread(key.hashCode());
//将一个int 类型的hashCode h 转为二进制(不够32位,前面补0),进行右移16(>>> 16)位运算,得到的值再和 h 进行异或运算(&)。
static final int spread(int h) {
    return (h ^ (h >>> 16)) & HASH_BITS;
}
// static final int HASH_BITS = 0x7fffffff; 一个16进制的数字,转为二进制是 01111111 11111111 11111111 11111111
//二进制的最高位是符号位,0为正数,1为负数。
// h 和 HASH_BITS 做 & 运算,h做完异或运算后 和 HASH_BITS做 & 运算,第一位必定是0,这样就保证了数据是正数。因为hash值为负数有特殊含义。

//在确定数据存放到数组的哪个位置时,是要基于hash值与数组的长度 - 1进行&运算的,因为数组的长度不会特别的长,hash值的高位一般不参与到运算中,需要在计算索引位置之前先将高位右移16位,与原来的hash值进行^异或运算,让高16位也参与到计算索引位置的运算中。(为了尽量打散HashMap中的数据)
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
}

为什么右移16位

是为了让高位hash值参与运算。让 hash 值的散列度更高,尽可能减少 hash 表的 hash 冲突,从而提升数据查找的性能

hash值的特殊含义

//hash值为-1,代表当期数据迁移到了新数组中。(正在扩容)
static final int MOVED     = -1; // hash for forwarding nodes
//hash值为-2,当前索引位置下是一颗红黑树
static final int TREEBIN   = -2; // hash for roots of trees
//hash值为-3,代表当前索引位置已经被占座了,只是数据还没添加进来compute和computeIfAbsent
static final int RESERVED  = -3; // hash for transient reservations

ConcurrentHashMap保证安全的方式

hashtable:将方法追加上synchronized 保证线程安全。

jdk1.7的ConcurrentHashMap:是使用分段锁segment(继承了ReentrantLock),保证线程安全的。

jdk1.8的ConcurrentHashMap:基于CAS和synchronized 同步代码块实现线程安全的。

jdk1.8

1、插入数据时,数组索引位置没数据,那就使用CAS的方式,将数据插入到索引位

//如果当前数组索引位置没数据 
//f : Node节点。数组中的元素,链表,红黑树。synchronized 锁住的就是这个对象。
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
}

2、插入数据时,数组索引位置有数据,可能需要追加到链表或者红黑树上,这时候synchronized 锁住了当前的链表或者红黑树,但是不影响其他的桶,其他的可以正常操作。

V oldVal = null;
//使用synchronized 锁住f Node节点 。
synchronized (f) {

}

ConcurrentHashMap扩容

sizeCtl的含义

sizeCtl = -1:当前ConcurrentHashMap的数组正在初始化

sizeCtl < -1:当前ConcurrentHashMap正在扩容,值为-2代表有一个线程在扩容。-3 代表两个线程在扩容,

sizeCtl = 0:还没初始化

sizeCtl > 0:如果还没初始化,代表初始数组长度。如果已经初始化了,就代表扩容阈值。

ConcurrentHashMap在第一次put时,才会初始化数组。

触发扩容的条件

1、数组长度达到扩容的阈值

2、链表达到了8,数组长度没到64。

3、执行putAll操作时,会直接优先判断是否需要扩容。

有些方法扩容时,有的会先执行tryPresize方法,有的会执行判断逻辑,计算扩容戳,执行transfer方法开始扩容。

扩容戳

ConcurrentHashMap扩容时,会触发helpTransfer操作,也就是多线程扩容。扩容前,会先计算扩容戳(resizeStamp方法),扩容戳只是一个标识,并不是完整的扩容。

完整的扩容戳: (rs << RESIZE_STAMP_SHIFT) + 2)

扩容戳是 一个负数,高16位表示当前老数组的长度,用来保证多线程扩容是从同样的长度开始扩容,到2倍长度。低16位,用来标识当前正在扩容的线程的(个数 -1) 。

多线程扩容时,多个线程扩容的长度都是一样的。

//扩容方法transfer
else if (U.compareAndSwapInt(this, SIZECTL, sc,
                             (rs << RESIZE_STAMP_SHIFT) + 2))
    transfer(tab, null);
s = sumCount();

流程

1、在扩容时,会先指定每个线程的长度,最小值为16(根据数组和CPU内核去指定每次扩容长度)。

2、开始扩容,开始扩容的线程只有一个,第一个扩容的线程需要将新数组new 出来。有了新数组之后,其他线程再执行transfer方法(可能从helpTransfer进来),其他线程进来后,对扩容戳进行 +1 操作。也就是如果一个线程低位是-2,那么两个线程低位是-3。

3、每次迁移时,会从后往前迁移数据,也就是说两个线程并发扩容:

线程A负责索引位置:16~31

线程B负责索引位置:15~0

一个桶一个桶的迁移数据,每次迁移完一个桶后,会将ForwardingNode(hash值为-1,代表当期数据迁移到了新数组中。(正在扩容))设置到老数组中,证明当前老数组的数据已经迁移到新数组中了。

在迁移链表数据时,会基于lastRun机制,提升效率。

lastRun机制:提前将链表数据进行计算,算出链表需要将数据存放到哪个新数组的位置,将不同位置算完打个标记。

//transfer方法中
Node<K,V> lastRun = f;
for (Node<K,V> p = f.next; p != null; p = p.next) {
    int b = p.hash & n;
    if (b != runBit) {
        runBit = b;
        lastRun = p;
    }
}

ConcurrentHashMap哪线程不安全

1、数据覆盖

2、扩容时数据会造成丢失。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值