Skip to content

Commit eb92aec

Browse files
committed
添加内容
1 parent 94d35be commit eb92aec

11 files changed

+432
-0
lines changed

notes/HashMap的前世今生.md

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
@[toc]
2+
> 我们都知道HashMap是线程不安全的,在多线程的环境下不建议使用它。那么,它到底是哪里线程不安全呢?
3+
* HashMap根据键的HashCode值存储数据,大多数情况下可以直接定位到它的值,因此具有很快的访问速度,但是遍历顺序确实不确定的。HashMap最多允许一条记录的键为null,允许多条记录的值为null。HashMap非线程安全,即任一时刻可以有多个线程同时写HashMa,可能会导致数据的不一致。下面通过JDK1.7和JDK1.8分别来说一下HashMap的内部结构和底层原理。
4+
# JDK1.7的HashMap
5+
* JDK1.7HashMap的结构:数组+链表
6+
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200418123611363.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwNzIyODI3,size_16,color_FFFFFF,t_70#pic_center)
7+
* HashMap 里面是一个数组,然后数组中每个元素是一个单向链表。上图中,每个绿色的实体是嵌套类(内部类) Entry 的实例,Entry 包含四个属性:key, value, hash 值和用于单向链表的 next。
8+
> 1. capacity:当前数组容量,始终保持 2^n,可以扩容,扩容后数组大小为当前的 2 倍。
9+
> 2. loadFactor:负载因子,默认为 0.75。
10+
> 3. threshold:扩容的阈值,等于 capacity * loadFactor
11+
## 不安全原因之一:死循环
12+
> 死循环发生在HashMap的扩容函数中,根源在transfer函数中,jdk1.7中HashMap的transfer函数如下:
13+
14+
```java
15+
void transfer(Entry[] newTable, boolean rehash) {
16+
//newCapacity 新数组的容量
17+
int newCapacity = newTable.length;
18+
for (Entry<K,V> e : table) {
19+
while(null != e) {
20+
Entry<K,V> next = e.next;
21+
if (rehash) {
22+
e.hash = null == e.key ? 0 : hash(e.key);
23+
}
24+
int i = indexFor(e.hash, newCapacity);
25+
//头插法
26+
e.next = newTable[i];
27+
newTable[i] = e;
28+
e = next;
29+
}
30+
}
31+
}
32+
```
33+
* 该函数主要作用:对table进行扩容到newTable后,需要将原来数据转移到newTable中,可以看出在转移元素的过程中,当发生hash碰撞时,使用的是头插法,也就是链表的顺序会翻转,这里也是形成死循环的关键点。
34+
* JDK1.7还存在安全问题,如果有一组相同hash的数存入HashMap,那么HashMap就会退化为一个链表。而且黑客可以利用这个问题,进行DOS注入,造成性能问题。
35+
# JDK1.8的HashMap
36+
> 在 Java8 中,当链表中的元素超过了 8 个以后,并且数组的长度超过64时,会将链表转换为红黑树,在这些位置进行查找的时候可以降低时间复杂度为 O(logN)。
37+
38+
* JDK1.8HashMap的结构:数组+链表+红黑树
39+
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200418125423603.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwNzIyODI3,size_16,color_FFFFFF,t_70#pic_center)
40+
* JDK1.7 节点名字由Entry改为了Node,但是属性没改变。
41+
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200418130433911.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwNzIyODI3,size_16,color_FFFFFF,t_70#pic_center)
42+
## 不安全原因之一:数据覆盖
43+
* 在jdk1.8中对HashMap进行了优化,在发生hash碰撞,不再采用头插法方式,而是直接插入链表尾部(尾插法),因此不会出现环形链表的情况,但是在多线程的情况下仍然不安全。
44+
![在这里插入图片描述](https://img-blog.csdnimg.cn/20200418131902465.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3FxXzQwNzIyODI3,size_16,color_FFFFFF,t_70#pic_center)
45+
* putVal中的两行代码:
46+
```java
47+
if ((p = tab[i = (n - 1) & hash]) == null) // 如果没有hash碰撞则直接插入元素
48+
tab[i] = newNode(hash, key, value, null);
49+
```
50+
* 如果线程A和线程B同时进行put操作,刚好这两条不同的数据hash值一样,并且该位置数据为null,所以这线程A、B都会进入上述的代码中。假设一种情况,线程A进入后还未进行数据插入时挂起,而线程B正常执行,从而正常插入数据,然后线程A获取CPU时间片,此时线程A不用再进行hash判断了,问题出现:线程A会把线程B插入的数据给覆盖,发生线程不安全。
51+
# 总结
52+
* HashMap在JDK1.7中会出现死循环的问题,线程不安全
53+
* HashMap在JDK1.8中会出现值覆盖问题,线程同样不安全
54+
* HashMap的数字大小只能为2^n^ ,当初始化时赋的值不是 2^n^,则HashMap内部帮你调整为最接近初始值的下一个2^n^的数。
55+
* HashMap默认初始容量为16,负载因子为0.75.
56+
* HashMap发生hash碰撞时,JDK1.7采用头插法插入数据,JDK1.8改为了尾插法。
57+
* HashMap最多允许一条记录的键为null,允许多条记录的值为null。
58+
59+
如果需要线程满足安全,可以使用`HashTable`或者`Collections的synchronizedMap`方法使HashMap具有线程安全能力,也可以使用`ConcurrentHashMap` 。更多时候,为了并发性能,我们选择使用`ConcurrentHashMap`
60+
61+
**你知道的越多,你不知道的越多。
62+
有道无术,术尚可求,有术无道,止于术。
63+
如有其它问题,欢迎大家留言,我们一起讨论,一起学习,一起进步**
64+
65+

notes/Java基础-目录.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
* [JVM从入门到精通](/notes/JVM从入门到精通.md)
66
* [JVM大厂高频面试题](/notes/JVM大厂高频面试题.md)
77
* [Java反射机制](/notes/Java反射机制.md)
8+
* [Java面试必备知识查缺补漏(一)](/notes/Java面试必备知识查缺补漏(一).md)
89
# 💡 总结
910
- 你知道的越多,你不知道的越多。
1011

notes/Java线程-目录.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
* [Java多线程总结](/notes/Java多线程总结.md)
33
* [多线程安全问题](/notes/多线程安全问题.md)
44
* [多线程下的并发包](/notes/多线程下的并发包.md)
5+
* [锁升级](/notes/锁升级.md)
56
# 💡 总结
67
- 你知道的越多,你不知道的越多。

notes/Java集合-目录.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# ☕️ Java集合
22

33
* [Java集合总结](/notes/Java集合总结.md)
4+
* [HashMap的前世今生](/notes/HashMap的前世今生.md)
45

56
# 💡 总结
67

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
@[toc]
2+
> 写再前面,希望大家在看下面的具体内容之前,对照目录依次询问自己是否清楚这个问题。看完之后,在对照我写的查看相关知识点。本人水平有限,有些地方写的可能不够全面或者有异议。欢迎各位大佬们指出来,给我一个改正的机会、学习的机会,让我们一起进步,谢谢。
3+
----------------------
4+
### 线程池的常用的参数和含义
5+
* corePoolSize:核心线程数
6+
* maxinumPoolSize:最大线程数
7+
* keepAliveTime:线程不执行任务时保持存活的时间
8+
* TimeUnit:keepAliveTime的时间单位
9+
* ThreadFactory:线程工厂
10+
### 线程池的拒绝策略
11+
* `AbortPolicy`:线程池的默认策略。使用该策略时,如果线程池队列满了丢掉这个任务并且抛出RejectedExecutionException异常。
12+
* `DiscardPolicy`:如果线程池队列满了,会直接丢掉这个任务并且不会有任何异常。
13+
* `DiscardOldestPolicy`:丢弃最老的。也就是说如果队列满了,会将最早进入队列的任务删掉腾出空间,再尝试加入队列。
14+
* `CallerRunsPolicy`:使用此策略,如果添加到线程池失败,那么主线程会自己去执行该任务,不会等待线程池中的线程去执行。
15+
* `自定义执行策略`:如果以上策略都不符合业务场景,那么可以自己定义一个拒绝策略,只要实现RejectedExecutionHandler接口,并且实现rejectedExecution方法就可以了。
16+
### Java线程停止的几种方法和对比
17+
* `volatile + 标志位`:使用退出标志,使线程正常退出,也就是当 run() 方法完成后线程中止。定义一个用volatile修饰的成员变量来控制线程的停止,这点是利用了volatile修饰的成员变量可以在多线程之间达到共享,也就是可见性来实现的。
18+
* `stop()`:过期了的API,不建议使用的,因为stop()在结束一个线程时并不会保证线程的资源正常释放,会导致程序可能会出现一些不确定的状态。
19+
* `interrupt()`:当其他线程调用当前线程的interrupt方法时,即设置了一个标识,表示当前线程可以中断了,至于什么时候中断,取决于当前线程。
20+
21+
> **为什么弃用stop?**
22+
> 调用 stop() 方法会立刻停止 run() 方法中剩余的全部工作,包括在 catch 或 finally 语句中的,并抛出ThreadDeath异常(通常情况下此异常不需要显示的捕获),因此可能会导致一些清理性的工作的得不到完成,如文件,数据库等的关闭。
23+
> 调用 stop() 方法会立即释放该线程所持有的所有的锁,导致数据得不到同步,出现数据不一致的问题。
24+
### ThreadLocal原理
25+
> ThreadLocal提供一个线程(Thread)局部变量,访问到某个变量的每一个线程都拥有自己的局部变量。简单来说,ThreadLocal就是想在多线程环境下去保证成员变量的安全。
26+
* **ThreadLocal的实现原理**:在每个线程中维护一个Map,键是ThreadLocal类型,值是Object类型。当想获取ThreadLocal的值时,就从当前线程中拿出Map,然后在把ThreadLocal本身作为键从Map中拿出值返回。
27+
### ThreadLocal和Synchonized区别
28+
> ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的差别。
29+
* synchronized是利用锁的机制,使变量或代码块在某一时该仅仅能被一个线程访问。而ThreadLocal为每个线程都提供了变量的副本,使得每个线程在某一时间访问到的并非同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时可以获得数据共享。
30+
31+
**Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。**
32+
### 局部变量存在线程安全问题吗
33+
* `不存在`**原因****局部变量放在栈帧里,栈帧是线程私有的**。局部变量的作用域是方法内部,局部变量和方法的生命周期一样(同生共死),一个变量如果想跨越方法的边界,就必须创建在堆里。`每个线程都有自己独立的调用栈`,局部变量保存在线程各自的调用栈里面,不会共享,没有共享就没有安全问题。
34+
### 公平锁和非公平锁
35+
36+
> 简单的来说,如果一个线程组里,能保证每个线程都能拿到锁,那么这个锁就是`公平锁`。相反,如果保证不了每个线程都能拿到锁,也就是存在有线程饿死,那么这个锁就是`非公平锁`
37+
* 公平锁中,如果有另一个线程持有锁或者有其他线程在等待队列中等待这个所,那么新发出的请求的线程将被放入到队列中。
38+
* 非公平锁中,只有当锁被某个线程持有时,新发出请求的线程才会被放入队列中(此时和公平锁是一样的)。当前的锁状态没有被占用时,当前线程可以直接占用,而不需要判断当前队列中是否有等待线程。
39+
* `公平锁和非公平锁的差别在于非公平锁会有更多的机会去抢占锁。`
40+
### ReentrantLock和synchronized区别
41+
* 两者加锁方式都是`阻塞式的同步`,也就是如果一个线程获得了对象锁,进入了同步块,其他访问该同步块的线程都必须阻塞在同步块外面等待,而进行线程阻塞和唤醒的代价是比较高的(操作系统需要在用户态与内核态之间来回切换,代价很高,不过可以通过对锁优化进行改善)。
42+
* synchronized是java语言的关键字,需要jvm实现。而ReentrantLock它是JDK 1.5之后提供的Java API层面的互斥锁,需要lock()和unlock()方法配合try/finally语句块来完成。
43+
* 在synchronized优化以前,synchronized的性能是比ReenTrantLock差很多的,但是自从synchronized引入了[锁升级](https://blog.csdn.net/qq_40722827/article/details/105598682)后,两者的性能就差不多了。
44+
* ReentrantLock默认是非公平锁,可通过**构造传参**(true)改为公平锁。而synchronized是公平锁。
45+
46+
**你知道的越多,你不知道的越多。
47+
有道无术,术尚可求,有术无道,止于术。
48+
如有其它问题,欢迎大家留言,我们一起讨论,一起学习,一起进步**
49+
50+

notes/MySQL-目录.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# 💾 MySQL
22
* [MySQL高级性能优化总结](/notes/MySQL高级性能优化总结.md)
33
* [MySQL性能分析神器Explain](/notes/MySQL性能分析神器Explain.md)
4+
* [Spring事务和数据库事务联系](/notes/Spring事务和数据库事务联系.md)
45
# 💡 总结
56
- 你知道的越多,你不知道的越多。

notes/Spring-目录.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
1+
## 📝 Spring
2+
3+
* [Spring事务和数据库事务联系](/notes/Spring事务和数据库事务联系.md)
4+
5+
## 💡 总结
6+
7+
- 你知道的越多,你不知道的越多。
18

0 commit comments

Comments
 (0)