1
0
mirror of https://github.com/Snailclimb/JavaGuide synced 2025-06-20 22:17:09 +08:00

集合部分面试题重构完善

This commit is contained in:
shuang.kou 2020-06-15 19:05:28 +08:00
parent 1d30b7e1de
commit 90f1744da0
5 changed files with 1348 additions and 143 deletions

View File

@ -145,7 +145,7 @@ ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
`ThreadLocalMap``ThreadLocal`的静态内部类。 `ThreadLocalMap``ThreadLocal`的静态内部类。
![ThreadLocal内部类](https://ws1.sinaimg.cn/large/006rNwoDgy1g2f47u9li2j30ka08cq43.jpg)
### ThreadLocal 内存泄露问题 ### ThreadLocal 内存泄露问题

View File

@ -0,0 +1,941 @@
### 前言
![Ym8V9H.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba8d77264d4?w=1579&h=1167&f=png&s=217526)
**全文共10000+字31张图这篇文章同样耗费了不少的时间和精力才创作完成原创不易请大家点点关注+在看,感谢。**
对于`ThreadLocal`,大家的第一反应可能是很简单呀,线程的变量副本,每个线程隔离。那这里有几个问题大家可以思考一下:
- **ThreadLocal**的key是**弱引用**,那么在 threadLocal.get()的时候,发生**GC之后**key是否**为null**
- **ThreadLocal**中**ThreadLocalMap**的**数据结构**
- **ThreadLocalMap**的**Hash算法**
- **ThreadLocalMap**中**Hash冲突**如何解决?
- **ThreadLocalMap**扩容机制?
- **ThreadLocalMap**中过期key的清理机制**探测式清理**和**启发式清理**流程?
- **ThreadLocalMap.set()**方法实现原理?
- **ThreadLocalMap.get()**方法实现原理?
- 项目中**ThreadLocal**使用情况?遇到的坑?
- ......
上述的一些问题你是否都已经掌握的很清楚了呢?本文将围绕这些问题使用图文方式来剖析`ThreadLocal`的**点点滴滴**。
### 目录
<!-- TOC -->
- [前言](#前言)
- [目录](#目录)
- [ThreadLocal代码演示](#threadlocal代码演示)
- [ThreadLocal的数据结构](#threadlocal的数据结构)
- [GC 之后key是否为null](#gc-之后key是否为null)
- [ThreadLocal.set()方法源码详解](#threadlocalset方法源码详解)
- [ThreadLocalMap Hash算法](#threadlocalmap-hash算法)
- [ThreadLocalMap Hash冲突](#threadlocalmap-hash冲突)
- [ThreadLocalMap.set()详解](#threadlocalmapset详解)
- [ThreadLocalMap.set()原理图解](#threadlocalmapset原理图解)
- [ThreadLocalMap.set()源码详解](#threadlocalmapset源码详解)
- [ThreadLocalMap过期key的探测式清理流程](#threadlocalmap过期key的探测式清理流程)
- [ThreadLocalMap扩容机制](#threadlocalmap扩容机制)
- [ThreadLocalMap.get()详解](#threadlocalmapget详解)
- [ThreadLocalMap.get()图解](#threadlocalmapget图解)
- [ThreadLocalMap.get()源码详解](#threadlocalmapget源码详解)
- [ThreadLocalMap过期key的启发式清理流程](#threadlocalmap过期key的启发式清理流程)
- [InheritableThreadLocal](#inheritablethreadlocal)
- [ThreadLocal项目中使用实战](#threadlocal项目中使用实战)
- [ThreadLocal使用场景](#threadlocal使用场景)
- [Feign远程调用解决方案](#feign远程调用解决方案)
- [线程池异步调用requestId传递](#线程池异步调用requestid传递)
- [使用MQ发送消息给第三方系统](#使用mq发送消息给第三方系统)
<!-- /TOC -->
**注明:** 本文源码基于`JDK 1.8`
### ThreadLocal代码演示
我们先看下`ThreadLocal`使用示例:
```java
public class ThreadLocalTest {
private List<String> messages = Lists.newArrayList();
public static final ThreadLocal<ThreadLocalTest> holder = ThreadLocal.withInitial(ThreadLocalTest::new);
public static void add(String message) {
holder.get().messages.add(message);
}
public static List<String> clear() {
List<String> messages = holder.get().messages;
holder.remove();
System.out.println("size: " + holder.get().messages.size());
return messages;
}
public static void main(String[] args) {
ThreadLocalTest.add("一枝花算不算浪漫");
System.out.println(holder.get().messages);
ThreadLocalTest.clear();
}
}
```
打印结果:
```java
[一枝花算不算浪漫]
size: 0
```
`ThreadLocal`对象可以提供线程局部变量,每个线程`Thread`拥有一份自己的**副本变量**,多个线程互不干扰。
### ThreadLocal的数据结构
![image.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba819625d64?w=878&h=551&f=png&s=35947)
`Thread`类有一个类型为`ThreadLocal.ThreadLocalMap`的实例变量`threadLocals`,也就是说每个线程有一个自己的`ThreadLocalMap`
`ThreadLocalMap`有自己的独立实现,可以简单地将它的`key`视作`ThreadLocal``value`为代码中放入的值(实际上`key`并不是`ThreadLocal`本身,而是它的一个**弱引用**)。
每个线程在往`ThreadLocal`里放值的时候,都会往自己的`ThreadLocalMap`里存,读也是以`ThreadLocal`作为引用,在自己的`map`里找对应的`key`,从而实现了**线程隔离**。
`ThreadLocalMap`有点类似`HashMap`的结构,只是`HashMap`是由**数组+链表**实现的,而`ThreadLocalMap`中并没有**链表**结构。
我们还要注意`Entry` 它的`key``ThreadLocal<?> k` ,继承自`WeakReference` 也就是我们常说的弱引用类型。
### GC 之后key是否为null
回应开头的那个问题, `ThreadLocal``key`是弱引用,那么在` threadLocal.get()`的时候,发生`GC`之后,`key`是否是`null`
为了搞清楚这个问题,我们需要搞清楚`Java`的**四种引用类型**
- **强引用**我们常常new出来的对象就是强引用类型只要强引用存在垃圾回收器将永远不会回收被引用的对象哪怕内存不足的时候
- **软引用**使用SoftReference修饰的对象被称为软引用软引用指向的对象在内存要溢出的时候被回收
- **弱引用**使用WeakReference修饰的对象被称为弱引用只要发生垃圾回收若这个对象只被弱引用指向那么就会被回收
- **虚引用**:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知
接着再来看下代码,我们使用反射的方式来看看`GC``ThreadLocal`中的数据情况:(下面代码来源自https://blog.csdn.net/thewindkee/article/details/103726942本地运行演示GC回收场景)
```java
public class ThreadLocalDemo {
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException {
Thread t = new Thread(()->test("abc",false));
t.start();
t.join();
System.out.println("--gc后--");
Thread t2 = new Thread(() -> test("def", true));
t2.start();
t2.join();
}
private static void test(String s,boolean isGC) {
try {
new ThreadLocal<>().set(s);
if (isGC) {
System.gc();
}
Thread t = Thread.currentThread();
Class<? extends Thread> clz = t.getClass();
Field field = clz.getDeclaredField("threadLocals");
field.setAccessible(true);
Object threadLocalMap = field.get(t);
Class<?> tlmClass = threadLocalMap.getClass();
Field tableField = tlmClass.getDeclaredField("table");
tableField.setAccessible(true);
Object[] arr = (Object[]) tableField.get(threadLocalMap);
for (Object o : arr) {
if (o != null) {
Class<?> entryClass = o.getClass();
Field valueField = entryClass.getDeclaredField("value");
Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
valueField.setAccessible(true);
referenceField.setAccessible(true);
System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o)));
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
```
结果如下:
```java
弱引用key:java.lang.ThreadLocal@433619b6,值:abc
弱引用key:java.lang.ThreadLocal@418a15e3,值:java.lang.ref.SoftReference@bf97a12
--gc后--
弱引用key:null,值:def
```
![image.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba81584d3ca?w=1568&h=561&f=png&s=137867)
如图所示,因为这里创建的`ThreadLocal`并没有指向任何值,也就是没有任何引用:
```java
new ThreadLocal<>().set(s);
```
所以这里在`GC`之后,`key`就会被回收,我们看到上面`debug`中的`referent=null`, 如果**改动一下代码:**
![image.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba815948bf9?w=1336&h=590&f=png&s=131688)
这个问题刚开始看,如果没有过多思考,**弱引用**,还有**垃圾回收**,那么肯定会觉得是`null`
其实是不对的,因为题目说的是在做 `threadlocal.get()` 操作,证明其实还是有**强引用**存在的,所以 `key` 并不为 `null`,如下图所示,`ThreadLocal`的**强引用**仍然是存在的。
![image.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba81bd745e6?w=1219&h=394&f=png&s=25434)
如果我们的**强引用**不存在的话,那么 `key` 就会被回收,也就是会出现我们 `value` 没被回收,`key` 被回收,导致 `value` 永远存在,出现内存泄漏。
### ThreadLocal.set()方法源码详解
![image.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba897dc6204?w=1284&h=448&f=png&s=51070)
`ThreadLocal`中的`set`方法原理如上图所示,很简单,主要是判断`ThreadLocalMap`是否存在,然后使用`ThreadLocal`中的`set`方法进行数据处理。
代码如下:
```java
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
```
主要的核心逻辑还是在`ThreadLocalMap`中的,一步步往下看,后面还有更详细的剖析。
### ThreadLocalMap Hash算法
既然是`Map`结构,那么`ThreadLocalMap`当然也要实现自己的`hash`算法来解决散列表数组冲突问题。
```java
int i = key.threadLocalHashCode & (len-1);
```
`ThreadLocalMap``hash`算法很简单,这里`i`就是当前key在散列表中对应的数组下标位置。
这里最关键的就是`threadLocalHashCode`值的计算,`ThreadLocal`中有一个属性为`HASH_INCREMENT = 0x61c88647`
```java
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
private static int nextHashCode() {
return nextHashCode.getAndAdd(HASH_INCREMENT);
}
static class ThreadLocalMap {
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
}
```
每当创建一个`ThreadLocal`对象,这个`ThreadLocal.nextHashCode` 这个值就会增长 `0x61c88647`
这个值很特殊,它是**斐波那契数** 也叫 **黄金分割数**`hash`增量为 这个数字,带来的好处就是 `hash` **分布非常均匀**
我们自己可以尝试下:
![YKbSGn.png](https://user-gold-cdn.xitu.io/2020/5/8/171f4d82ead289da?w=1743&h=887&f=png&s=113401)
可以看到产生的哈希码分布很均匀,这里不去细纠**斐波那契**具体算法,感兴趣的可以自行查阅相关资料。
### ThreadLocalMap Hash冲突
> **注明:** 下面所有示例图中,**绿色块**`Entry`代表**正常数据****灰色块**代表`Entry``key`值为`null`**已被垃圾回收**。**白色块**表示`Entry``null`
虽然`ThreadLocalMap`中使用了**黄金分隔数来**作为`hash`计算因子,大大减少了`Hash`冲突的概率,但是仍然会存在冲突。
`HashMap`中解决冲突的方法是在数组上构造一个**链表**结构,冲突的数据挂载到链表上,如果链表长度超过一定数量则会转化成**红黑树**。
`ThreadLocalMap`中并没有链表结构,所以这里不能适用`HashMap`解决冲突的方式了。
![Ynzr5D.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba8fc715e1b?w=1478&h=424&f=png&s=66966)
如上图所示,如果我们插入一个`value=27`的数据,通过`hash`计算后应该落入第4个槽位中而槽位4已经有了`Entry`数据。
此时就会线性向后查找,一直找到`Entry``null`的槽位才会停止查找,将当前元素放入此槽位中。当然迭代过程中还有其他的情况,比如遇到了`Entry`不为`null``key`值相等的情况,还有`Entry`中的`key`值为`null`的情况等等都会有不同的处理,后面会一一详细讲解。
这里还画了一个`Entry`中的`key``null`的数据(**Entry=2的灰色块数据**),因为`key`值是**弱引用**类型,所以会有这种数据存在。在`set`过程中,如果遇到了`key`过期的`Entry`数据,实际上是会进行一轮**探测式清理**操作的,具体操作方式后面会讲到。
### ThreadLocalMap.set()详解
#### ThreadLocalMap.set()原理图解
看完了`ThreadLocal` **hash算法**后,我们再来看`set`是如何实现的。
`ThreadLocalMap``set`数据(**新增**或者**更新**数据)分为好几种情况,针对不同的情况我们画图来说说明。
**第一种情况:** 通过`hash`计算后的槽位对应的`Entry`数据为空:
![YuSniD.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba95568266d?w=1494&h=363&f=png&s=47111)
这里直接将数据放到该槽位即可。
**第二种情况:** 槽位数据不为空,`key`值与当前`ThreadLocal`通过`hash`计算获取的`key`值一致:
![image.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba8ec7d6e78?w=1212&h=376&f=png&s=32578)
这里直接更新该槽位的数据。
**第三种情况:** 槽位数据不为空,往后遍历过程中,在找到`Entry``null`的槽位之前,没有遇到`key`过期的`Entry`
![image.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba902a2896a?w=1238&h=398&f=png&s=41278)
遍历散列数组,线性往后查找,如果找到`Entry``null`的槽位,则将数据放入该槽位中,或者往后遍历过程中,遇到了**key值相等**的数据,直接更新即可。
**第四种情况:** 槽位数据不为空,往后遍历过程中,在找到`Entry``null`的槽位之前,遇到`key`过期的`Entry`,如下图,往后遍历过程中,一到了`index=7`的槽位数据`Entry``key=null`
![Yu77qg.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba9509b36c1?w=1318&h=404&f=png&s=75663)
散列数组下标为7位置对应的`Entry`数据`key``null`,表明此数据`key`值已经被垃圾回收掉了,此时就会执行`replaceStaleEntry()`方法,该方法含义是**替换过期数据的逻辑**,以**index=7**位起点开始遍历,进行探测式数据清理工作。
初始化探测式清理过期数据扫描的开始位置:`slotToExpunge = staleSlot = 7`
以当前`staleSlot`开始 向前迭代查找,找其他过期的数据,然后更新过期数据起始扫描下标`slotToExpunge``for`循环迭代,直到碰到`Entry``null`结束。
如果找到了过期的数据,继续向前迭代,直到遇到`Entry=null`的槽位才停止迭代,如下图所示,**slotToExpunge被更新为0**
![YuHSMT.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba957014857?w=1347&h=470&f=png&s=100229)
以当前节点(`index=7`)向前迭代,检测是否有过期的`Entry`数据,如果有则更新`slotToExpunge`值。碰到`null`则结束探测。以上图为例`slotToExpunge`被更新为0。
上面向前迭代的操作是为了更新探测清理过期数据的起始下标`slotToExpunge`的值,这个值在后面会讲解,它是用来判断当前过期槽位`staleSlot`之前是否还有过期元素。
接着开始以`staleSlot`位置(index=7)向后迭代,**如果找到了相同key值的Entry数据**
![YuHEJ1.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba96c6b080d?w=1353&h=487&f=png&s=95173)
从当前节点`staleSlot`向后查找`key`值相等的`Entry`元素,找到后更新`Entry`的值并交换`staleSlot`元素的位置(`staleSlot`位置为过期元素),更新`Entry`数据,然后开始进行过期`Entry`的清理工作,如下图所示:
![Yu4oWT.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba9af057e1e?w=1336&h=361&f=png&s=63049)
**向后遍历过程中如果没有找到相同key值的Entry数据**
![YuHMee.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba9848c608b?w=1336&h=397&f=png&s=73680)
从当前节点`staleSlot`向后查找`key`值相等的`Entry`元素,直到`Entry``null`则停止寻找。通过上图可知,此时`table`中没有`key`值相同的`Entry`
创建新的`Entry`,替换`table[stableSlot]`位置:
![YuH3FA.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3ba9da434d2b?w=1341&h=398&f=png&s=61130)
替换完成后也是进行过期元素清理工作,清理工作主要是有两个方法:`expungeStaleEntry()``cleanSomeSlots()`,具体细节后面会讲到,请继续往后看。
#### ThreadLocalMap.set()源码详解
上面已经用图的方式解析了`set()`实现的原理,其实已经很清晰了,我们接着再看下源码:
`java.lang.ThreadLocal.ThreadLocalMap.set()`:
```java
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
for (Entry e = tab[i];
e != null;
e = tab[i = nextIndex(i, len)]) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
return;
}
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
```
这里会通过`key`来计算在散列表中的对应位置,然后以当前`key`对应的桶的位置向后查找,找到可以使用的桶。
```java
Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);
```
什么情况下桶才是可以使用的呢?
1. `k = key` 说明是替换操作,可以使用
2. 碰到一个过期的桶,执行替换逻辑,占用过期桶
3. 查找过程中,碰到桶中`Entry=null`的情况,直接使用
接着就是执行`for`循环遍历,向后查找,我们先看下`nextIndex()``prevIndex()`方法实现:
![YZSC5j.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3baa0b7231c8?w=861&h=389&f=png&s=32052)
```java
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
```
接着看剩下`for`循环中的逻辑:
1. 遍历当前`key`值对应的桶中`Entry`数据为空,这说明散列数组这里没有数据冲突,跳出`for`循环,直接`set`数据到对应的桶中
2. 如果`key`值对应的桶中`Entry`数据不为空
2.1 如果`k = key`,说明当前`set`操作是一个替换操作,做替换逻辑,直接返回
2.2 如果`key = null`,说明当前桶位置的`Entry`是过期数据,执行`replaceStaleEntry()`方法(核心方法),然后返回
3. `for`循环执行完毕,继续往下执行说明向后迭代的过程中遇到了`entry``null`的情况
3.1 在`Entry``null`的桶中创建一个新的`Entry`对象
3.2 执行`++size`操作
4. 调用`cleanSomeSlots()`做一次启发式清理工作,清理散列数组中`Entry``key`过期的数据
4.1 如果清理工作完成后,未清理到任何数据,且`size`超过了阈值(数组长度的2/3),进行`rehash()`操作
4.2 `rehash()`中会先进行一轮探测式清理,清理过期`key`,清理完成后如果**size >= threshold - threshold / 4**,就会执行真正的扩容逻辑(扩容逻辑往后看)
接着重点看下`replaceStaleEntry()`方法,`replaceStaleEntry()`方法提供替换过期数据的功能,我们可以对应上面**第四种情况**的原理图来再回顾下,具体代码如下:
`java.lang.ThreadLocal.ThreadLocalMap.replaceStaleEntry()`:
```java
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
```
`slotToExpunge`表示开始探测式清理过期数据的开始下标,默认从当前的`staleSlot`开始。以当前的`staleSlot`开始,向前迭代查找,找到没有过期的数据,`for`循环一直碰到`Entry``null`才会结束。如果向前找到了过期数据更新探测清理过期数据的开始下标为i`slotToExpunge=i`
```java
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len)){
if (e.get() == null){
slotToExpunge = i;
}
}
```
接着开始从`staleSlot`向后查找,也是碰到`Entry``null`的桶结束。
如果迭代过程中,**碰到k == key**,这说明这里是替换逻辑,替换新数据并且交换当前`staleSlot`位置。如果`slotToExpunge == staleSlot`,这说明`replaceStaleEntry()`一开始向前查找过期数据时并未找到过期的`Entry`数据接着向后查找过程中也未发现过期数据修改开始探测式清理过期数据的下标为当前循环的index`slotToExpunge = i`。最后调用`cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);`进行启发式过期数据清理。
```java
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
```
`cleanSomeSlots()``expungeStaleEntry()`方法后面都会细讲,这两个是和清理相关的方法,一个是过期`key`相关`Entry`的启发式清理(`Heuristically scan`),另一个是过期`key`相关`Entry`的探测式清理。
**如果k != key**则会接着往下走,`k == null`说明当前遍历的`Entry`是一个过期数据,`slotToExpunge == staleSlot`说明,一开始的向前查找数据并未找到过期的`Entry`。如果条件成立,则更新`slotToExpunge` 为当前位置,这个前提是前驱节点扫描时未发现过期数据。
```java
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
```
往后迭代的过程中如果没有找到`k == key`的数据,且碰到`Entry``null`的数据,则结束当前的迭代操作。此时说明这里是一个添加的逻辑,将新的数据添加到`table[staleSlot]` 对应的`slot`中。
```java
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
```
最后判断除了`staleSlot`以外,还发现了其他过期的`slot`数据,就要开启清理数据的逻辑:
```java
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
```
### ThreadLocalMap过期key的探测式清理流程
上面我们有提及`ThreadLocalMap`的两种过期`key`数据清理方式:**探测式清理**和**启发式清理**。
我们先讲下探测式清理,也就是`expungeStaleEntry`方法,遍历散列数组,从开始位置向后探测清理过期数据,将过期数据的`Entry`设置为`null`,沿途中碰到未过期的数据则将此数据`rehash`后重新在`table`数组中定位,如果定位的位置已经有了数据,则会将未过期的数据放到最靠近此位置的`Entry=null`的桶中,使`rehash`后的`Entry`数据距离正确的桶的位置更近一些。操作逻辑如下:
![YuH2OU.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3baa1285c833?w=1373&h=324&f=png&s=73657)
如上图,`set(27)` 经过hash计算后应该落到`index=4`的桶中,由于`index=4`桶已经有了数据,所以往后迭代最终数据放入到`index=7`的桶中,放入后一段时间后`index=5`中的`Entry`数据`key`变为了`null`
![YuHb6K.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3baa2cc322cb?w=1351&h=384&f=png&s=96329)
如果再有其他数据`set``map`中,就会触发**探测式清理**操作。
如上图,执行**探测式清理**后,`index=5`的数据被清理掉,继续往后迭代,到`index=7`的元素时,经过`rehash`后发现该元素正确的`index=4`,而此位置已经已经有了数据,往后查找离`index=4`最近的`Entry=null`的节点(刚被探测式清理掉的数据index=5),找到后移动`index= 7`的数据到`index=5`中,此时桶的位置离正确的位置`index=4`更近了。
经过一轮探测式清理后,`key`过期的数据会被清理掉,没过期的数据经过`rehash`重定位后所处的桶位置理论上更接近`i= key.hashCode & (tab.len - 1)`的位置。这种优化会提高整个散列表查询性能。
接着看下`expungeStaleEntry()`具体流程,我们还是以先原理图后源码讲解的方式来一步步梳理:
![Yuf301.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3baa2a52731c?w=1113&h=377&f=png&s=49974)
我们假设`expungeStaleEntry(3)` 来调用此方法,如上图所示,我们可以看到`ThreadLocalMap``table`的数据情况,接着执行清理操作:
![YufupF.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3baa72c52453?w=908&h=231&f=png&s=29340)
第一步是清空当前`staleSlot`位置的数据,`index=3`位置的`Entry`变成了`null`。然后接着往后探测:
![YufAwq.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3baa8b341c89?w=1256&h=396&f=png&s=95675)
执行完第二步后index=4的元素挪到index=3的槽位中。
继续往后迭代检查,碰到正常数据,计算该数据位置是否偏移,如果被偏移,则重新计算`slot`位置,目的是让正常数据尽可能存放在正确位置或离正确位置更近的位置
![YuWjTP.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3baa9503bd8d?w=1029&h=287&f=png&s=32999)
在往后迭代的过程中碰到空的槽位,终止探测,这样一轮探测式清理工作就完成了,接着我们继续看看具体**实现源代码**
```java
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
```
这里我们还是以`staleSlot=3` 来做示例说明,首先是将`tab[staleSlot]`槽位的数据清空,然后设置`size--`
接着以`staleSlot`位置往后迭代,如果遇到`k==null`的过期数据,也是清空该槽位数据,然后`size--`
```java
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
}
```
如果`key`没有过期,重新计算当前`key`的下标位置是不是当前槽位下标位置,如果不是,那么说明产生了`hash`冲突,此时以新计算出来正确的槽位位置往后迭代,找到最近一个可以存放`entry`的位置。
```java
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
```
这里是处理正常的产生`Hash`冲突的数据,经过迭代后,有过`Hash`冲突数据的`Entry`位置会更靠近正确位置,这样的话,查询的时候 效率才会更高。
### ThreadLocalMap扩容机制
`ThreadLocalMap.set()`方法的最后,如果执行完启发式清理工作后,未清理到任何数据,且当前散列数组中`Entry`的数量已经达到了列表的扩容阈值`(len*2/3)`,就开始执行`rehash()`逻辑:
```java
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
```
接着看下`rehash()`具体实现:
```java
private void rehash() {
expungeStaleEntries();
if (size >= threshold - threshold / 4)
resize();
}
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
for (int j = 0; j < len; j++) {
Entry e = tab[j];
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}
```
这里首先是会进行探测式清理工作,从`table`的起始位置往后清理,上面有分析清理的详细流程。清理完成之后,`table`中可能有一些`key``null``Entry`数据被清理掉,所以此时通过判断`size >= threshold - threshold / 4` 也就是`size >= threshold* 3/4` 来决定是否扩容。
我们还记得上面进行`rehash()`的阈值是`size >= threshold`,所以当面试官套路我们`ThreadLocalMap`扩容机制的时候 我们一定要说清楚这两个步骤:
![YuqwPs.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3baaa9f7fb5f?w=1483&h=511&f=png&s=76905)
接着看看具体的`resize()`方法,为了方便演示,我们以`oldTab.len=8`来举例:
![Yu2QOI.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3baad1dec348?w=1438&h=366&f=png&s=70262)
扩容后的`tab`的大小为`oldLen * 2`,然后遍历老的散列表,重新计算`hash`位置,然后放到新的`tab`数组中,如果出现`hash`冲突则往后寻找最近的`entry``null`的槽位,遍历完成之后,`oldTab`中所有的`entry`数据都已经放入到新的`tab`中了。重新计算`tab`下次扩容的**阈值**,具体代码如下:
```java
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
```
### ThreadLocalMap.get()详解
上面已经看完了`set()`方法的源码,其中包括`set`数据、清理数据、优化数据桶的位置等操作,接着看看`get()`操作的原理。
#### ThreadLocalMap.get()图解
**第一种情况:** 通过查找`key`值计算出散列表中`slot`位置,然后该`slot`位置中的`Entry.key`和查找的`key`一致,则直接返回:
![YuWfdx.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3baaed5b6b60?w=1342&h=404&f=png&s=55569)
**第二种情况:** `slot`位置中的`Entry.key`和要查找的`key`不一致:
![YuWyz4.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3bab054f8f50?w=1288&h=423&f=png&s=69512)
我们以`get(ThreadLocal1)`为例,通过`hash`计算后,正确的`slot`位置应该是4`index=4`的槽位已经有了数据,且`key`值不等于`ThreadLocal1`,所以需要继续往后迭代查找。
迭代到`index=5`的数据时,此时`Entry.key=null`,触发一次探测式数据回收操作,执行`expungeStaleEntry()`方法,执行完后,`index 5,8`的数据都会被回收,而`index 6,7`的数据都会前移,此时继续往后迭代,到`index = 6`的时候即找到了`key`值相等的`Entry`数据,如下图所示:
![YuW8JS.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3bab26c905a5?w=1443&h=421&f=png&s=91983)
#### ThreadLocalMap.get()源码详解
`java.lang.ThreadLocal.ThreadLocalMap.getEntry()`:
```java
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
```
### ThreadLocalMap过期key的启发式清理流程
上面多次提及到`ThreadLocalMap`过期可以的两种清理方式:**探测式清理(expungeStaleEntry())**、**启发式清理(cleanSomeSlots())**
探测式清理是以当前`Entry` 往后清理,遇到值为`null`则结束清理,属于**线性探测清理**。
而启发式清理被作者定义为:**Heuristically scan some cells looking for stale entries**.
![YK5HJ0.png](https://user-gold-cdn.xitu.io/2020/5/8/171f49d18669ff50?w=1434&h=924&f=png&s=90010)
具体代码如下:
```java
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
```
### InheritableThreadLocal
我们使用`ThreadLocal`的时候,在异步场景下是无法给子线程共享父线程中创建的线程副本数据的。
为了解决这个问题JDK中还有一个`InheritableThreadLocal`类,我们来看一个例子:
```java
public class InheritableThreadLocalDemo {
public static void main(String[] args) {
ThreadLocal<String> threadLocal = new ThreadLocal<>();
ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
threadLocal.set("父类数据:threadLocal");
inheritableThreadLocal.set("父类数据:inheritableThreadLocal");
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("子线程获取父类threadLocal数据" + threadLocal.get());
System.out.println("子线程获取父类inheritableThreadLocal数据" + inheritableThreadLocal.get());
}
}).start();
}
}
```
打印结果:
```java
子线程获取父类threadLocal数据null
子线程获取父类inheritableThreadLocal数据父类数据:inheritableThreadLocal
```
实现原理是子线程是通过在父线程中通过调用`new Thread()`方法来创建子线程,`Thread#init`方法在`Thread`的构造方法中被调用。在`init`方法中拷贝父线程数据到子线程中:
```java
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
this.stackSize = stackSize;
tid = nextThreadID();
}
```
`InheritableThreadLocal`仍然有缺陷,一般我们做异步化处理都是使用的线程池,而`InheritableThreadLocal`是在`new Thread`中的`init()`方法给赋值的,而线程池是线程复用的逻辑,所以这里会存在问题。
当然,有问题出现就会有解决问题的方案,阿里巴巴开源了一个`TransmittableThreadLocal`组件就可以解决这个问题,这里就不再延伸,感兴趣的可自行查阅资料。
### ThreadLocal项目中使用实战
#### ThreadLocal使用场景
我们现在项目中日志记录用的是`ELK+Logstash`,最后在`Kibana`中进行展示和检索。
现在都是分布式系统统一对外提供服务项目间调用的关系可以通过traceId来关联但是不同项目之间如何传递`traceId`呢?
这里我们使用`org.slf4j.MDC`来实现此功能,内部就是通过`ThreadLocal`来实现的,具体实现如下:
当前端发送请求到**服务A**时,**服务A**会生成一个类似`UUID``traceId`字符串,将此字符串放入当前线程的`ThreadLocal`中,在调用**服务B**的时候,将`traceId`写入到请求的`Header`中,**服务B**在接收请求时会先判断请求的`Header`中是否有`traceId`,如果存在则写入自己线程的`ThreadLocal`中。
![YeMO3t.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3bb27f41cec0?w=1657&h=554&f=png&s=65612)
图中的`requestId`即为我们各个系统链路关联的`traceId`,系统间互相调用,通过这个`requestId`即可找到对应链路,这里还有会有一些其他场景:
![Ym3861.png](https://user-gold-cdn.xitu.io/2020/5/8/171f3bb290c4d1d1?w=965&h=469&f=png&s=29983)
针对于这些场景,我们都可以有相应的解决方案,如下所示
#### Feign远程调用解决方案
**服务发送请求:**
```java
@Component
@Slf4j
public class FeignInvokeInterceptor implements RequestInterceptor {
@Override
public void apply(RequestTemplate template) {
String requestId = MDC.get("requestId");
if (StringUtils.isNotBlank(requestId)) {
template.header("requestId", requestId);
}
}
}
```
**服务接收请求:**
```java
@Slf4j
@Component
public class LogInterceptor extends HandlerInterceptorAdapter {
@Override
public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3) {
MDC.remove("requestId");
}
@Override
public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3) {
}
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestId = request.getHeader(BaseConstant.REQUEST_ID_KEY);
if (StringUtils.isBlank(requestId)) {
requestId = UUID.randomUUID().toString().replace("-", "");
}
MDC.put("requestId", requestId);
return true;
}
}
```
#### 线程池异步调用requestId传递
因为`MDC`是基于`ThreadLocal`去实现的,异步过程中,子线程并没有办法获取到父线程`ThreadLocal`存储的数据,所以这里可以自定义线程池执行器,修改其中的`run()`方法:
```java
public class MyThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {
@Override
public void execute(Runnable runnable) {
Map<String, String> context = MDC.getCopyOfContextMap();
super.execute(() -> run(runnable, context));
}
@Override
private void run(Runnable runnable, Map<String, String> context) {
if (context != null) {
MDC.setContextMap(context);
}
try {
runnable.run();
} finally {
MDC.remove();
}
}
}
```
#### 使用MQ发送消息给第三方系统
在MQ发送的消息体中自定义属性`requestId`,接收方消费消息后,自己解析`requestId`使用即可。
[1]:https://juejin.im/post/5eacc1c75188256d976df748

View File

@ -1,97 +1,182 @@
点击关注[公众号](#公众号)及时获取笔主最新更新文章并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。 点击关注[公众号](#公众号)及时获取笔主最新更新文章并可免费领取本文档配套的《Java 面试突击》以及 Java 工程师必备学习资源。
<!-- TOC --> <!-- TOC -->
* [1.1 集合概述](#) - [剖析面试最常见问题之 Java 集合框架](#剖析面试最常见问题之-java-集合框架)
* [1.1.1 说说List,Set,Map三者的区别](#ListSetMap) - [集合概述](#集合概述)
* [1.1.2 集合框架底层数据结构总结](#-1) - [Java 集合概览](#java-集合概览)
* [Collection](#Collection) - [说说 List,Set,Map 三者的区别?](#说说-listsetmap-三者的区别)
* [Map](#Map) - [集合框架底层数据结构总结](#集合框架底层数据结构总结)
* [1.1.3 如何选用集合?](#-1) - [List](#list)
* [1.1.4 为什么要使用集合?](#-1) - [Set](#set)
* [1.2 Iterator迭代器接口](#Iterator) - [Map](#map)
* [1.2.1 为什么要使用迭代器](#1.2.1为什么要使用迭代器) - [如何选用集合?](#如何选用集合)
* [1.3 Collection子接口之List](#CollectionList) - [为什么要使用集合?](#为什么要使用集合)
* [ 1.3.1 Arraylist 与 LinkedList 区别?](#ArraylistLinkedList) - [Iterator 迭代器](#iterator-迭代器)
* [1.3.2 说一说 ArrayList 的扩容机制吧](#ArrayList) - [迭代器 Iterator 是什么?](#迭代器-iterator-是什么)
* [1.4 Collection子接口之Set](#CollectionSet) - [迭代器 Iterator 有啥用?](#迭代器-iterator-有啥用)
* [1.4.1 comparable 和 Comparator的区别](#comparableComparator) - [如何使用?](#如何使用)
* [Comparator定制排序](#Comparator) - [有哪些集合是线程不安全的?怎么解决呢?](#有哪些集合是线程不安全的怎么解决呢)
* [重写compareTo方法实现按年龄来排序](#compareTo) - [Collection 子接口之 List](#collection-子接口之-list)
* [1.4.2 无序性和不可重复性的含义是什么](#1.4.2无序性和不可重复性的含义是什么) - [Arraylist 和 Vector 的区别?](#arraylist-和-vector-的区别)
* [1.4.3 比较HashSet 、LinkedHashSet和TreeSet三者的异同 ](#1.4.3比较HashSet、LinkedHashSet和TreeSet三者的异同 ) - [Arraylist 与 LinkedList 区别?](#arraylist-与-linkedlist-区别)
* [1.5 Map接口](#Map-1) - [补充内容:双向链表和双向循环链表](#补充内容双向链表和双向循环链表)
* [1.5.1 HashMap 和 Hashtable 的区别](#HashMapHashtable) - [补充内容:RandomAccess 接口](#补充内容randomaccess-接口)
* [1.5.2 HashMap 和 HashSet区别](#HashMapHashSet) - [说一说 ArrayList 的扩容机制吧](#说一说-arraylist-的扩容机制吧)
* [1.5.3 HashSet如何检查重复](#HashSet) - [Collection 子接口之 Set](#collection-子接口之-set)
* [1.5.4 HashMap的底层实现](#HashMap) - [comparable 和 Comparator 的区别](#comparable-和-comparator-的区别)
* [JDK1.8之前](#JDK1.8) - [Comparator 定制排序](#comparator-定制排序)
* [JDK1.8之后](#JDK1.8-1) - [重写 compareTo 方法实现按年龄来排序](#重写-compareto-方法实现按年龄来排序)
* [1.5.5 HashMap 的长度为什么是2的幂次方](#HashMap2) - [无序性和不可重复性的含义是什么](#无序性和不可重复性的含义是什么)
* [ 1.5.6 HashMap 多线程操作导致死循环问题](#HashMap-1) - [比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同](#比较-hashsetlinkedhashset-和-treeset-三者的异同)
* [1.5.7 ConcurrentHashMap 和 Hashtable 的区别](#ConcurrentHashMapHashtable) - [Map 接口](#map-接口)
* [1.5.8 ConcurrentHashMap线程安全的具体实现方式/底层具体实现](#ConcurrentHashMap) - [HashMap 和 Hashtable 的区别](#hashmap-和-hashtable-的区别)
* [JDK1.7(上面有示意图)](#JDK1.7) - [HashMap 和 HashSet 区别](#hashmap-和-hashset-区别)
* [JDK1.8 (上面有示意图)](#JDK1.8-1) - [HashMap 和 TreeMap 区别](#hashmap-和-treemap-区别)
* [1.6 Collections工具类](#Collections) - [HashSet 如何检查重复](#hashset-如何检查重复)
* [公众号](#-1) - [HashMap 的底层实现](#hashmap-的底层实现)
- [JDK1.8 之前](#jdk18-之前)
- [JDK1.8 之后](#jdk18-之后)
- [HashMap 的长度为什么是 2 的幂次方](#hashmap-的长度为什么是-2-的幂次方)
- [HashMap 多线程操作导致死循环问题](#hashmap-多线程操作导致死循环问题)
- [HashMap 有哪几种常见的遍历方式?](#hashmap-有哪几种常见的遍历方式)
- [ConcurrentHashMap 和 Hashtable 的区别](#concurrenthashmap-和-hashtable-的区别)
- [ConcurrentHashMap 线程安全的具体实现方式/底层具体实现](#concurrenthashmap-线程安全的具体实现方式底层具体实现)
- [JDK1.7(上面有示意图)](#jdk17上面有示意图)
- [JDK1.8 (上面有示意图)](#jdk18-上面有示意图)
- [Collections 工具类](#collections-工具类)
- [排序操作](#排序操作)
- [查找,替换操作](#查找替换操作)
- [同步控制](#同步控制)
- [其他重要问题](#其他重要问题)
- [什么是快速失败(fail-fast)](#什么是快速失败fail-fast)
- [什么是安全失败(fail-safe)呢?](#什么是安全失败fail-safe呢)
- [公众号](#公众号)
<!-- /TOC --> <!-- /TOC -->
# 剖析面试最常见问题之 Java 集合框架 # 剖析面试最常见问题之 Java 集合框架
## 1.1 集合概述 ## 集合概述
### 1.1.1 说说List,Set,Map三者的区别
- **List(对付顺序的好帮手)** 存储的元素是有序的、可重复的。 ### Java 集合概览
- **Set(注重独一无二的性质):** 存储的元素是无序的、不可重复的。
- **Map(用Key来搜索的专家):** 使用键值对kye-value存储类似于数学上的函数y=f(x)“x”代表key"y"代表valueKey是无序的、不可重复的value是无序的、可重复的每个键最多映射到一个值。
### 1.1.2 集合框架底层数据结构总结 从下图可以看出,在 Java 中除了以 Map 结尾的类之外, 其他类都实现了 `Collection` 接口。
#### Collection 并且,以 `Map` 结尾的类都实现了 `Map` 接口。
##### List ![](./images/Java-Collections.jpeg)
- **Arraylist** Object[]数组 ### 说说 List,Set,Map 三者的区别?
- **Vector** Object[]数组
- **LinkedList** 双向链表(JDK1.6之前为循环链表JDK1.7取消了循环)
##### Set - `List`(对付顺序的好帮手) 存储的元素是有序的、可重复的。
- `Set`(注重独一无二的性质): 存储的元素是无序的、不可重复的。
- `Map`(用 Key 来搜索的专家): 使用键值对kye-value存储类似于数学上的函数 y=f(x)“x”代表 key"y"代表 valueKey 是无序的、不可重复的value 是无序的、可重复的,每个键最多映射到一个值。
- **HashSet无序唯一:** 基于 HashMap 实现的,底层采用 HashMap 来保存元素 ### 集合框架底层数据结构总结
- **LinkedHashSet** LinkedHashSet 是 HashSet的子类并且其内部是通过 LinkedHashMap 来实现的。有点类似于我们之前说的LinkedHashMap 其内部是基于 HashMap 实现一样,不过还是有一点点区别的
- **TreeSet有序唯一** 红黑树(自平衡的排序二叉树) 先来看一下 `Collection` 接口下面的集合。
#### List
- `Arraylist` `Object[]`数组
- `Vector``Object[]`数组
- `LinkedList` 双向链表(JDK1.6 之前为循环链表JDK1.7 取消了循环)
#### Set
- `HashSet`(无序,唯一): 基于 `HashMap` 实现的,底层采用 `HashMap` 来保存元素
- `LinkedHashSet``LinkedHashSet``HashSet` 的子类,并且其内部是通过 `LinkedHashMap` 来实现的。有点类似于我们之前说的 `LinkedHashMap` 其内部是基于 `HashMap` 实现一样,不过还是有一点点区别的
- `TreeSet`(有序,唯一): 红黑树(自平衡的排序二叉树)
再来看看 `Map` 接口下面的集合。
#### Map #### Map
- **HashMap** JDK1.8之前HashMap由数组+链表组成的数组是HashMap的主体链表则是主要为了解决哈希冲突而存在的“拉链法”解决冲突。JDK1.8以后在解决哈希冲突时有了较大的变化当链表长度大于阈值默认为8将链表转换成红黑树前会判断如果当前数组的长度小于 64那么会选择先进行数组扩容而不是转换为红黑树将链表转化为红黑树以减少搜索时间 - `HashMap` JDK1.8 之前 HashMap 由数组+链表组成的,数组是 HashMap 的主体链表则是主要为了解决哈希冲突而存在的“拉链法”解决冲突。JDK1.8 以后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8将链表转换成红黑树前会判断如果当前数组的长度小于 64那么会选择先进行数组扩容而不是转换为红黑树将链表转化为红黑树以减少搜索时间
- **LinkedHashMap** LinkedHashMap 继承自 HashMap所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外LinkedHashMap 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:[《LinkedHashMap 源码详细分析JDK1.8)》](https://www.imooc.com/article/22931) - `LinkedHashMap` `LinkedHashMap` 继承自 `HashMap`,所以它的底层仍然是基于拉链式散列结构即由数组和链表或红黑树组成。另外,`LinkedHashMap` 在上面结构的基础上,增加了一条双向链表,使得上面的结构可以保持键值对的插入顺序。同时通过对链表进行相应的操作,实现了访问顺序相关逻辑。详细可以查看:[《LinkedHashMap 源码详细分析JDK1.8)》](https://www.imooc.com/article/22931)
- **Hashtable** 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的 - `Hashtable` 数组+链表组成的,数组是 HashMap 的主体,链表则是主要为了解决哈希冲突而存在的
- **TreeMap** 红黑树(自平衡的排序二叉树) - `TreeMap` 红黑树(自平衡的排序二叉树)
### 1.1.3如何选用集合? ### 如何选用集合?
主要根据集合的特点来选用比如我们需要根据键值获取到元素值时就选用Map接口下的集合需要排序时选择TreeMap,不需要排序时就选择HashMap,需要保证线程安全就选用ConcurrentHashMap.当我们只需要存放元素值时就选择实现Collection接口的集合需要保证元素唯一时选择实现Set接口的集合比如TreeSet或HashSet不需要就选择实现List接口的比如ArrayList或LinkedList然后再根据实现这些接口的集合的特点来选用。 主要根据集合的特点来选用,比如我们需要根据键值获取到元素值时就选用 Map 接口下的集合,需要排序时选择 TreeMap,不需要排序时就选择 HashMap,需要保证线程安全就选用 ConcurrentHashMap。
当我们只需要存放元素值时,就选择实现 Collection 接口的集合,需要保证元素唯一时选择实现 Set 接口的集合比如 TreeSet 或 HashSet不需要就选择实现 List 接口的比如 ArrayList 或 LinkedList然后再根据实现这些接口的集合的特点来选用。
### 为什么要使用集合?
### 1.1.4 为什么要使用集合?
当我们需要保存一组类型相同的数据的时候,我们应该是用一个容器来保存,这个容器就是数组,但是,使用数组存储对象具有一定的弊端, 当我们需要保存一组类型相同的数据的时候,我们应该是用一个容器来保存,这个容器就是数组,但是,使用数组存储对象具有一定的弊端,
因为我们在实际开发中,存储的数据的类型是多种多样的,于是,就出现了“集合”,集合同样也是用来存储多个数据的。 因为我们在实际开发中,存储的数据的类型是多种多样的,于是,就出现了“集合”,集合同样也是用来存储多个数据的。
数组的缺点是一旦声明之后,长度就不可变了;同时,声明数组时的数据类型也决定了该数组存储的数据的类型;而且,数组存储的数据是有序的、可重复的,特点单一。 数组的缺点是一旦声明之后,长度就不可变了;同时,声明数组时的数据类型也决定了该数组存储的数据的类型;而且,数组存储的数据是有序的、可重复的,特点单一。
但是集合提高了数据存储的灵活性Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据 但是集合提高了数据存储的灵活性Java 集合不仅可以用来存储不同类型不同数量的对象,还可以保存具有映射关系的数据
## 1.2 Iterator迭代器接口 ### Iterator 迭代器
### 1.2.1 为什么要使用迭代器
Iterator对象称为迭代器设计模式的一种迭代器可以对集合进行遍历但每一个集合内部的数据结构可能是不尽相同的所以每一个集合存和取都很可能是不一样的虽然我们可以人为地在每一个类中定义 hasNext() 和 next() 方法,但这样做会让整个集合体系过于臃肿。于是就有了迭代器。
迭代器是将这样的方法抽取出接口然后在每个类的内部定义自己迭代方式这样做就规定了整个集合体系的遍历方式都是hasNext()和next()方法,使用者不用管怎么实现的,会用即可。迭代器的定义为:提供一种方法访问一个容器对象中各个元素,而又不需要暴露该对象的内部细节。 #### 迭代器 Iterator 是什么?
## 1.3 Collection子接口之List ```java
### 1.3.1 Arraylist、LinkedList与Vector的区别? public interface Iterator<E> {
//集合中是否还有元素
boolean hasNext();
//获得集合中的下一个元素
E next();
......
}
```
- **1. ArrayList是List的主要实现类底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全 `Iterator` 对象称为迭代器(设计模式的一种),迭代器可以对集合进行遍历,但每一个集合内部的数据结构可能是不尽相同的,所以每一个集合存和取都很可能是不一样的,虽然我们可以人为地在每一个类中定义 `hasNext()``next()` 方法,但这样做会让整个集合体系过于臃肿。于是就有了迭代器。
- **2. LinkedList是底层使用双向链表存储适合频繁的增、删操作线程不安全
- **3. Vector是List的古老实现类底层使用 Object[ ]存储,线程安全的。 迭代器是将这样的方法抽取出接口,然后在每个类的内部,定义自己迭代方式,这样做就规定了整个集合体系的遍历方式都是 `hasNext()``next()`方法,使用者不用管怎么实现的,会用即可。迭代器的定义为:提供一种方法访问一个容器对象中各个元素,而又不需要暴露该对象的内部细节。
#### 迭代器 Iterator 有啥用?
`Iterator` 主要是用来遍历集合用的,它的特点是更加安全,因为它可以确保,在当前遍历的集合元素被更改的时候,就会抛出 `ConcurrentModificationException` 异常。
#### 如何使用?
我们通过使用迭代器来遍历 `HashMap`,演示一下 迭代器 Iterator 的使用。
```java
Map<Integer, String> map = new HashMap();
map.put(1, "Java");
map.put(2, "C++");
map.put(3, "PHP");
Iterator<Map.Entry<Integer, String>> iterator = map.entrySet().iterator();
while (iterator.hasNext()) {
Map.Entry<Integer, String> entry = iterator.next();
System.out.println(entry.getKey() + entry.getValue());
}
```
### 有哪些集合是线程不安全的?怎么解决呢?
我们常用的 `Arraylist` ,`LinkedList`,`Hashmap`,`HashSet`,`TreeSet`,`TreeMap``PriorityQueue` 都不是线程安全的。解决办法很简单,可以使用线程安全的集合来代替。
如果你要使用线程安全的集合的话, `java.util.concurrent` 包中提供了很多并发容器供你使用:
1. `ConcurrentHashMap`: 可以看作是线程安全的 `HashMap`
2. `CopyOnWriteArrayList`:可以看作是线程安全的 `ArrayList`,在读多写少的场合性能非常好,远远好于 `Vector`.
3. `ConcurrentLinkedQueue`:高效的并发队列,使用链表实现。可以看做一个线程安全的 `LinkedList`,这是一个非阻塞队列。
4. `BlockingQueue`: 这是一个接口JDK 内部通过链表、数组等方式实现了这个接口。表示阻塞队列,非常适合用于作为数据共享的通道。
5. `ConcurrentSkipListMap` :跳表的实现。这是一个`Map`,使用跳表的数据结构进行快速查找。
## Collection 子接口之 List
### Arraylist 和 Vector 的区别?
1. ArrayList 是 List 的主要实现类,底层使用 Object[ ]存储,适用于频繁的查找工作,线程不安全
2. Vector 是 List 的古老实现类,底层使用 Object[ ]存储,线程安全的。
### Arraylist 与 LinkedList 区别?
1. **是否保证线程安全:** `ArrayList``LinkedList` 都是不同步的,也就是不保证线程安全;
2. **底层数据结构:** `Arraylist` 底层使用的是 **`Object` 数组**`LinkedList` 底层使用的是 **双向链表** 数据结构JDK1.6 之前为循环链表JDK1.7 取消了循环。注意双向链表和双向循环链表的区别,下面有介绍到!)
3. **插入和删除是否受元素位置的影响:****`ArrayList` 采用数组存储,所以插入和删除元素的时间复杂度受元素位置的影响。** 比如:执行`add(E e)`方法的时候, `ArrayList` 会默认在将指定的元素追加到此列表的末尾,这种情况时间复杂度就是 O(1)。但是如果要在指定位置 i 插入和删除元素的话(`add(int index, E element)`)时间复杂度就为 O(n-i)。因为在进行上述操作的时候集合中第 i 和第 i 个元素之后的(n-i)个元素都要执行向后位/向前移一位的操作。 ② **`LinkedList` 采用链表存储,所以对于`add(E e)`方法的插入,删除元素时间复杂度不受元素位置的影响,近似 O(1),如果是要在指定位置`i`插入和删除元素的话(`(add(int index, E element)` 时间复杂度近似为`o(n))`因为需要先移动到指定位置再插入。**
4. **是否支持快速随机访问:** `LinkedList` 不支持高效的随机元素访问,而 `ArrayList` 支持。快速随机访问就是通过元素的序号快速获取元素对象(对应于`get(int index)`方法)。
5. **内存空间占用:** ArrayList 的空 间浪费主要体现在在 list 列表的结尾会预留一定的容量空间,而 LinkedList 的空间花费则体现在它的每一个元素都需要消耗比 ArrayList 更多的空间(因为要存放直接后继和直接前驱以及数据)。
#### 补充内容:双向链表和双向循环链表 #### 补充内容:双向链表和双向循环链表
@ -105,17 +190,41 @@ Iterator对象称为迭代器设计模式的一种迭代器可以对集
![双向循环链表](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/双向循环链表.png) ![双向循环链表](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/双向循环链表.png)
### 1.3.2 说一说 ArrayList 的扩容机制吧 #### 补充内容:RandomAccess 接口
```java
public interface RandomAccess {
}
```
查看源码我们发现实际上 `RandomAccess` 接口中什么都没有定义。所以,在我看来 `RandomAccess` 接口不过是一个标识罢了。标识什么? 标识实现这个接口的类具有随机访问功能。
`binarySearch)` 方法中,它要判断传入的 list 是否 `RamdomAccess` 的实例,如果是,调用`indexedBinarySearch()`方法,如果不是,那么调用`iteratorBinarySearch()`方法
```java
public static <T>
int binarySearch(List<? extends Comparable<? super T>> list, T key) {
if (list instanceof RandomAccess || list.size()<BINARYSEARCH_THRESHOLD)
return Collections.indexedBinarySearch(list, key);
else
return Collections.iteratorBinarySearch(list, key);
}
```
`ArrayList` 实现了 `RandomAccess` 接口, 而 `LinkedList` 没有实现。为什么呢?我觉得还是和底层数据结构有关!`ArrayList` 底层是数组,而 `LinkedList` 底层是链表。数组天然支持随机访问,时间复杂度为 O(1),所以称为快速随机访问。链表需要遍历到特定位置才能访问特定位置的元素,时间复杂度为 O(n),所以不支持快速随机访问。,`ArrayList` 实现了 `RandomAccess` 接口,就表明了他具有快速随机访问功能。 `RandomAccess` 接口只是标识,并不是说 `ArrayList` 实现 `RandomAccess` 接口才具有快速随机访问功能的!
### 说一说 ArrayList 的扩容机制吧
详见笔主的这篇文章:[通过源码一步一步分析 ArrayList 扩容机制](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList-Grow.md) 详见笔主的这篇文章:[通过源码一步一步分析 ArrayList 扩容机制](https://github.com/Snailclimb/JavaGuide/blob/master/docs/java/collection/ArrayList-Grow.md)
## 1.4 Collection子接口之Set ## Collection 子接口之 Set
### 1.4.1 comparable 和 Comparator的区别 r
- comparable接口实际上是出自java.lang包 它有一个 `compareTo(Object obj)`方法用来排序 ### comparable 和 Comparator 的区别
- comparator接口实际上是出自 java.util 包它有一个`compare(Object obj1, Object obj2)`方法用来排序
一般我们需要对一个集合使用自定义排序时,我们就要重写`compareTo()`方法或`compare()`方法当我们需要对某一个集合实现两种排序方式比如一个song对象中的歌名和歌手名分别采用一种排序方法的话我们可以重写`compareTo()`方法和使用自制的Comparator方法或者以两个Comparator来实现歌名排序和歌星名排序第二种代表我们只能使用两个参数版的 `Collections.sort()`. - `comparable` 接口实际上是出自`java.lang`包 它有一个 `compareTo(Object obj)`方法用来排序
- `comparator`接口实际上是出自 java.util 包它有一个`compare(Object obj1, Object obj2)`方法用来排序
一般我们需要对一个集合使用自定义排序时,我们就要重写`compareTo()`方法或`compare()`方法,当我们需要对某一个集合实现两种排序方式,比如一个 song 对象中的歌名和歌手名分别采用一种排序方法的话,我们可以重写`compareTo()`方法和使用自制的`Comparator`方法或者以两个 Comparator 来实现歌名排序和歌星名排序,第二种代表我们只能使用两个参数版的 `Collections.sort()`.
#### Comparator 定制排序 #### Comparator 定制排序
@ -199,11 +308,10 @@ public class Person implements Comparable<Person> {
} }
/** /**
* TODO重写compareTo方法实现按年龄来排序 * T重写compareTo方法实现按年龄来排序
*/ */
@Override @Override
public int compareTo(Person o) { public int compareTo(Person o) {
// TODO Auto-generated method stub
if (this.age > o.getAge()) { if (this.age > o.getAge()) {
return 1; return 1;
} }
@ -241,21 +349,23 @@ Output
30-张三 30-张三
``` ```
### 1.4.2 无序性和不可重复性的含义是什么 ### 无序性和不可重复性的含义是什么
1、什么是无序性无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。 1、什么是无序性无序性不等于随机性 ,无序性是指存储的数据在底层数组中并非按照数组索引的顺序添加 ,而是根据数据的哈希值决定的。
2、什么是不可重复性不可重复性是指添加的元素按照 equals()判断时 ,返回 false需要同时重写 equals()方法和 HashCode()方法。 2、什么是不可重复性不可重复性是指添加的元素按照 equals()判断时 ,返回 false需要同时重写 equals()方法和 HashCode()方法。
### 1.4.3 比较HashSet、LinkedHashSet和TreeSet三者的异同 ### 比较 HashSet、LinkedHashSet 和 TreeSet 三者的异同
HashSet 是 Set 接口的主要实现类 HashSet 的底层是 HashMap线程不安全的可以存储 null 值; HashSet 是 Set 接口的主要实现类 HashSet 的底层是 HashMap线程不安全的可以存储 null 值;
LinkedHashSet 是 HashSet 的子类,能够按照添加的顺序遍历; LinkedHashSet 是 HashSet 的子类,能够按照添加的顺序遍历;
TreeSet 底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式有自然排序和定制排序。 TreeSet 底层使用红黑树,能够按照添加元素的顺序进行遍历,排序的方式有自然排序和定制排序。
## Map 接口
## 1.5 Map接口 ### HashMap 和 Hashtable 的区别
### 1.5.1 HashMap 和 Hashtable 的区别
1. **线程是否安全:** HashMap 是非线程安全的HashTable 是线程安全的,因为 HashTable 内部的方法基本都经过`synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!); 1. **线程是否安全:** HashMap 是非线程安全的HashTable 是线程安全的,因为 HashTable 内部的方法基本都经过`synchronized` 修饰。(如果你要保证线程安全的话就使用 ConcurrentHashMap 吧!);
2. **效率:** 因为线程安全的问题HashMap 要比 HashTable 效率高一点。另外HashTable 基本被淘汰,不要在代码中使用它; 2. **效率:** 因为线程安全的问题HashMap 要比 HashTable 效率高一点。另外HashTable 基本被淘汰,不要在代码中使用它;
@ -300,18 +410,86 @@ TreeSet底层使用红黑树能够按照添加元素的顺序进行遍历
} }
``` ```
### 1.5.2 HashMap 和 HashSet区别 ### HashMap 和 HashSet 区别
如果你看过 `HashSet` 源码的话就应该知道HashSet 底层就是基于 HashMap 实现的。HashSet 的源码非常非常少,因为除了 `clone()``writeObject()``readObject()`是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。 如果你看过 `HashSet` 源码的话就应该知道HashSet 底层就是基于 HashMap 实现的。HashSet 的源码非常非常少,因为除了 `clone()``writeObject()``readObject()`是 HashSet 自己不得不实现之外,其他方法都是直接调用 HashMap 中的方法。
| HashMap | HashSet | | HashMap | HashSet |
| :------------------------------: | :----------------------------------------------------------: | | :--------------------------------: | :-----------------------------------------------------------------------------------------------------------------: |
| 实现了 Map 接口 | 实现 Set 接口 | | 实现了 Map 接口 | 实现 Set 接口 |
| 存储键值对 | 仅存储对象 | | 存储键值对 | 仅存储对象 |
| 调用 `put()`向 map 中添加元素 | 调用 `add()`方法向 Set 中添加元素 | | 调用 `put()`向 map 中添加元素 | 调用 `add()`方法向 Set 中添加元素 |
| HashMap 使用键Key计算 Hashcode | HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以 equals()方法用来判断对象的相等性, | | HashMap 使用键Key计算 Hashcode | HashSet 使用成员对象来计算 hashcode 值,对于两个对象来说 hashcode 可能相同,所以 equals()方法用来判断对象的相等性, |
### 1.5.3 HashSet如何检查重复 ### HashMap 和 TreeMap 区别
`TreeMap``HashMap` 都继承自`AbstractMap` ,但是需要注意的是`TreeMap`它还实现了`NavigableMap`接口和`SortedMap` 接口。
![](./images/TreeMap继承结构.png)
实现 `NavigableMap` 接口让 `TreeMap` 有了对集合内元素的搜索的能力。
实现`SortMap`接口让 `TreeMap` 有了对集合中的元素根据键排序的能力。默认是按 key 的升序排序,不过我们也可以指定排序的比较器。示例代码如下:
```java
/**
* @author shuang.kou
* @createTime 2020年06月15日 17:02:00
*/
public class Person {
private Integer age;
public Person(Integer age) {
this.age = age;
}
public Integer getAge() {
return age;
}
public static void main(String[] args) {
TreeMap<Person, String> treeMap = new TreeMap<>(new Comparator<Person>() {
@Override
public int compare(Person person1, Person person2) {
int num = person1.getAge() - person2.getAge();
return Integer.compare(num, 0);
}
});
treeMap.put(new Person(3), "person1");
treeMap.put(new Person(18), "person2");
treeMap.put(new Person(35), "person3");
treeMap.put(new Person(16), "person4");
treeMap.entrySet().stream().forEach(personStringEntry -> {
System.out.println(personStringEntry.getValue());
});
}
}
```
输出:
```
person1
person4
person2
person3
```
可以看出,`TreeMap` 中的元素已经是按照 `Person` 的 age 字段的升序来排列了。
上面,我们是通过传入匿名内部类的方式实现的,你可以将代码替换成 Lambda 表达式实现的方式:
```java
TreeMap<Person, String> treeMap = new TreeMap<>((person1, person2) -> {
int num = person1.getAge() - person2.getAge();
return Integer.compare(num, 0);
});
```
**综上,相比于`HashMap`来说 `TreeMap` 主要多了对集合中的元素根据键排序的能力以及对集合内元素的搜索的能力。**
### HashSet 如何检查重复
当你把对象加入`HashSet`HashSet 会先计算对象的`hashcode`值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcodeHashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用`equals()`方法来检查 hashcode 相等的对象是否真的相同。如果两者相同HashSet 就不会让加入操作成功。(摘自我的 Java 启蒙书《Head fist java》第二版 当你把对象加入`HashSet`HashSet 会先计算对象的`hashcode`值来判断对象加入的位置,同时也会与其他加入的对象的 hashcode 值作比较,如果没有相符的 hashcodeHashSet 会假设对象没有重复出现。但是如果发现有相同 hashcode 值的对象,这时会调用`equals()`方法来检查 hashcode 相等的对象是否真的相同。如果两者相同HashSet 就不会让加入操作成功。(摘自我的 Java 启蒙书《Head fist java》第二版
@ -331,7 +509,7 @@ TreeSet底层使用红黑树能够按照添加元素的顺序进行遍历
对于引用类型包括包装类型来说equals 如果没有被重写,对比它们的地址是否相等;如果 equals()方法被重写(例如 String则比较的是地址里的内容。 对于引用类型包括包装类型来说equals 如果没有被重写,对比它们的地址是否相等;如果 equals()方法被重写(例如 String则比较的是地址里的内容。
### 1.5.4 HashMap的底层实现 ### HashMap 的底层实现
#### JDK1.8 之前 #### JDK1.8 之前
@ -372,7 +550,7 @@ static int hash(int h) {
![jdk1.8之前的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/jdk1.8之前的内部结构-HashMap.jpg) ![jdk1.8之前的内部结构-HashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/jdk1.8之前的内部结构-HashMap.jpg)
#### 5.4.2. <a name='JDK1.8-1'></a>JDK1.8之后 #### JDK1.8 之后
相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8将链表转换成红黑树前会判断如果当前数组的长度小于 64那么会选择先进行数组扩容而不是转换为红黑树将链表转化为红黑树以减少搜索时间。 相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8将链表转换成红黑树前会判断如果当前数组的长度小于 64那么会选择先进行数组扩容而不是转换为红黑树将链表转化为红黑树以减少搜索时间。
@ -384,7 +562,7 @@ static int hash(int h) {
- 《Java 8 系列之重新认识 HashMap》 <https://zhuanlan.zhihu.com/p/21673805> - 《Java 8 系列之重新认识 HashMap》 <https://zhuanlan.zhihu.com/p/21673805>
### 1.5.5 HashMap 的长度为什么是2的幂次方 ### HashMap 的长度为什么是 2 的幂次方
为了能让 HashMap 存取高效尽量较少碰撞也就是要尽量把数据分配均匀。我们上面也讲到了过了Hash 值的范围值-2147483648 到 2147483647前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ `(n - 1) & hash`”。n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。 为了能让 HashMap 存取高效尽量较少碰撞也就是要尽量把数据分配均匀。我们上面也讲到了过了Hash 值的范围值-2147483648 到 2147483647前后加起来大概 40 亿的映射空间,只要哈希函数映射得比较均匀松散,一般应用是很难出现碰撞的。但问题是一个 40 亿长度的数组,内存是放不下的。所以这个散列值是不能直接拿来用的。用之前还要先做对数组的长度取模运算,得到的余数才能用来要存放的位置也就是对应的数组下标。这个数组下标的计算方法是“ `(n - 1) & hash`”。n 代表数组长度)。这也就解释了 HashMap 的长度为什么是 2 的幂次方。
@ -392,13 +570,17 @@ static int hash(int h) {
我们首先可能会想到采用%取余的操作来实现。但是,重点来了:**“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。”** 并且 **采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。** 我们首先可能会想到采用%取余的操作来实现。但是,重点来了:**“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。”** 并且 **采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。**
### 1.5.6 HashMap 多线程操作导致死循环问题 ### HashMap 多线程操作导致死循环问题
主要原因在于并发下的 Rehash 会造成元素之间会形成一个循环链表。不过jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。 主要原因在于并发下的 Rehash 会造成元素之间会形成一个循环链表。不过jdk 1.8 后解决了这个问题,但是还是不建议在多线程下使用 HashMap,因为多线程下使用 HashMap 还是会存在其他问题比如数据丢失。并发环境下推荐使用 ConcurrentHashMap 。
详情请查看:<https://coolshell.cn/articles/9606.html> 详情请查看:<https://coolshell.cn/articles/9606.html>
### 1.5.7 ConcurrentHashMap 和 Hashtable 的区别 ### HashMap 有哪几种常见的遍历方式?
[HashMap 的 7 种遍历方式与性能分析!(强烈推荐)](https://mp.weixin.qq.com/s/Zz6mofCtmYpABDL1ap04ow)
### ConcurrentHashMap 和 Hashtable 的区别
ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。 ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方式上不同。
@ -421,7 +603,7 @@ ConcurrentHashMap 和 Hashtable 的区别主要体现在实现线程安全的方
![JDK1.8的ConcurrentHashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JDK1.8-ConcurrentHashMap-Structure.jpg) ![JDK1.8的ConcurrentHashMap](https://my-blog-to-use.oss-cn-beijing.aliyuncs.com/2019-6/JDK1.8-ConcurrentHashMap-Structure.jpg)
### 1.5.8 ConcurrentHashMap线程安全的具体实现方式/底层具体实现 ### ConcurrentHashMap 线程安全的具体实现方式/底层具体实现
#### JDK1.7(上面有示意图) #### JDK1.7(上面有示意图)
@ -444,10 +626,92 @@ ConcurrentHashMap取消了Segment分段锁采用CAS和synchronized来保证
synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。 synchronized 只锁定当前链表或红黑二叉树的首节点,这样只要 hash 不冲突,就不会产生并发,效率又提升 N 倍。
## 1.6 Collections工具类 ## Collections 工具类
详见笔主的这篇文章: https://gitee.com/SnailClimb/JavaGuide/blob/master/docs/java/basic/Arrays,CollectionsCommonMethods.md Collections 工具类常用方法:
1. 排序
2. 查找,替换操作
3. 同步控制(不推荐,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合)
### 排序操作
```java
void reverse(List list)//反转
void shuffle(List list)//随机排序
void sort(List list)//按自然排序的升序排序
void sort(List list, Comparator c)//定制排序由Comparator控制排序逻辑
void swap(List list, int i , int j)//交换两个索引位置的元素
void rotate(List list, int distance)//旋转。当distance为正数时将list后distance个元素整体移到前面。当distance为负数时将 list的前distance个元素整体移到后面
```
### 查找,替换操作
```java
int binarySearch(List list, Object key)//对List进行二分查找返回索引注意List必须是有序的
int max(Collection coll)//根据元素的自然顺序,返回最大的元素。 类比int min(Collection coll)
int max(Collection coll, Comparator c)//根据定制排序返回最大元素排序规则由Comparatator类控制。类比int min(Collection coll, Comparator c)
void fill(List list, Object obj)//用指定的元素代替指定list中的所有元素。
int frequency(Collection c, Object o)//统计元素出现次数
int indexOfSubList(List list, List target)//统计target在list中第一次出现的索引找不到则返回-1类比int lastIndexOfSubList(List source, list target).
boolean replaceAll(List list, Object oldVal, Object newVal), 用新元素替换旧元素
```
### 同步控制
`Collections` 提供了多个`synchronizedXxx()`方法·,该方法可以将指定集合包装成线程同步的集合,从而解决多线程并发访问集合时的线程安全问题。
我们知道 `HashSet``TreeSet``ArrayList`,`LinkedList`,`HashMap`,`TreeMap` 都是线程不安全的。`Collections` 提供了多个静态方法可以把他们包装成线程同步的集合。
**最好不要用下面这些方法,效率非常低,需要线程安全的集合类型时请考虑使用 JUC 包下的并发集合。**
方法如下:
```java
synchronizedCollection(Collection<T> c) //返回指定 collection 支持的同步线程安全的collection。
synchronizedList(List<T> list)//返回指定列表支持的同步线程安全的List。
synchronizedMap(Map<K,V> m) //返回由指定映射支持的同步线程安全的Map。
synchronizedSet(Set<T> s) //返回指定 set 支持的同步线程安全的set。
```
## 其他重要问题
### 什么是快速失败(fail-fast)
**快速失败(fail-fast)** 是 Java 集合的一种错误检测机制。**在使用迭代器对集合进行遍历的时候,我们在多线程下操作非安全失败(fail-safe)的集合类可能就会触发 fail-fast 机制,导致抛出 `ConcurrentModificationException` 异常。 另外,在单线程下,如果在遍历过程中对集合对象的内容进行了修改的话也会触发 fail-fast 机制。**
> 注:增强 for 循环也是借助迭代器进行遍历。
举个例子:多线程下,如果线程 1 正在对集合进行遍历,此时线程 2 对集合进行修改(增加、删除、修改),或者线程 1 在遍历过程中对集合进行修改,都会导致线程 1 抛出 `ConcurrentModificationException` 异常。
**为什么呢?**
每当迭代器使用 `hashNext()`/`next()`遍历下一个元素之前,都会检测 `modCount` 变量是否为 `expectedModCount` 值,是的话就返回遍历;否则抛出异常,终止遍历。
如果我们在集合被遍历期间对其进行修改的话,就会改变 `modCount` 的值,进而导致 `modCount != expectedModCount` ,进而抛出 `ConcurrentModificationException` 异常。
> 注:通过 `Iterator` 的方法修改集合的话会修改到 `expectedModCount` 的值,所以不会抛出异常。
```java
final void checkForComodification() {
if (modCount != expectedModCount)
throw new ConcurrentModificationException();
}
```
好吧!相信大家已经搞懂了快速失败(fail-fast)机制以及它的原理。
我们再来趁热打铁,看一个阿里巴巴手册相关的规定:
![](https://imgkr.cn-bj.ufileos.com/ad28e3ba-e419-4724-869c-73879e604da1.png)
有了前面讲的基础,我们应该知道:使用 `Iterator` 提供的 `remove` 方法,可以修改到 `expectedModCount` 的值。所以,才不会再抛出`ConcurrentModificationException` 异常。
### 什么是安全失败(fail-safe)呢?
明白了快速失败(fail-fast)之后,安全失败(fail-safe)我们就很好理解了。
采用安全失败机制的集合容器,在遍历时不是直接在集合内容上访问的,而是先复制原有集合内容,在拷贝的集合上进行遍历。所以,在遍历过程中对原集合所作的修改并不能被迭代器检测到,故不会抛 `ConcurrentModificationException` 异常
## 公众号 ## 公众号

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB