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

1. 思维导图

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

2. 容器对比

类名 底层实现 特征 线程安全性 默认迭代器实现(Itr)
ArrayList Object数组 查询快,增删慢 不安全,有modCount 数组下标
LinkedList 双向链表 查询慢,增删快 不安全,有modCount 当前遍历的节点
Vector Object数组 查询快,增删慢 方法使用synchronized确保安全(注1);有modCount 数组下标
Stack Vector 同Vector 同Vector 同Vector
HashSet HashMap (使用带特殊参数的构造方法则为LinkedHashMap) 和HashMap一致 和HashMap一致 和HashMap一致
LinkedHashSet LinkedHashMap 和LinkedHashMap一致 和LinkedHashMap一致 和LinkedHashMap一致
TreeSet TreeMap 和TreeMap一致 和TreeMap一致 和TreeMap一致
TreeMap 红黑树和Comparator(注2) key和value可以为null(注2),key必须实现Comparable接口 非线程安全,有modCount 当前节点在中序遍历的后继
HashMap 见第3节 key和value可以为null 非线程安全,有modCount HashIterator按数组索引遍历,在此基础上按Node遍历
LinkedHashMap extends HahsMap (注3), Node有前驱和后继 可以按照插入顺序或访问顺序遍历(注4) 非线程安全,有modCount 同HshMap
ConcurrentHashMap 见第3节 key和value不能为null 线程安全(注1) 基于Traverser(注5)
Hashtable Entry数组 + Object.hashCode() + 同key的Entry形成链表 key和value不允许为null 线程安全, 有modCount 枚举类或通过KeySet/EntrySet

操作的时间复杂度

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

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

复合操作

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

  1. if (!vector.contains(element))
  2. vector.add(element);
  3. ...
  4. }
  1. ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap();
  2. map.put("key", 1);
  3. // 多线程环境下执行
  4. Integer currentVal = map.get("key");
  5. map.put("key", currentVal + 1);

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

  1. ConcurrentHashMap<String, AtomicInteger> map = new ConcurrentHashMap();
  2. // 多线程环境下执行
  3. 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()首次使用时初始化

差异点

容器类型 底层实现(见说明4) key的hash方法 table下标计算 扩容后table容量(见说明1、5) 插入 clone hash桶的最大容量
Hashtable hash值桶数组 + 链表 hashCode() (hashCode & MAX_INT) % table.length origin*2+1 头部插入 浅拷贝 MAXINT- 8
HashMap(1.7) hash值桶数组 + 链表 String使用sun.misc.Hashing.stringHash32,其他用hashCode()后多次异或折叠(见说明2) (length-1) & hashCode origin*2 头部插入(见说明6) 浅拷贝 2^30
HashMap(1.8) hash值桶数组 + 链表/红黑树(见说明3) hashCode()高低16位异或 (length-1) & hashCode origin*2(见说明7) 尾部插入 浅拷贝 2^30
ConcurrentHashMap(1.7) hash值桶数组 + Segment extends ReentrantLock(见说明9) + 数组 String使用sun.misc.Hashing.stringHash32,其他用hashCode()后多次异或折叠和加法操作(见说明8) (length-1) & hashCode origin*2 头部插入 不支持 2^30
ConcurrentHashMap(1.8) hash值桶数组 + 链表/红黑树(见说明10) hashCode()高低16位异或 % MAX_INT (length-1) & hashCode origin*2 尾部插入 不支持 2^30

说明

  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]),但是返回值不是最新的

参考文献

没有在文中特殊标注的文章,是参考了其结构或部分内容,进行了重新组织。

  1. Vector 是线程安全的?
  2. 使用ConcurrentHashMap一定线程安全?
  3. TreeMap原理实现及常用方法
  4. Java容器常见面试题
  5. Java高级程序员必备ConcurrentHashMap实现原理:扩容遍历与计数
  6. Java容器面试总结
  7. Java:手把手带你源码分析 HashMap 1.7
  8. Java源码分析:关于 HashMap 1.8 的重大更新 注:本篇的resize()源码和我本地JDK8的不一致!
  9. HashMap底层详解-003-resize、并发下的安全问题
  10. JDK8中HashMap依然会死循环!
  11. HashMap在jdk1.8中也会死循环
  12. ConcurrentHashMap中tabAt方法分析
  13. HashMap?ConcurrentHashMap?相信看完这篇没人能难住你!
  14. ConcurrentHashMap 1.8 计算 size 的方式
  15. Java集合类框架学习 5.3—— ConcurrentHashMap(JDK1.8)

Java容器相关知识点整理的更多相关文章

  1. Java并发相关知识点梳理和研究

    1. 知识点思维导图 (图比较大,可以右键在新窗口打开) 2. 经典的wait()/notify()/notifyAll()实现生产者/消费者编程范式深入分析 & synchronized 注 ...

  2. Java 容器相关知识全面总结

    Java实用类库提供了一套相当完整的容器来帮助我们解决很多具体问题.因为我本身是一名Android开发者,包括我在内很多安卓开发,最拿手的就是ListView(RecycleView)+BaseAda ...

  3. EasyUI相关知识点整理

    EasyUI相关知识整理 EasyUI是一种基于jQuery.Angular..Vue和React的用户界面插件集合.easyui为创建现代化,互动,JavaScript应用程序,提供必要的功能.也就 ...

  4. Spring和Springboot相关知识点整理

    简介 本文主要整理一些Spring & SpringBoot应用时和相关原理的知识点,对于源码不做没有深入的讲解. 1. 思维导图 右键新窗口打开可以放大. 说明 使用@Configurati ...

  5. Java 生态核心知识点整理

    又到了求职的金三银四的黄金月份,我相信有不少小伙伴已经摩拳擦掌的准备寻找下一份工作. 就目前国内的面试模式来讲,在面试前积极的准备面试,复习整个 Java 知识体系将变得非常重要,可以很负责任的说一句 ...

  6. 高级 Java 面试通关知识点整理!

    1.常用设计模式 单例模式:懒汉式.饿汉式.双重校验锁.静态加载,内部类加载.枚举类加载.保证一个类仅有一个实例,并提供一个访问它的全局访问点. 代理模式:动态代理和静态代理,什么时候使用动态代理. ...

  7. java某些基础知识点整理

    1. \n换行 \r回车 \"双引号 \\反斜杠 2.Java语言提供了八种基本类型.六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型. byte: byte 数据类型是 ...

  8. iframe高度相关知识点整理

    IFRAME 元素也就是文档中的文档. contentWindow属性是指指定的frame或者iframe所在的window对象. 用iframe嵌套页面是,如果父页面要获取子页面里面的内容,可以使用 ...

  9. Java 入门学习知识点整理

    [JAVA一个文件写多个类 ( 同级类 ) 规则和注意点] 在一个.java文件中可以有多个同级类,  其修饰符只可以public/abstract/final/和无修饰符 public修饰的只能有一 ...

随机推荐

  1. wordpress中文章发布时间不显示?用get_the_date代替the_date

    今天发现,在主题中部分地方使用the_date函数来显示文章发布的时间时,竟然发生不显示时间的情况,再仔细看了一下这些文章,有些都是经过几次修改和保存的,可能是由于the_date只是显示文章第一次发 ...

  2. SpringCloud(二)- Consul介绍、安装、使用

    唯能极于情,故能极于剑有问题或错误请及时联系小编或关注小编公众号 “CodeCow”,小编一定及时回复和改正,期待和大家一起学习交流 此文由四部分组成(Consul简介.安装.实操.总结),别着急,慢 ...

  3. 【JAVA习题三】求s=a+aa+aaa+aaaa+aa...a的值,其中a是一个数字。例如2+22+222+2222+22222(此时共有5个数相加

    import java.util.Scanner; public class a加aa加aaa { public static void main(String[] args) { // TODO A ...

  4. 抛开 Spring ,你知道 MyBatis 加载 Mapper 的底层原理吗?

    原文链接:抛开 Spring ,你知道 MyBatis 加载 Mapper 的底层原理吗? 大家都知道,利用 Spring 整合 MyBatis,我们可以直接利用 @MapperScan 注解或者 @ ...

  5. 使用fileupload组件

    1. 进行文件上传时, 表单需要做的准备: 1). 请求方式为 POST: <form action="uploadServlet" method="post&qu ...

  6. hashMap探析

    本篇文章包括: 数据结构 各个参数 为什么数组的长度是2的整数次方 为什么要将装载因子定义为0.75 为什么链表转红黑树的阈值为8 hash碰撞 put方法 resize方法 jdk7中数组扩容产生环 ...

  7. SSIS 数据类型 第二篇:变量的数据类型

    变量(Variable)用于存储在Package运行时用到的值,集成服务支持两种类型的变量:用户自定义的变量和系统变量,自定义的变量由用户来定义,系统变量由集成服务来定义. 变量的用途十分广泛,用于容 ...

  8. PAT 1036 Boys vs Girls (25分) 比大小而已

    题目 This time you are asked to tell the difference between the lowest grade of all the male students ...

  9. Chisel3 - util - Bitwise

    https://mp.weixin.qq.com/s/MQzX1Ned35ztz0vusPdkdQ   比特相关的操作.   参考链接: https://github.com/freechipspro ...

  10. SSM 的 基本原理与面试相关

    个人博客网:https://wushaopei.github.io/    (你想要这里多有) 一.MyBatis 1.Mybatis出现最多的问题是什么? 问题: 在XML配置文件中语句的书写与对象 ...