常用集合类与ThreadLocal总结

135 阅读6分钟

学习参考:面经手册 - 小傅哥

ArrayList(数组)

源码阅读地址:juejin.cn/post/684490…

只能存储对象类型,底层由数组构成,在插入数据时按需动态扩容、数据拷贝等;

1.底层由数组实现,默认初始化空间容量为 10;

2.添加元素时将元素添加到数组末尾;

3.扩容 = 原始长度 + 右移一位 (原始长度 + 原始长度/2);

4.删除元素时会检查长度,索引越界异常由rangeCheck(index)触发;

5.删除元素时将指定位置元素指向oldValue,长度减1,将指定位置(index)+1 上的元素都往前移动一位; System.arraycopy(),将最后面的一个元素置空,方便垃圾回收器回收;

6.set(int index, E element)修改指定位置元素,返回oldValue;

LinkedList(链表)

源码阅读地址:juejin.cn/post/684490…

  • 底层基于双向链表实现,具体实现方式由Node节点对象构成;
private static class Node<E> {
    E item;
    Node<E> next;
    Node<E> prev;
    Node(Node<E> prev, E element, Node<E> next) {
        this.item = element;
        this.next = next;
        this.prev = prev;
    }
}

E item :当前节点的值;
Node<E> next :当前节点的后一个节点的引用(可以理解为指向当前节点的后一个节点的指针)
Node<E> prev:当前节点的前一个节点的引用(可以理解为指向当前节点的前一个节点的指针)  
  • 添加元素默认在末尾,可以指定首、尾位置添加元素
  • 删除元素将指定位置节点设置为null,然后将链表再连接起来

ArrayList的修改与查询效率高,增删效率慢,因为ArrayList底层基于数组实现,可以通过索引快速定位到需要查询或者修改的元素,定位时间复杂度为O(1),但是在增加和删除的时候需要涉及到扩容需要将前面的数据进行复制;

LinkedList恰好相反,底层基于链表实现,在获取元素的时候需要先获取对应的节点,定位元素的时间复杂度为O(n),但是在删除和增加的时候只需要修改前后链表指针即可;

HashMap(数组+链表/红黑树)

源码阅读地址:juejin.cn/post/684490…

扩容时refresh参考文章:www.cnblogs.com/zwh0910/p/1…

涉及知识点:散列实现、扰动函数、初始化容量、负载因子、扩容元素拆分、链表树化、红黑树、crud、分段锁等

底层数据结构由 数组 + 链表/红黑树实现,数组的查询时间负载度为O(1)、链表为O(n)、红黑树为O(logn)   
红黑树特征:
    - 根节点是黑色
    - 节点是红黑或者黑色
    - 所有叶子节点都是黑色
    - 每个红色节点必须有两个黑色子节点
    - 黑高,从任一节点到齐每个叶子节点,经过的路径都包含相同数目的黑色节点
  • 默认初始化容量为 16,默认加载因子为 0.75
  • 为什么HashMap中链表的长度大于8且数组中实际元素个数大于64才转成红黑树?

因为链表长度小于8或者整体桶数量小于64时,hash冲突并不严重;

  • 日常使用设置初始化容量 = (实际使用容量 / 0.75) + 1
  • 扩容时不需要重新计算key的hash值 (e.hash & oldCap) == 0
扰动函数用于优化key散列效果
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
为什么不直接使用hashCode,因为hashCode是一个int值,取值范围【-2147483648, 2147483647】,不可能初始化这么大的数组,HashMap默认长度为
static final int DEFAULT_INITIAL_CAPACITY = 1 << 4; // aka 16

h = key.hashCode()) ^ (h >>> 16)
将key的hash值右移16位 + 与原哈希值做异或运算
为什么初始化容量、扩容都是2的幂次方?
HashMap的容量为什么是2的n次幂,和这个(n - 1) & hash的计算方法有着千丝万缕的关系,符号&是按位与的计算,这是位运算,计算机能直接运算,特别高效,按位与&的计算方法是,只有当对应位置的数据都为1时,运算结果也为1,当HashMap的容量是2的n次幂时,(n-1)的2进制也就是1111111***111这样形式的,这样与添加元素的hash值进行位运算时,能够充分的散列,使得添加的元素均匀分布在HashMap的每个位置上,减少hash碰撞

ThreadLocal

涉及知识点: 数据结构、 拉链寻址、斐波那契散列、神奇的 0x61c88647、弱引用 Reference、过期 key 探测清理 和启发式清理、内存泄露等;

  • 本质是在Thread中维护的一个成员变量:ThreadLocal.ThreadLocalMap threadLocals = null;
  • ThreadLocalMap对象底层由Entry数组构成,初始化容量为16key为ThreadLocal对象,value为传入对象;
  • key为弱引用实现,在发生GC时会被垃圾回收;
  • 插入数据时,该索引下标位置不存在元素,直接插入 e = tab[i = nextIndex(i, len)])
  • 如果元素存在,判断是否key值相等,如果相等就更新,不相等调用replaceStaleEntry探测式清理
  • 扩容时会先进行启发式清理cleanSomeSlots,扩容条件为大于 len * 2/3,2倍长度扩容,扩容时重新计算hash值填充元素到新数组中;

思考:

如果ThreadLcoal对象没有外部强引用关系,在GC时会进行对象回收,那么会导致ThreadLocalMap中的Entry对象key为null,那么就没有办法访问这些key为null的Entry,如果此时线程迟迟不结束,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

永远无法回收,就会产生内存溢出;

那么为什么ThreadLocalMap的key要设计成弱引用呢?如果key设计成强引用且没有手动remove(),那么key会和value一样伴随线程的整个生命周期,如果key是弱引用,被GC后至少ThreadLocal被回收了,在下一次的set()、get()、remove()还会回收key为null的Entry的value;

建议:

1.使用ThreadLocal,建议用static修饰 static ThreadLocal headerLocal = new ThreadLocal();
2.使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况;

3.不要在线程池中线程对象使用ThreadLocal;

知识拓展点:

  • 内存溢出(Out Of Memory) :就是申请内存时,JVM没有足够的内存空间。通俗说法就是去蹲坑发现坑位满了;
  • 内存泄露(Memory Leak) :就是申请了内存,但是没有释放,导致内存空间浪费。通俗说法就是有人占着茅坑不拉屎;
  • 强引用(StrongReference): 只有在根节点不可达时,GC才回收对象;
  • 软引用(SoftReference) :在内存空间不足时,GC才回收此类对象;
  • 弱引用(WeakReference) :只要发生GC就会回收此类对象;
  • 虚引用(PhantomReference) :虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中,具体回收策略由队列实现,可以控制堆外内存回收;