Java容器相关知识点整理,梳理好,靠这些拿到了京东的offer

 

Java容器相关知识点整理

结合一些文章阅读源码后整理的Java容器常见知识点。对于一些代码细节,本文不展开来讲,有兴趣可以自行阅读参考文献。

1. 思维导图

各个容器的知识点比较分散,没有在思维导图上体现,因此看上去右半部分很像类的继承关系。

Java容器相关知识点整理,梳理好,靠这些拿到了京东的offer

2. 容器对比

Java容器相关知识点整理,梳理好,靠这些拿到了京东的offer

操作的时间复杂度

  • ArrayList下标查找O(1),插入O(n)
  • 涉及到树,查找和插入都可以看做log(n)
  • 链表查找O(n),插入O(1)
  • Hash直接查找hash值为 O(1)

注1:关于容器的线程安全

复合操作

无论是Vetcor还是SynchronizedCollection甚至是ConcurrentHashMap,复合操作都不是线程安全的。如下面的代码[1]在并发环境中可能会不符合预期:

if (!vector.contains(element)) 
    vector.add(element); 
    ...
}
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap();
map.put("key", 1);

// 多线程环境下执行
Integer currentVal = map.get("key");
map.put("key", currentVal + 1);

在复合操作的场景下,通用解法是对容器加锁,但这样会大幅降低性能。根据具体的场景来解决效果更好,如第二段代码的场景,可以改写为[1]

ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap();
// 多线程环境下执行
map.get("key").incrementAndGet();

modCount和迭代器Iterator问题

modCount是大多数容器(比如ConcurrentHashMap就没有)用来检测是否发生了并发操作,从而判断是否需要抛出异常通知程序员去处理的一个简单的变量,也被称为fast-fail。
一开始我注意到,Vector也有modCount这个属性,这个字段用来检测对于容器的操作期间是否并发地进行了其他操作,如果有会抛出并发异常。既然Vector是线程安全的,为什么还会有modCount?顺藤摸瓜,我发现虽然Vector的Iterator()方法是synchronized的,但是迭代器本身的方法并不是synchronized的。这就意味着在使用迭代器操作时,对Vector的增删等操作可能导致并发异常。
为了避免这个问题,应该在使用Iterator时对Vector加锁。
同理可以推广到
Collecitons.synchronizedCollection()方法,可以看到这个方法创建的容器,对于迭代器和stream方法,都有一行// Must be manually synched by user!的注释。

注2:TreeMap的comparator和key

comparator是可以为空的,此时使用key的compare接口比较。因此,这种情况下如果key==null会抛NPE。

注3:

JDK8的HashMap中有afterNodeAccess()、afterNodeInsertion()、afterNodeRemoval()三个空方法,在LinkedHashMap中覆盖,用于回调。

注4:LinkedHashMap插入顺序和访问顺序

插入顺序不必解释。访问顺序指的是,每次访问一个节点,都将它插入到双向链表的末尾。

注5:Traverser

其实现类EntryIterator的构造方法实际上是有bug的[5]:它与子类的参数表顺序不一致。它能确保在扩容期间,每个节点只访问一次。这个原理比较复杂,我没有深入去看,可以参考本小节的参考文献。

3. Hashtable & HashMap & ConcurrentHashMap

这是一个老生常谈的话题了,但是涉及面比较广,本节好好总结一下。本节不列出具体的源码,大部分直接给出结论,源码部分分析可以参考文献[7][8]。table表示Map的hash值桶,即每一个元素对应所有同一个hash值的key-value对。

相同点

  • keySet、values、entrySet()首次使用时初始化

差异点

Java容器相关知识点整理,梳理好,靠这些拿到了京东的offer

说明

  1. HashMap和ConcurrentHashMap的key桶大小都是2的幂,便于将计算下标的取模操作转化为按位与操作
  2. Map的key建议使用不可变类如String、Integer等包装类型,其值是final的,这样可以防止key的hash发生变化
  3. 1.8以后,链表转红黑树的阈值为8,红黑树转回链表的阈值为6。8是链表和红黑树平均查找时间(n/2和logn)的阈值,不在7转回是为了防止反复转换。
  4. 1.7的HashMap的Entry和1.8中的Node几乎是一样的,区别在于:后者的equals()使用了Objects.equals()做了封装,而不是对象本身的equals()。另外链表节点Node和红黑树节点TreeNode没有关系,后者是extends LinkedHashMap的Node,通过红黑树查找算法找value。1.7的ConcurrentHashMap的Node中value、next是用volatile修饰的。但是,1.8的ConcurrentHashMap有TreeNode<K,V> extends Node<K,V>,遍历查找值时是用Node的next进行的。
  5. 扩容的依据是k-v容量>=扩容阈值threshold,而threshold= table数组大小 * 装载因子。扩容前后hash值没有变,但是取模(^length)变了,所以在新的table中所在桶的下标可能会变
  6. HashMap1.7的头插法在并发场景下reszie()容易导致链表循环,具体的执行场景见文献[7][9]。这一步不太好理解,我个人是用[9]的示意图自己完整在纸上推演了一遍才理解。关键点在于,被中断的线程,对同一个节点遍历了两次。虽然1.8改用了尾插法,仍然有循环引用的可能[10][11]。
  7. 1.8的HashMap在resize()时,要将节点分开,根据扩容后多计算hash的那一位是0还是1来决定放在原来的桶[i]还是桶[i+原始length]中。
  8. 1.7中计算出hash值后,还会使用它计算所在的Segement
  9. put(key,value)时锁定分段锁,先用非阻塞tryLock()自旋,超过次数上限后升级为阻塞Lock()。
  10. 1.8的ConcurrentHashMap抛弃了Segement,使用synchronized+CAS(使用tabAt()计算所在桶的下标,实际是用UNSAFE类计算内存偏移量)[12]进行写入。具体来说,当桶[i]为空时,CAS写值;非空则对桶[i]加锁[13]。

ConcurrentHashMap的死锁问题

1.7场景

对于跨段操作,如size()、containsValue(),是需要按Segement的下标递增逐段加锁、统计,然后按原先顺序解锁的。这样就有一个很严重的隐患:如果线程A在跨段操作时,中间的Segement[i]被线程B锁定,B又要去锁定Segement[j] (i>j),此时就发生了死锁。

1.8场景

由于没有段,也就没有了跨段。但是size()还是要统计各个桶的数目,仍然有跨桶的可能。如何计算?如果没有冲突发生,只将 size 的变化写入 baseCount。一旦发生冲突,就用一个数组(counterCells)来存储后续所有 size 的变化[14]。而containsValue()则借助了Traverser(见第2节注5及参考文献[15]),但是返回值不是最新的

今天也给大家带来了独家密的BATJ的面试题和相关学习资料,进大厂就从现在开始吧!想获取这些资料的可以关注小编,转发收藏文章,私信小编【学习】来免费获取吧

Java容器相关知识点整理,梳理好,靠这些拿到了京东的offer

BATJ及大厂面试题

Java容器相关知识点整理,梳理好,靠这些拿到了京东的offer

Java容器相关知识点整理,梳理好,靠这些拿到了京东的offer

JVM系列实战宝典

Java容器相关知识点整理,梳理好,靠这些拿到了京东的offer

名厂面试题

Java容器相关知识点整理,梳理好,靠这些拿到了京东的offer

坦克大战100集系列—23种设计模式

Java容器相关知识点整理,梳理好,靠这些拿到了京东的offer

有了这份面试题,自己梳理好,我不信你进不去大厂,想获取这些资料的可以关注小编,转发收藏文章,私信小编【学习】来免费获取吧!

猜你喜欢

转载自blog.csdn.net/ITjianshuzhai/article/details/106836393
今日推荐