结合一些文章阅读源码后整理的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]在并发环境中可能会不符合预期:

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()首次使用时初始化

差异点

容器类型 底层实现(见说明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. PhpStorm2016.3激活

    选择License server,输入以下任意一个地址: http://idea.imsxm.com/http://114.215.133.70:41017/http://mcpmcc.com:101 ...

  2. Nginx 配置文件语法

    一.语法规则: location [=|~|~*|^~] /uri/ { … } = 开头表示精确匹配 ^~ 开头表示uri以某个常规字符串开头,理解为匹配 url路径即可.nginx不对url做编码 ...

  3. CDN是啥?

    CDN 介绍 CDN ( Content Delivery Network ),也即内容分发网络.通过将网站内容(如图片.JavaScript .CSS.网页等)分发至全网加速节点,配合精准智能调度系 ...

  4. arch 系列manjaro更新deepin-screenshot没有图标

    问题描述 deepin软件安装到其他分支后,这个问题出现,相信各位一点都不意外,原因不细说,简单的概括就是没有DDE的桌面环境!! 简单介绍 deepin-screen截图软件在使用的时候是深受国人的 ...

  5. Python 每日一练(3)

    引言 今天的每日一练,学习了一下用Python生成四位的图像验证码,就是我们常常在登录时见到的那种(#`O′) 思路分析 正如常见的那种图像验证码,它是由数字和字母的随机组合产生的,所以我们首先的第一 ...

  6. shell script的简单使用

    shell script的简单介绍 shell变量 1.命名规则 命名只能使用英文字母,数字和下划线,首个字符不能以数字开头 中间不能有空格,可以使用下划线(_) 不能使用标点符号. 不能使用bash ...

  7. ASP.NET MVC 数据传递 控制器向视图传递

    控制器向视图传递 MVC 控制器向视图传递传递主要分为单页面传递和全局页面传递 1.单页面传递主要是用 ViewData属性 和ViewBag属性 语法: 赋值: ViewData["名称& ...

  8. Java实现 LeetCode 124 二叉树中的最大路径和

    124. 二叉树中的最大路径和 给定一个非空二叉树,返回其最大路径和. 本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列.该路径至少包含一个节点,且不一定经过根节点. 示例 1: 输入: ...

  9. java实现文件管理

    ** 文件管理** 显示"DaSai"目录下以"Ex"开头的文件和目录,写了如下代码,请完善之: import java.io.*; class JavaFil ...

  10. Android开源框架选择

    Android开源项目推荐之「网络请求哪家强」 Android开源项目推荐之「图片加载到底哪家强」 Android网络框架的封装 Android Volley+OkHttp3+Gson(Jackson ...