Skip to content

Commit 1f795ed

Browse files
committed
update
1 parent c11e505 commit 1f795ed

File tree

392 files changed

+7369
-646
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

392 files changed

+7369
-646
lines changed
Loading
Loading

java/0-JavaSE/c-集合类/3-HashMap.md

+165-56
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,12 @@
22

33
> https://zhuanlan.zhihu.com/p/45430524
44
5+
当创建HashMap集合对象的时候:
6+
7+
- 在JDK8 前,构造方法中创建一个一个长度是16的`Entry[] table`用来存储键值对数据的
8+
- 在JDK8后,<u>不是在HashMap的构造方法底层创建数组了</u>,是在**第一次调用**`put`方法时创建的数组,
9+
`Node[] table`用来存储键值对数据的。
10+
511
HashMap底层的实现采用了哈希表,哈希表的实现:
612

713
- JDK8之 前:数组 + 单向链表
@@ -40,7 +46,68 @@ HashMap内部节点
4046

4147
我们接着来探讨。
4248

43-
## 2. 存储数据
49+
## 2. 添加put
50+
51+
**put过程**
52+
53+
![put](3-HashMap.assets/put.jpeg)
54+
55+
**实现步骤大致如下:**
56+
57+
1. 先通过`hash`值计算出**key**映射到哪个桶;
58+
2. 如果桶上没有碰撞冲突,则直接插入;
59+
3. 如果出现碰撞冲突,则需要处理冲突:
60+
- 如果该桶使用红黑树处理冲突,则调用红黑树的方法插入数据;
61+
- 否则采用传统的链式方法插入。如果链的长度达到临界值,则把链转变为红黑树;
62+
4. 如果桶中存在重复的键,则为该键替换新值`value`
63+
5. 如果`size`大于國值`threshold`,则进行扩容;
64+
65+
`put`方法:
66+
67+
```java
68+
public V put(K key, V value) {
69+
return putVal(hash(key), key, value, false, true);
70+
}
71+
```
72+
73+
74+
75+
76+
77+
78+
79+
<hr>
80+
81+
### Hash算法
82+
83+
**【提问】:HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?**
84+
85+
- hashCode产生的hash值太大,与数组长度不匹配
86+
87+
- HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行`^`运算,<u>降低哈希碰撞概率使得数据分布更平均</u>;
88+
89+
**【面试提问】:哈希表层釆用何种算法计算hash值?还有哪些算法可以计算出hash值?**
90+
91+
底层采用的**key**的hashCode方法的值,结合数组长度进行无符号右移`>>>16`、按位异或`^`、按位与`&`,计算出索引。
92+
93+
```java
94+
static final int hash(Object key) {
95+
int h;
96+
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
97+
}
98+
```
99+
100+
![image-20200822120845788](3-HashMap.assets/image-20200822120845788.png)
101+
102+
还可以采用:
103+
104+
- 平方取中法
105+
- 取余数
106+
- 伪随机数法
107+
108+
----------------
109+
110+
### 存入过程
44111

45112
我们的目的是将`key-value`两个对象**成对**存放到 HashMap的Node数组中。**核心就是产生hash值,该值用来对应数组的存储位置**
46113

@@ -105,20 +172,63 @@ hash值 = hashCode &(数组长度 - 1);
105172
**第四步:将Node对象放到table数组中**
106173

107174
- 如果本Node对象对应的数组索引位置还没有放Node对象,则直接将Node对象存储进数组;
108-
109175
- 如果对应索引位置已经有Node对象,则将已有Node对象的next指向本Node对象,形成链表
110176

111-
**总结:**
177+
-------------
178+
179+
### 链表插入元素
180+
181+
插入:如果`key`相同,`value`会<u>覆盖</u>为最新
182+
183+
**JDK7**
184+
185+
- 数组 + 链表
186+
187+
- 头插法
188+
189+
【头插法的原因】
190+
191+
1. 头插法相对于尾插法,遍历链表的长度平均来说较短,每次不一定全部遍历
192+
2. 根据时间局部性原理,最近插入的最有可能被使用
193+
194+
**JDK8**
195+
196+
- 数组 + 链表 / 红黑树
197+
- 尾插法
198+
199+
------------------
200+
201+
### 解决hash碰撞
112202

113203
当添加一个元素`key-value`时,首先计算`key`的hash值,以此确定插入数组中的位置。但是可能存在同hash值的元素已经被放在数组同一位置了,这时就添加到同一hash值的元素的后面,他们在数组的同一位置,就形成了链表。
114204

115205
同一个链表上的hash值是相同的,所以说数组存放的是链表。
116206

117-
JDK8及之后,当链表长度大于8时,链表就转换为红黑树,这样又大大提高了查找的效率
207+
JDK8及之后,当**链表长度大于8时,并且数组长度大于64**(二者必须同时满足)链表就转换为红黑树,这样又大大提高了查找的效率
208+
209+
链表长度小于`6`时,会再次退化为链表
118210

119211
![](3-HashMap.assets/20200524222943.png)
120212

121-
-----------------------
213+
查询复杂度:
214+
215+
- 链表`O(n)`
216+
- 红黑树`O(log n)`
217+
218+
-----------------
219+
220+
### 转化的阈值为什么为8
221+
222+
根据统计学**Poisson-泊松分布**规律,一个桶中链表长度**大于**8个元素的概率为`0.0000006`,几乎为不可能事件。**权衡空间和事件选择阈值为8**
223+
224+
**【自己的理解】**
225+
226+
红黑树的平均查找长度是`log(n)`
227+
228+
- 如果长度为8,平均查找长度为`log(8)=3`;链表的平均查找长度为`n / 2`,当长度为8时,平均查找长度为`8/2=4`,这才有转换成树的必要;
229+
- 链表长度如果是小于等于6,`6/2=3`,而`log(6)=2.6`,虽然速度也很快的,但是转化为树结构和生成树的时间并不会太短
230+
231+
<br>
122232

123233
## 3.取出数据
124234

@@ -152,7 +262,7 @@ get(1); //test
152262

153263
不管是存储数据还是取出数据,都必须先计算hash值,根据哈希值来确定数据在Node[]数组上存放/取出的位置
154264

155-
## 4. 扩容问题
265+
## 4. 扩容resize
156266

157267
**扩容的目的:** 在达到阈值时,扩容是为了缩短链表/红黑树,散开hash,提高查询效率
158268

@@ -196,41 +306,61 @@ HashMap 总是使用**2^n^**作为哈希表的大小,`tableSizeFor`方法保
196306

197307
```java
198308
18 - 1 == 17
199-
17 0001 0001
200-
201-
>>1 0000 1000
202-
| 0001 1001
203-
>>2 0000 0110
204-
| 0001 1111
205-
>>4 0000 0001
206-
| 0001 1111
207-
>>8 0000 0000
208-
| 0001 1111
209-
>>16 0000 0000
210-
| 0001 1111
211-
309+
减一是防止2的幂次方数扩容,传入16时变为32
310+
=========================================
311+
17 0001 0001
312+
>>>1 0000 1000
313+
| 0001 1001
314+
>>>2 0000 0110
315+
| 0001 1111
316+
>>>4 0000 0001
317+
| 0001 1111
318+
>>>8 0000 0000
319+
| 0001 1111
320+
>>>16 0000 0000
321+
| 0001 1111
322+
323+
===========================================
212324
i - (i >> 1)
213-
0001 1111
214-
>>1 0000 1111
215-
-- 0001 0000
325+
0001 1111
326+
>>>1 0000 1111
327+
-- 0001 0000
216328

217-
0001 0000 ------ 16
329+
[原始数二进制保留最高位,其余全变为0]
218330

331+
0001 0000 ------ 16
332+
===========================================
219333
【最终】
220-
(18 - 1) * 2 == 34 ---- 32
334+
(18 - 1) * 2 == 34 ---> 32
221335
```
222336

223337
扩容很耗时,扩容的本质是定义新的更大的数组,并将原数组内容拷贝到新数组中
224338

225-
#### 为什么是2的幂次方
339+
### 为什么是2的幂次方
340+
341+
- 只有当数组长度为2的幂次方时,`hash & (length-1)`才等价于`hash % length`,而`%`运算效率太低,使用位运算来实现key的定位
342+
- 2的幂次方可以减少`hash`冲突次数,提高HashMap的查询效率;
343+
344+
> 如果数组长度不是2的n次幂,容易发生hash碰撞,导致其余数组空间很大程度上并没有存储数据,链表或者红黑树过长,效率降低
226345
227-
1、HashMap为什么不直接使用hashCode()处理后的哈希值直接作为table的下标?
346+
### 加载因子loadFactor
228347

229-
HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己的哈希值高低位自行进行异或运算,降低哈希碰撞概率也使得数据分布更平均;
348+
**loadFactor怎么计算**
230349

231-
2、为什么数组长度要保证为2的幂次方呢?
350+
- 存储的元素个数`size`
351+
- 桶数组的长度`capacity`
232352

233-
只有当数组长度为2的幂次方时,h&(length-1)才等价于h%length,即实现了key的定位,2的幂次方也可以减少冲突次数,提高HashMap的查询效率;
353+
```java
354+
loadFactor = size / capacity
355+
0.75 = 12 / 16
356+
```
357+
358+
**加载因子为什么为`0.75`?**
359+
360+
根据泊松分布统计学规律,**兼顾了**桶数组的利用率又避免的链表节点太多
361+
362+
- 大于`0.75`:链表挂载的节点太多
363+
- 小于`0.75`:数组使用率低
234364

235365
### 位桶数组2倍扩容
236366

@@ -242,7 +372,11 @@ HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己
242372

243373
![](3-HashMap.assets/20200812101302.png)
244374

245-
<font color = red>【注意】</font>扩容并不是直接将数组对应`hash`下的链表直接复制,而是逐个移动,*重新计算Hash值***扩容达到缩短链表,散开hash,提高查询效率**的效果
375+
<font color = red>【注意】:</font>
376+
377+
- 扩容并不是直接将数组对应`hash`下的链表直接复制,而是节点逐个移动,*重新计算Hash值*
378+
379+
- **扩容达到缩短链表,散开hash,提高查询效率**的效果
246380

247381

248382

@@ -252,32 +386,7 @@ HashMap自己实现了自己的hash()方法,通过两次扰动使得它自己
252386

253387
【原因】:
254388

255-
- JDK7扩容采用头插法,导致数据-移动到扩容后的数组顺序发生变化
256-
257-
258-
259-
## 5. 插入元素
260-
261-
插入:如果`key`相同,`value`会<u>覆盖</u>为最新
262-
263-
**JDK7**
264-
265-
- 数组 + 链表
266-
267-
- 头插法
268-
269-
【头插法的原因】
270-
271-
1. 头插法相对于尾插法,遍历链表的长度平均来说较短,每次不一定全部遍历
272-
2. 根据时间局部性原理,最近插入的最有可能被使用
273-
274-
**JDK8**
275-
276-
- 数组 + 链表 / 红黑树
277-
278-
- 尾插法
279-
280-
389+
- JDK7扩容采用头插法,导致数据-移动到扩容后的数组顺序发生变化
281390

282391
## 6. 并发问题
283392

java/0-JavaSE/d-多线程/JUC/4.Java内存模型JMM详解.md

+33-1
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,39 @@ A-32架构软件开发者手册对lock指令的解释:
319319

320320
## 5. volatile不保证原子性
321321

322-
### 不保证原子性验证
322+
### 原子性
323+
324+
**定义:**
325+
326+
原子具有不可分割性。比如 `i=1`,这个操作是不可分割的,那么我们说这个操作是原子操作。再比如:`i++`,这个操作实际是`i= i + 1`,包括读取`i``i+1`,将结果写入内存三个操作,它们翻译成底层的字节码指令可能需要很多条指令来完成,是可以分割的,所以他不是一个原子操作。
327+
328+
非原子操作都会存在线程安全问题,需要我们使用相关技术(比如sychronized)让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。
329+
330+
这句话的丰富含义有:
331+
332+
1. **原子操作是对于多线程而言的***,对于单一线程,无所谓原子性。
333+
334+
2. **原子操作是针对共享变量的**
335+
336+
3. **原子操作是不可分割的**。指访问某个共享变量的操作从其他任意线程来看是不可分割的。
337+
338+
### 保证原子性
339+
340+
保证多线程原子性的方式
341+
342+
**1. 加锁**
343+
344+
使用synchronized同步代码块保证线程的同步,从而保证多线程的原子性,但是加锁的话,就会使开销比较大。加锁和解锁是有消耗的。并且只要有加锁、解锁就会伴随着线程阻塞、线程唤醒,这样线程的切换也是消耗性能的。加锁本质上是将并发转变为串行来实现的,势必会影响吞吐量。
345+
346+
**2. CAS无锁算法**
347+
348+
CAS 是在不使用锁的情况下实现多线程之间的变量同步。
349+
350+
CAS包含 3 个参数:共享变量的原始值A、预期值B和新值 C。只有当A的值等于B,才能把A的值变成C。也就是说预期值B等于原始值A,说明共享变量没有被其他线程修改过,所以才允许更新新值,这样就保证了原子性!如果A不等于B,说明共享变量已经被其他线程修改过了,当前线程可以放弃此操作。
351+
352+
基于这样的算法,CAS算法即使没有锁,也可以发现其他线程对当前线程的干扰,并进行恰当的处理。
353+
354+
### volatile不保证原子性验证
323355

324356
还是通过代码来说明问题:
325357

java/0-JavaSE/d-多线程/JUC/5.Volatile.md

+4
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,10 @@ b. 这个写会操作会导致其他线程中的缓存无效。
8080

8181
对于类似`i++`这样的复合操作,要想保证原子性,只能借助于`synchronized``Lock`以及并发包下的`AtomicInteger`的原子操作类。AtomicInteger对基本数据类型的 自增(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证这些操作是原子性操作。
8282

83+
#### 怎样才能保证原子性?
84+
85+
86+
8387

8488

8589
### Q5:了解过JMM内存模型吗?简单的讲讲

0 commit comments

Comments
 (0)