Java容器相关知识点整理
结合一些文章阅读源码后整理的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 |
说明
- HashMap和ConcurrentHashMap的key桶大小都是2的幂,便于将计算下标的取模操作转化为按位与操作
- Map的key建议使用不可变类如String、Integer等包装类型,其值是final的,这样可以防止key的hash发生变化
- 1.8以后,链表转红黑树的阈值为8,红黑树转回链表的阈值位6。8是链表和红黑树平均查找时间(n/2和logn)的阈值,不在7转回是为了防止反复转换。
- 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进行的。
- 扩容的依据是k-v容量>=扩容阈值threshold,而threshold= table数组大小 * 装载因子。扩容前后hash值没有变,但是取模(^length)变了,所以在新的table中所在桶的下标可能会变
- HashMap1.7的头插法在并发场景下reszie()容易导致链表循环,具体的执行场景见文献[7][9]。这一步不太好理解,我个人是用[9]的示意图自己完整在纸上推演了一遍才理解。关键点在于,被中断的线程,对同一个节点遍历了两次。虽然1.8改用了尾插法,仍然有循环引用的可能[10][11]。
- 1.8的HashMap在resize()时,要将节点分开,根据扩容后多计算hash的那一位是0还是1来决定放在原来的桶[i]还是桶[i+原始length]中。
- 1.7中计算出hash值后,还会使用它计算所在的Segement
- put(key,value)时锁定分段锁,先用非阻塞tryLock()自旋,超过次数上限后升级为阻塞Lock()。
- 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]),但是返回值不是最新的
参考文献
没有在文中特殊标注的文章,是参考了其结构或部分内容,进行了重新组织。
- Vector 是线程安全的?
- 使用ConcurrentHashMap一定线程安全?
- TreeMap原理实现及常用方法
- Java容器常见面试题
- Java高级程序员必备ConcurrentHashMap实现原理:扩容遍历与计数
- Java容器面试总结
- Java:手把手带你源码分析 HashMap 1.7
- Java源码分析:关于 HashMap 1.8 的重大更新 注:本篇的resize()源码和我本地JDK8的不一致!
- HashMap底层详解-003-resize、并发下的安全问题
- JDK8中HashMap依然会死循环!
- HashMap在jdk1.8中也会死循环
- ConcurrentHashMap中tabAt方法分析
- HashMap?ConcurrentHashMap?相信看完这篇没人能难住你!
- ConcurrentHashMap 1.8 计算 size 的方式
- Java集合类框架学习 5.3—— ConcurrentHashMap(JDK1.8)
Java容器相关知识点整理的更多相关文章
- Java并发相关知识点梳理和研究
1. 知识点思维导图 (图比较大,可以右键在新窗口打开) 2. 经典的wait()/notify()/notifyAll()实现生产者/消费者编程范式深入分析 & synchronized 注 ...
- Java 容器相关知识全面总结
Java实用类库提供了一套相当完整的容器来帮助我们解决很多具体问题.因为我本身是一名Android开发者,包括我在内很多安卓开发,最拿手的就是ListView(RecycleView)+BaseAda ...
- EasyUI相关知识点整理
EasyUI相关知识整理 EasyUI是一种基于jQuery.Angular..Vue和React的用户界面插件集合.easyui为创建现代化,互动,JavaScript应用程序,提供必要的功能.也就 ...
- Spring和Springboot相关知识点整理
简介 本文主要整理一些Spring & SpringBoot应用时和相关原理的知识点,对于源码不做没有深入的讲解. 1. 思维导图 右键新窗口打开可以放大. 说明 使用@Configurati ...
- Java 生态核心知识点整理
又到了求职的金三银四的黄金月份,我相信有不少小伙伴已经摩拳擦掌的准备寻找下一份工作. 就目前国内的面试模式来讲,在面试前积极的准备面试,复习整个 Java 知识体系将变得非常重要,可以很负责任的说一句 ...
- 高级 Java 面试通关知识点整理!
1.常用设计模式 单例模式:懒汉式.饿汉式.双重校验锁.静态加载,内部类加载.枚举类加载.保证一个类仅有一个实例,并提供一个访问它的全局访问点. 代理模式:动态代理和静态代理,什么时候使用动态代理. ...
- java某些基础知识点整理
1. \n换行 \r回车 \"双引号 \\反斜杠 2.Java语言提供了八种基本类型.六种数字类型(四个整数型,两个浮点型),一种字符类型,还有一种布尔型. byte: byte 数据类型是 ...
- iframe高度相关知识点整理
IFRAME 元素也就是文档中的文档. contentWindow属性是指指定的frame或者iframe所在的window对象. 用iframe嵌套页面是,如果父页面要获取子页面里面的内容,可以使用 ...
- Java 入门学习知识点整理
[JAVA一个文件写多个类 ( 同级类 ) 规则和注意点] 在一个.java文件中可以有多个同级类, 其修饰符只可以public/abstract/final/和无修饰符 public修饰的只能有一 ...
随机推荐
- PhpStorm2016.3激活
选择License server,输入以下任意一个地址: http://idea.imsxm.com/http://114.215.133.70:41017/http://mcpmcc.com:101 ...
- Nginx 配置文件语法
一.语法规则: location [=|~|~*|^~] /uri/ { … } = 开头表示精确匹配 ^~ 开头表示uri以某个常规字符串开头,理解为匹配 url路径即可.nginx不对url做编码 ...
- CDN是啥?
CDN 介绍 CDN ( Content Delivery Network ),也即内容分发网络.通过将网站内容(如图片.JavaScript .CSS.网页等)分发至全网加速节点,配合精准智能调度系 ...
- arch 系列manjaro更新deepin-screenshot没有图标
问题描述 deepin软件安装到其他分支后,这个问题出现,相信各位一点都不意外,原因不细说,简单的概括就是没有DDE的桌面环境!! 简单介绍 deepin-screen截图软件在使用的时候是深受国人的 ...
- Python 每日一练(3)
引言 今天的每日一练,学习了一下用Python生成四位的图像验证码,就是我们常常在登录时见到的那种(#`O′) 思路分析 正如常见的那种图像验证码,它是由数字和字母的随机组合产生的,所以我们首先的第一 ...
- shell script的简单使用
shell script的简单介绍 shell变量 1.命名规则 命名只能使用英文字母,数字和下划线,首个字符不能以数字开头 中间不能有空格,可以使用下划线(_) 不能使用标点符号. 不能使用bash ...
- ASP.NET MVC 数据传递 控制器向视图传递
控制器向视图传递 MVC 控制器向视图传递传递主要分为单页面传递和全局页面传递 1.单页面传递主要是用 ViewData属性 和ViewBag属性 语法: 赋值: ViewData["名称& ...
- Java实现 LeetCode 124 二叉树中的最大路径和
124. 二叉树中的最大路径和 给定一个非空二叉树,返回其最大路径和. 本题中,路径被定义为一条从树中任意节点出发,达到任意节点的序列.该路径至少包含一个节点,且不一定经过根节点. 示例 1: 输入: ...
- java实现文件管理
** 文件管理** 显示"DaSai"目录下以"Ex"开头的文件和目录,写了如下代码,请完善之: import java.io.*; class JavaFil ...
- Android开源框架选择
Android开源项目推荐之「网络请求哪家强」 Android开源项目推荐之「图片加载到底哪家强」 Android网络框架的封装 Android Volley+OkHttp3+Gson(Jackson ...