保证线程安全的几种方式
# JAVA集合基础
![](https://csdnimg.cn/release/blogv2/dist/pc/img/original.png)
[吃猫的大鱼](https://blog.csdn.net/qq_32979219) ![](https://csdnimg.cn/release/blogv2/dist/pc/img/newUpTime2.png) 已于 2022-01-29 15:18:30 修改 ![](https://csdnimg.cn/release/blogv2/dist/pc/img/articleReadEyes2.png) 120 ![](https://csdnimg.cn/release/blogv2/dist/pc/img/tobarCollect2.png) ![](https://csdnimg.cn/release/blogv2/dist/pc/img/tobarCollectionActive2.png) 收藏
分类专栏: [集合](https://blog.csdn.net/qq_32979219/category_11041859.html) [hashMap](https://blog.csdn.net/qq_32979219/category_11041861.html) [面试](https://blog.csdn.net/qq_32979219/category_11068351.html) 文章标签: [java](https://so.csdn.net/so/search/s.do?q=java&t=blog&o=vip&s=&l=&f=&viparticle=) [hashmap](https://so.csdn.net/so/search/s.do?q=hashmap&t=blog&o=vip&s=&l=&f=&viparticle=) [数据结构](https://so.csdn.net/so/search/s.do?q=%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84&t=blog&o=vip&s=&l=&f=&viparticle=)
于 2021-05-07 23:20:43 首次发布
版权声明:本文为博主原创文章,遵循 [CC 4.0 BY-SA](http://creativecommons.org/licenses/by-sa/4.0/) 版权协议,转载请附上原文出处链接和本声明。
本文链接:[https://blog.csdn.net/qq\_32979219/article/details/116432407](https://blog.csdn.net/qq_32979219/article/details/116432407)
版权
[![](https://img-blog.csdnimg.cn/20201014180756724.png?x-oss-process=image/resize,m_fixed,h_64,w_64) 集合 同时被 3 个专栏收录![](https://csdnimg.cn/release/blogv2/dist/pc/img/newArrowDown1White.png)](https://blog.csdn.net/qq_32979219/category_11041859.html "集合")
1 篇文章 0 订阅
订阅专栏
[![](https://img-blog.csdnimg.cn/20201014180756928.png?x-oss-process=image/resize,m_fixed,h_64,w_64) hashMap](https://blog.csdn.net/qq_32979219/category_11041861.html "hashMap")
1 篇文章 0 订阅
订阅专栏
[![](https://img-blog.csdnimg.cn/20201014180756927.png?x-oss-process=image/resize,m_fixed,h_64,w_64) 面试](https://blog.csdn.net/qq_32979219/category_11068351.html "面试")
25 篇文章 0 订阅
订阅专栏
# 前言
最近复习了一下数据结构,对数据结构有了更深了解,回头再来看一下集合相关知识就感觉豁然开朗,面试中集合也是必考题,便有了这篇集合总结,其中HashMap(包括部分源码分析)篇幅大概有6000+字,希望大家能耐心看完,看完后多少都会有一些收获。
# 数据结构
先来简单复习一下集合相关的数据结构。
一、数据结构的分类:
1.数据结构包括:逻辑结构和物理结构,其中逻辑结构稍复杂一些;
2.逻辑结构包括:线性结构(如 顺序表、栈、队列)和非线性结构(如 树、图);
3.物理结构包括:顺序存储结构(如 数组)、链式存储结构(如 链表)。
二、集合数据结构:
1\. 数组:采用一段连续的存储单元来存储数据。对于指定下标的查找,时间复杂度为O(1);通过给定值进行查找,需要遍历数组,逐一比对给定关键字和数组元素,时间复杂度为O(n);
2.链表:对于链表的新增,删除等操作,仅需处理结点间的引用即可,时间复杂度为O(1),而查找操作需要遍历链表逐一进行比对,复杂度为O(n);
3.二叉树:对一棵相对平衡的有序二叉树,对其进行插入,查找,删除等操作,平均复杂度均为O(logn);
4.哈希表:在哈希表中进行添加,删除,查找等操作,性能十分之高,不考虑哈希冲突的情况下,仅需一次定位即可完成,时间复杂度为O(1)。
数据结构本篇不做详细说明,后续会有专题写相关的数据结构及算法。
# 集合框架图
Java 集合,也称作容器就是用来存放数据的,主要是由两大接口 派生出来的Collection 和 Map,具体请看下图:
![](https://img-blog.csdnimg.cn/2021050523123525.jpeg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMyOTc5MjE5,size_16,color_FFFFFF,t_70#pic_center)
# Collection
collection主要存储单值类型的数据,主要的子接口有 list、set和queue(队列本篇不做介绍)。
操作集合,无非就是「增删改查」四大类,也叫 CRUD,这里对集合API就不做介绍了。
## List
主要特点是 元素有序且元素可重复。
### 面试题:ArrayList、LinkList和Vector的区别?
ArrayList 底层是数组,查询效率是O(1) ,新增和删除效率是O(n),查询效率高,线程不安全;
LinkList 底层是双向链表,查询效率是O(n),新增和删除效率是O(1),新增和删除效率高,线程不安全;
Vector 底层是数组结构,属于线程安全集合,使用频率较低。
## Set
主要特点是 元素无序且元素不可重复。
### 面试题:HashSet和TreeSet的区别?
HashSet :
1\. 数据结构 底层是HashMap实现;
2\. 顺序性 不能保证元素的排列顺;
3\. null元素 元素可以为null,但只能存放一个null元素;
4\. 时间复杂度 add(),remove(),contains()方法的时间复杂度是O(1)。
TreeSet:
1\. 数据结构 底层是treeMap(红黑树)实现;
2\. 顺序性 元素是自动排好序的;
3\. null元素 不能存放null元素;
4\. 时间复杂度 add(),remove(),contains()方法的时间复杂度是O(logn)。
# Map
主要存放键值对数据,本篇会重点介绍面试率极高的hashmap。
## HashMap
JDK1.7 Hashmap由数组和链表组成,JDK1.8做了主要做了2处有优化: 1. 数据结构变成了数组、链表和红黑树,解决是链表太长查询效率低问题;2. 在哈希冲突时头插法变成了尾插法,解决是在hashmap扩容时形成环形链表问题。
以下篇幅介绍的都是JDK1.8的hashmap。
```java
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
final K key;
V value;
Node<K,V> next;
...
}
```
### HashMap数据结构
JDK 1.8 hashMap的数据结构图:
![在这里插入图片描述](https://img-blog.csdnimg.cn/20210507232829981.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMyOTc5MjE5,size_16,color_FFFFFF,t_70#pic_center)
### 扩容条件
先看一下hashMap源码中几个关键的字段:
```java
/**实际存储的key-value键值对的个数*/
transient int size;
/**阈值,当table == {}时,该值为初始容量(初始容量默认为16);当table被填充了,也就是为table分配内存空间后,
threshold一般为 capacity*loadFactory。HashMap在进行扩容时需要参考threshold,后面会详细谈到*/
int threshold;
/**负载因子,代表了table的填充度有多少,默认是0.75
加载因子存在的原因,还是因为减缓哈希冲突,如果初始桶为16,等到满16个元素才扩容,某些桶里可能就有不止一个元素了。
所以加载因子默认为0.75,也就是说大小为16的HashMap,到了第13个元素,就会扩容成32。
*/
final float loadFactor;
/**HashMap被改变的次数,由于HashMap非线程安全,在对HashMap进行迭代时,
如果期间其他线程的参与导致HashMap的结构发生变化了(比如put,remove等操作),
需要抛出异常ConcurrentModificationException*/
transient int modCount;
```
hashMap扩容条件:
1. 当集合容量超过了阈值(threshold)就会进行扩容;
2. 当链表长度>=8且数组长度小于64时,也会扩容。
```java
/**
* Replaces all linked nodes in bin at index for given hash unless
* table is too small, in which case resizes instead.
* 新增数据时链表长度大于8时会进行树化
*/
final void treeifyBin(Node<K,V>[] tab, int hash) {
int n, index; Node<K,V> e;
//树化时 判断 数组为空 或 数组长度 < MIN_TREEIFY_CAPACITY =64 则直接扩容
if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
resize();
else if ((e = tab[index = (n - 1) & hash]) != null) {
TreeNode<K,V> hd = null, tl = null;
do {
TreeNode<K,V> p = replacementTreeNode(e, null);
if (tl == null)
hd = p;
else {
p.prev = tl;
tl.next = p;
}
tl = p;
} while ((e = e.next) != null);
if ((tab[index] = hd) != null)
hd.treeify(tab);
}
}
```
### 扩容机制
1. 扩容:创建一个新的Entry空数组,长度是原数组的2倍;
```java
//hashmap扩容方法
final Node<K,V>[] resize() {
Node<K,V>[] oldTab = table;
//原数组不为空 则原数组容量为 table.length
int oldCap = (oldTab == null) ? 0 : oldTab.length;
//原数组阈值
int oldThr = threshold;
//新数组 容量和阈值 都默认为 0
int newCap, newThr = 0;
//原数组容量大于0
if (oldCap > 0) {
//原数组容量 大于等于 最大容量 MAXIMUM_CAPACITY = 1 << 30 则直接返回
if (oldCap >= MAXIMUM_CAPACITY) {
threshold = Integer.MAX_VALUE;
return oldTab;
}
//设置新容量为旧容量的两倍
else if ((newCap = oldCap << 1) < MAXIMUM_CAPACITY &&
oldCap >= DEFAULT_INITIAL_CAPACITY)
//阈值也变为原来的两倍
newThr = oldThr << 1; // double threshold
}
...
```
2. 计算hash:index = HashCode(Key) & (Length - 1);
3. ReHash: 由于新数组的长度变了,则需要遍历原Entry数组,重新计算hash把所有的Entry重新Hash到新数组。
小结: 可见在用hashMap时最好先计算好hashMap的容量,初始化带上容量大小,毕竟扩容是非常消耗性能的。
### 树化条件
树化必须同时满足2个条件:
1. 链表长度>=8 (binCount >=7 因为binCount 是从0开始算) ;
```java
//binCount 从0开始自增
for (int binCount = 0; ; ++binCount) {
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
//当 binCount == 7时 则树化 并跳出循环
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
p = e;
}
```
2. 数组长度>=64(具体看以上treeifyBin源码分析);
### 数据查询
```java
/**
*
* @param hash 需要被获取元素的hash值
* @param key 需要被获取的元素
* @return 返回被需要到的元素,没有获取到则返回null
*/
final HashMap.Node<K, V> getNode(int hash, Object key) {
//临时变量储存table数组
HashMap.Node[] tab;
//临时变量获取第一个元素
HashMap.Node first;
//n为table的长度
int n;
// first = tab[n - 1 & hash]) 计算数组下标 获取到数组上第一个node 且 node的key就是要查询key 则直接返回 node数组
if ((tab = this.table) != null && (n = tab.length) > 0 && (first = tab[n - 1 & hash]) != null) {
Object k;
//first元素存在,切first元素即锁需要查找的元素,直接返回first.
if (first.hash == hash && ((k = first.key) == key || key != null && key.equals(k))) {
return first;
}
//数组上的node不是查询目标且指向下一个node不为空
HashMap.Node e;
if ((e = first.next) != null) {
//判断node是否为红黑树
if (first instanceof HashMap.TreeNode) {
//按照红黑树方式去遍历
return ((HashMap.TreeNode)first).getTreeNode(hash, key);
}
//如果链表没有被树化,则使用链表的方式查询.
do {
//循环判断当前的临时变量e是否与所需元素相同
if (e.hash == hash && ((k = e.key) == key || key != null && key.equals(k))) {
return e;
}
} while((e = e.next) != null);
}
}
return null;
}
```
通过分析源码查询过程可以分为3部分:
1. 计算hashCode 获取到数组上的node,且node.key==key则数组上的就是就是查询目标;
2. 是链表结构则按照链表方式遍历查询目标;
3. 是红黑树结构则按照红黑树方式遍历查询目标。
### 数据存储
先上源码:
```java
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
Node<K,V>[] tab; Node<K,V> p; int n, i;
// 如果存储元素的table为空,则进行必要字段的初始化 默认为16
if ((tab = table) == null || (n = tab.length) == 0)
n = (tab = resize()).length;
// 如果根据hash值获取的node为空,则直接新增
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = newNode(hash, key, value, null);
else {
Node<K,V> e; K k;
// 如果新插入的结点和table中p结点的hash值,key值相同的话
if (p.hash == hash &&
((k = p.key) == key || (key != null && key.equals(k))))
e = p;
// 如果是红黑树结点的话,进行红黑树插入
else if (p instanceof TreeNode)
e = ((TreeNode<K,V>)p).putTreeVal(this, tab, hash, key, value);
else {
//如果不是红黑树则为链表按照链表方式插入 binCount从0开始自增
for (int binCount = 0; ; ++binCount) {
// 代表这个单链表只有一个头部结点,则直接新建一个结点即可
if ((e = p.next) == null) {
p.next = newNode(hash, key, value, null);
// 当binCount>=7 即 链表长度>=8时,将链表转红黑树
if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for 1st
treeifyBin(tab, hash);
break;
}
if (e.hash == hash &&
((k = e.key) == key || (key != null && key.equals(k))))
break;
// 及时更新p
p = e;
}
}
// 如果存在这个映射就覆盖
if (e != null) { // existing mapping for key
V oldValue = e.value;
// 判断是否允许覆盖,并且value是否为空
if (!onlyIfAbsent || oldValue == null)
e.value = value;
// 回调以允许LinkedHashMap后置操作
afterNodeAccess(e);
return oldValue;
}
}
++modCount;
// 如果容量超过阈值则进行扩容
if (++size > threshold)
resize();
// 回调以允许LinkedHashMap后置操作
afterNodeInsertion(evict);
return null;
}
```
从以上源码分析hashMap数据存储也可简单分为4部分:
1. 在数组上计算hashCode的node为空则直接新增;
2. 获取的node不为空,如果是红黑树就按照红黑树方式插入;
3. 获取的node不为空,如果是链表则按照链表方式插入(会触发树化或扩容操作);
4. 新增成功后,还需要判断数组容量是否有超过阈值,超过则需要扩容。
## 面试题:HashMap负载因子是多少?为什是这么多?
默认为0.75,这个是在时间和空间之间平衡的一个数值,负载因子是可以自定义的(不推荐)
## 面试题:HashMap和HashTable区别
1. 初始化容量 HashMap初始容量为16,HashTable初始容量为11(负载因子都相同);
2. 线程安全 HashMap为非线程安全,HashTable为线程安全(get和put方法都用synchronized修饰,效率较差);
3. 扩容容量 HashMap扩容时容量:capacity_2,HashTable扩容时容量:capacity_2+1
4. 遍历方式 HashMap仅支持Iterator的遍历方式,Hashtable支持Iterator和Enumeration两种遍历方式;
5. null值存储 HashMap中key和value都允许为null,HashTable在遇到null时,会抛出NullPointerException异常。
ps:一般问到hashMap就一定会问到currentHashMap,由于篇幅较长了下次会单独写一篇currentHashMap。
# 总结思维导图
![在这里插入图片描述](https://img-blog.csdnimg.cn/2021050722505317.jpg?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzMyOTc5MjE5,size_16,color_FFFFFF,t_70#pic_center)
最后奉上自己总结集合的思维导图,希望能帮助大家。
既然都看到最后了请帮忙一键三连哦!