1、List实现方式
底层数据结构 | 类 | 实现方式 | 线程安全性 | 初始容量 | 优点 | 缺点 | 备注 |
---|---|---|---|---|---|---|---|
数组 | ArrayList | 增加或减少元素时,需要考虑扩容;直接修改源数据。 | 非线程安全 | 初始容量:1.6和之前为10。jdk1.7+默认为0,增加一个元素后为10 扩容增量:1.6和之前为原容量的 1.5倍+1一次扩容后是容量为16。1.7和之后是1.5倍也就是15 | 按索引查找元素快 | 新增或减少元素需要考虑数组容量问题,数组的扩容与缩容有一定的性能损耗 | java.util |
链表 | LinkedList | 双端链表;增加元素时添加到尾节点;删除元素时,删除首节点。 | 非线程安全 | - | 新增、删除元素效率高 | 查找元素稍慢 | java.util |
数组 | CopyOnWriteArrayList | 增加或减少元素时,都会创建一个新的数组,并且加锁 | 线程安全 | - | 按索引查找元素快;不需要考虑数组的扩容与缩容 | 增加或减少元素的操作都会创建一个新数组,并发环境下内存消耗需要考虑; | java.util.concurrent |
list | SynchronizedList | 包装list,对每一个list的方法,都用synchronized修饰(静态代理的形式) | 线程安全 | - | 优点与其底层的list一样 | 缺点与其底层的list一样;同时,因为每一个方法都加锁,所以性能稍差 | Collections内部类 |
2、Set实现方式
类 | 数据结构 | 线程安全性 | 是否有序 | 初始容量 | 容量因子 | 是否可以为null |
---|---|---|---|---|---|---|
HashSet | 在 JDK 1.7 之前,哈希表,它是一个以链表为元素的数组。 在 JDK 1.8 之后,链表数组 + 二叉树,当链表长度大于 8 时,就将量表换成二叉树。 数组+链表(底层就是HashMap) | 非线程安全 | 无序 | 16 | 0.75 | 可以有一个Null |
TreeSet | 红黑树(底层就是TreeMap) | 非线程安全 | 有序(自然排序和定制排序) | - | - | 不能为Null |
LinkedHashSet | 链表和哈希表 | 非线程安全 | 以插入顺序存储 | - | - | 可以有一个Null |
3、Map
1、实现方式
类 | 并发性 | 线程安全 | 有序性 | 底层数据结构 | 初始容量 | 负载因子 | 实例化方式 | 一致性 | k/v是否可为null | |
---|---|---|---|---|---|---|---|---|---|---|
HashMap | 不支持 | 不安全 | 无序 | 数组+链表/红黑树(1.8才有)。链表转红黑树的阈值是8。红黑树转链表是6。 | 16一次扩容后是容量为32(2的5次方) | 0.75 | 懒加载 | - | k/v可为null | |
LinkedHashMap | 不支持 | 不安全 | 有序(插入序或者访问序) | 数组+单向链表+双向链表 | - | - | - | - | k/v可为null | |
TreeMap | 不支持 | 不安全 | 自然序(左小右大) | 红黑树 | - | - | - | - | 仅v能为null | |
ThreadLocalMap | 不支持 | 不是绝对的线程安全 | 无序 | 数组 | 16 | 0.75 | 懒加载 | - | 仅v能为null | |
HashTable | 支持 | 安全synchronized实现(锁住全部数据) | 无序 | 数组加链表 | 11(2n+1,下一次为23) | 0.75 | 初始化创建 | 强一致性 | 均不能为null | |
ConcurrentHashMap(1.7) | 支持 | 安全 | 无序 | 分段锁+数组+链表 | 16 | 0.75 | 懒加载 | 强一致性 | 均不能为null | |
ConcurrentHashMap(1.8) | 支持 | 安全 | 无序 | 数组+链表/红黑树+锁分段(通过Segment数组+分段锁实现线程安全:每一把锁用于锁容器其中一部分数据) | 16 | 0.75 | 懒加载 | 强一致性 | 均不能为null | |
ConcurrentSkipListMap | 支持 | 安全 | 自然序(左小右大) | 跳跃表 | - | - | - | 强一致性 | 均不能为null |
2、HashMap扩容过程
区别 | JDK1.7 | JDK1.8 |
---|---|---|
JDK1.7 | 数组 + 链表 | 数组 + 链表 + 红黑树 |
存储结构初始化方式 | 单独函数:inflateTable() | 直接集成到了扩容函数==resize()==中 |
hash值计算方式 | 扰动处理 = 9次扰动 = 4次位运算 + 5次异或运算 | 扰动处理 = 2次扰动 = 1次位运算 + 1次异或运算 |
存放数据的规则 | 无冲突时,存放数组;冲突时,存放链表 | 无冲突时,存放数组;冲突 & 链表长度 < 8 :存放单链表;冲突 & 链表长度 > 8 & 数组长度 < 64 : 扩容;冲突 & 链表长度 > 8 & 数组长度 > 64:树化并存放红黑树 |
插入数据方式 | 头插法(先将原位置的数据移到后1位,再插入数据到该位置) | 尾插法(直接插入到链表尾部/红黑树) |
扩容后存储位置的计算方式 | 全部按照原来方法进行计算(即hashCode ->> 扰动函数 ->> (h&length-1)) | 按照扩容后的规律计算(即扩容后的位置=原位置 or 原位置 + 旧容量) |
- 调用put方法时:根据key的hashCode,计算出将key放入数组的index下标,index= (数组长度 - 1) & hashCode
3、为什么ConcurrentHashMap是线程安全的?
-
为什么ConcurrentHashMap是线程安全的
JDK1.7中,ConcurrentHashMap使用的锁分段技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问。 -
那说说JDK1.7中Segment的原理
刚刚说的一段一段就是指Segment,它继承了ReentrantLock,具备锁和释放锁的功能。ConcurrentHashMap只有16个Segment,并且不会扩容,最多可以支持16个线程并发写。 -
JDK1.8的ConcurrentHashMap怎么实现线程安全的
JDK1.8放弃了锁分段的做法,采用CAS和synchronized方式处理并发。以put操作为例,CAS方式确定key的数组下标,synchronized保证链表节点的同步效果。 -
JDK1.8的做法有什么好处呢
- 减少内存开销
假设使用可重入锁,那么每个节点都需要继承AQS,但并不是每个节点都需要同步支持,只有链表的头节点(红黑树的根节点)需要同步,这无疑消耗巨大内存。 - 获得JVM的支持
可重入锁毕竟是API级别的,后续的性能优化空间很小。synchronized则是JVM直接支持的,JVM能够在运行时作出相应的优化措施:锁粗化、锁消除、锁自旋等等。使得synchronized能够随着JDK版本的升级而不改动代码的前提下获得性能上的提升。
- 减少内存开销
-
HashTable也是线程安全的,为什么不推荐使用HashTable呢
HashTable容器使用synchronized来保证线程安全,但在线程竞争激烈的情况下HashTable的效率非常低下。因为多个线程访问HashTable的同步方法时,可能会进入阻塞或轮询状态。如线程1使用put进行添加元素,线程2不但不能使用put方法添加元素,并且也不能使用get方法来获取元素,所以竞争越激烈效率越低。
4、Map性能比较
类 | get | put | remove |
---|---|---|---|
HashMap | >=O(1) | O(1)~O(log n) | O(1)~O(log n) |
LinkedHashMap | >=O(1) | O(1)~O(log n) | O(1)~O(log n) |
TreeMap | O(log n) | O(log n) | O(log n) |
ThreadLocalMap | O(1)~O(n) | O(1)~O(n) | O(1)~O(n) |
HashTable | >=O(1) | O(1)~O(n) | O(1)~O(n) |
ConcurrentHashMap(1.7) | >=O(1) | O(1)~O(log n) | O(1)~O(log n) |
ConcurrentHashMap(1.8) | >=O(1) | O(1)~O(log n) | O(1)~O(log n) |
ConcurrentSkipListMap | O(log n) | O(log n) | O(log n) |