说说 ArrayList

动态数组,初始容量10,扩容1.5倍,随机访问O(1)。

并发场景下有什么问题

迭代器 fail-fast,遍历的同时修改会抛出异常。
并发访问可以考虑以下2种方式:

  1. 使用 Collections.synchronizedList,写操作会阻塞迭代
  2. 使用 CopyOnWriteArrayList,写操作会在新复制的数组上进行,不影响旧数组迭代

    什么是 fail-fast

    防止集合在迭代过程中,被其它线程修改。
    记录修改次数 modCount,并发修改可能触发 fail-fast,抛出异常。
    可能不触发,因为 Iterator 实现的 hasNext() 没有比对 modCount。

    对比 ArrayList、LinkedList

    ArrayList 是动态数组,随机访问快,但 add 可能要扩容、移动元素,效率降低。
    LinkedList 是双端队列,随机访问慢,但 getFirst、getLast、add 操作效率很高。

    说说 HashMap

    为什么 capacity 必须是2的幂

    因为分桶索引 index = (n - 1) & hash()。
    当 n 是 2 的幂,(n - 1) 用二进制表示全都是1,只要 hash() 保证均匀分布,那么分桶的结果也均匀分布。

    为什么扩容总是2倍

    为了避免再哈希。扩容两倍可以让元素留在旧索引,或移动到新索引 = 旧索引 + 旧 capacity

    为什么 TREEIFY_THRESHOLD 是 8

    hash() 取 hashCode 的高16位、低16位异或运算的结果,使其服从泊松分布。
    当 hash 服从泊松分布,binCount 为 8 的概率小于百万分之一,因此树化概率不大,不用担心 HashMap 性能。

    为什么使用红黑树

    红黑树的综合性能最佳,查询效率优于 BST,而修改效率优于 AVL。

    并发场景下会有什么问题

  3. 链表使用头插法,resize 的时候会生成环。JDK8 使用尾插法解决,但插入效率降低,所以引入红黑树。

  4. 更新丢失。对象A、对象B 属于同一个 bucket,但不 equals,如果并发执行,后者可能会覆盖前者。
  5. size异常。代码中 size++ 自增不能保证原子性,会导致size异常。

    死锁形成的4个条件

  6. 互斥。线程独占资源,其它线程只能等待。

  7. 请求和保持。线程保持一个资源,但又请求了新的资源,而该资源已被其它进程占有。
  8. 不剥夺。线程不会主动释放资源。
  9. 循环等待。A等待B完成,B等待A完成,形成一个循环等待环路。

    哪些类适合作为 key

    使用不可变类 String、Integer,已经重写 equals()、hashCode()
    使用可变类 List,那么 put 进去的值可能取不出来,因为 hashCode 是可变的

    如何设计一个可以作为 key 的不可变类

    重写 equals()、hashCode(),要保证两个 equals 的对象一定拥有相同的 hashCode。

  10. 类 final,不允许继承

  11. 成员变量 private,不提供修改接口
  12. 构造器,使用深拷贝 (deep-copy) 初始化成员变量

    说说 ConcurrentHashmap

    // 参考文献

    区分 HashMap、TreeMap、LinkedHashMap

    LinkedHashMap 继承 HashMap,保存了记录的插入顺序,从而实现 HashMap 的顺序遍历
    TreeMap 实现 SortMap,按自然顺序、自定义顺序遍历

    参考文献