Skip to content

Commit

Permalink
🐞 fix: 更正部分知识 & 加入大厂面试题
Browse files Browse the repository at this point in the history
  • Loading branch information
zhiyu1998 committed Aug 8, 2023
1 parent b883e49 commit 53027c4
Show file tree
Hide file tree
Showing 40 changed files with 1,428 additions and 198 deletions.
76 changes: 76 additions & 0 deletions src/Java/eightpart/concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -875,6 +875,82 @@ static class ThreadLocalMap {

`ThreadLocalMap` 中使用的 key 为 `ThreadLocal` 的弱引用,而 value 是强引用。所以,如果 `ThreadLocal` 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。

**ThreadLocal为什么不用强引用?**

`ThreadLocal`在Java中是一种常用的线程隔离技术,它可以为每一个线程提供一个独立的变量副本,从而实现线程安全。但是,`ThreadLocal`并不使用强引用来存储这些变量副本,而是使用弱引用。这是出于以下几个原因:

1. **内存泄漏问题**:如果`ThreadLocal`使用强引用来存储变量副本,那么只要线程本身不结束,这些副本就不会被垃圾回收机制回收。这可能会导致内存泄漏问题,尤其是在长时间运行的线程和大量使用`ThreadLocal`的情况下。
2. **线程结束时的清理**:当线程结束时,所有由该线程持有的`ThreadLocal`变量副本应该被清理。如果`ThreadLocal`使用强引用,那么即使线程结束,这些副本仍然可能存在,除非显式地清理它们。而使用弱引用则可以确保线程结束时,这些副本会被自动清理。
3. **灵活的生命周期管理**:弱引用允许更灵活的生命周期管理。`ThreadLocal`中的变量副本只要不再被线程引用,就可以被垃圾回收机制回收。这可以有效防止内存过载,并允许更高效的资源利用。

虽然`ThreadLocal`使用弱引用可以带来以上好处,但也有一些需要注意的地方。比如,你需要确保在线程生命周期内,持有到`ThreadLocal`对象的强引用,否则这个`ThreadLocal`对象可能会提前被回收。另外,即使`ThreadLocal`使用弱引用,也不能完全防止内存泄漏。如果线程结束后,没有清理`ThreadLocal`的值,那么这些值将会保留在线程的ThreadLocalMap中,导致内存泄漏。所以,使用`ThreadLocal`时,最好的做法是在不再需要使用变量副本时,显式地清理它。



`ThreadLocal`实现线程本地存储的核心在于每个`Thread`都维护了一个`ThreadLocalMap``ThreadLocalMap`实际上是一个特殊设计的哈希映射,其键为`ThreadLocal`对象(使用弱引用), 值为线程局部变量。

`ThreadLocalMap`的设计使得`ThreadLocal`对象可以在不被线程引用的情况下被垃圾回收器回收。这是因为`ThreadLocalMap`的键使用了`ThreadLocal`的弱引用。弱引用是一种弱度小于强引用但大于软引用和弱引用的引用,只要垃圾回收器发现了弱引用,不论系统内存是否足够,都会回收掉只被弱引用关联的对象。

`ThreadLocal`对象被回收后,其对应的值在`ThreadLocalMap`中的实体(Entry)会变为null,这是一个空的引用。因此,`ThreadLocalMap`还需要提供一种机制来清理这些没有键的Entry,否则这些Entry的值可能会造成内存泄露。这个机制是在`ThreadLocal``get()`, `set()``remove()` 方法被调用时,通过调用`ThreadLocalMap``expungeStaleEntries()`方法来清理无效的Entry。(ps. 这里看源码调用链是这样的:get() -> map.getEntry(*this*) -> getEntryAfterMiss(key, i, e) -> expungeStaleEntry(i))

```java
// 清理过期的ThreadLocal变量
private void expungeStaleEntries() {
Entry[] tab = table;
int len = tab.length;
// 遍历数组中的每一个元素
for (int j = 0; j < len; j++) {
Entry e = tab[j];
// 如果该元素存在,但其对应的ThreadLocal变量已经被回收,则进行清理
if (e != null && e.get() == null)
expungeStaleEntry(j);
}
}

// 清理staleSlot位置上的元素
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;

// 清除staleSlot位置上的元素
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变量
ThreadLocal<?> k = e.get();
if (k == null) {
// 如果ThreadLocal变量已经被回收,则清除该元素
e.value = null;
tab[i] = null;
size--;
} else {
// 计算新的哈希值
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
// 如果哈希值不等于当前位置,则将该元素拷贝到新位置
tab[i] = null;

// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
```

然而,尽管`ThreadLocalMap`提供了自动清理无效Entry的机制,但是如果我们不再使用`ThreadLocal`对象,但是又没有显式地调用`ThreadLocal``remove()`方法,那么`ThreadLocal`的值是不会被自动清理的,因为`expungeStaleEntries()`方法只有在`ThreadLocal``get()`, `set()``remove()` 方法被调用时才会执行。这就可能导致内存泄露,尤其是在长时间运行的线程中。因此,我们在使用`ThreadLocal`时,通常需要手动调用`ThreadLocal``remove()`方法来清理不再使用的值。

### 实现 Runnable 接口和 Callable 接口的区别

继承Thread类或者实现Runnable接口这两种方式来创建线程类,但是这两种方式有一个共同的缺陷:不能获取异步执行的结果。Callable接口类似于Runnable。不同的是,Runnable的唯一抽象方法run()没有返回值,也没有受检异常的异常声明。比较而言,Callable接口的call()有返回值,并且声明了受检异常,其功能更强大一些。
Expand Down
80 changes: 79 additions & 1 deletion src/Java/eightpart/foundation.md
Original file line number Diff line number Diff line change
Expand Up @@ -1334,6 +1334,51 @@ http://softlab.sdut.edu.cn/blog/subaochen/2017/01/generics-type-erasure/

![image-20220622211220694](./personal_images/image-20220622211220694.webp)

具体的:

```java
public class Parent<T> {
public T data;

public void setData(T data) {
this.data = data;
}
}

public class Child extends Parent<Integer> {
@Override
public void setData(Integer data) {
super.setData(data);
}
}
```

在这个例子中,我们有一个泛型类 `Parent` 和一个扩展了 `Parent` 的非泛型类 `Child`。我们在 `Child` 中覆盖了 `Parent` 中的 `setData` 方法。这里要注意的是,`Child` 中的 `setData` 方法覆盖的是 `Parent<Integer>` 中的 `setData` 方法,而非 `Parent` 类中的泛型 `setData` 方法。

为了解决这个问题,编译器会在 `Child` 类中自动生成一个桥方法,如下:

```java
public void setData(Object data) {
setData((Integer) data);
}
```

这个方法的参数是 `Object` 类型,这样它就可以覆盖 `Parent` 类中的 `setData` 方法了。并且,这个方法内部会将参数转换为 `Integer` 类型,然后调用 `Child` 类中的 `setData(Integer)` 方法,也就是我们明确写出的方法。

所以,你并不需要手动写出桥方法,它是由编译器自动插入的。并且,桥方法只有在需要解决泛型类型擦除导致的方法覆盖问题时才会被生成。



小结一下:

在Java中,因为泛型信息在编译后会被擦除,所以运行时所有的泛型类型都会变成它们的上界,对于没有明确指定上界的泛型参数,它的上界默认是 `Object`

在你的例子中,`Parent` 类中的 `setData` 方法的参数类型 `T` 在运行时会变成 `Object`。因此,如果 `Child` 类想要覆盖这个方法,那么它的方法参数也必须是 `Object` 类型。但是,我们在 `Child` 类中定义的 `setData` 方法的参数类型是 `Integer`,因此它并没有真正覆盖 `Parent` 类中的 `setData` 方法。

为了解决这个问题,编译器会自动在 `Child` 类中生成一个桥方法,这个方法的参数类型是 `Object`,它覆盖了 `Parent` 类中的 `setData` 方法。这个桥方法内部会将参数强制转换为 `Integer` 类型,然后调用 `Child` 类中的 `setData(Integer)` 方法。这样,我们就可以通过 `Parent` 类的引用来调用 `Child` 类中的方法了,而不会出现类型不匹配的问题。



#### 为了更深层次的理解类型擦除,选取了Stackoverflow的高赞回答

原帖:https://stackoverflow.com/questions/339699/java-generics-type-erasure-when-and-what-happens
Expand Down Expand Up @@ -1552,7 +1597,7 @@ static int hash(int h) {

#### JDK1.8 之后

相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。
相比于之前的版本, JDK1.8 之后在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为 8)(将链表转换成红黑树前会判断,如果当前数组的长度小于 64,那么会选择先进行数组扩容,而不是转换为红黑树)时,将链表转化为红黑树,以减少搜索时间。如果是红黑树,当元素数量少于6时,会退化为链表。

![image-20220616155623710](./personal_images/image-20220616155623710.webp)

Expand Down Expand Up @@ -1581,6 +1626,39 @@ static final int hash(Object key) {

这样处理的主要目的是在不增加计算复杂度的前提下,尽可能减少哈希碰撞并更好地分布数据。因为HashMap中的bucket选择主要是通过哈希值的低位进行的,如果原始哈希值的高位和低位相似,那么进行这样的处理能够将高位的信息带入到低位,从而提高bucket的选择范围,减少碰撞。

让我们看一个具体的例子:

假设有一个`String`对象,其`hashCode()`方法返回的值为`123456789`

在32位二进制表示中,这个数值看起来是这样的:

```shell
0000 0111 0101 1011 1100 1101 0001 0101
```

当我们将这个值右移16位(`>>> 16`)后,我们得到:

```shell
0000 0000 0000 0000 0000 0111 0101 1011
```

接下来,我们将这两个值进行异或(`^`)操作:

```shell
0000 0111 0101 1011 1100 1101 0001 0101 (hashCode)
0000 0000 0000 0000 0000 0111 0101 1011 (hashCode >>> 16)
---------------------------------------
0000 0111 0101 1011 1100 1101 0110 1110 (result)
```

最终结果就是新的哈希值。

现在,你可能会问为什么需要这样的操作。在HashMap中,我们`通常使用哈希值的低位来决定一个元素存放在哪个桶中,高位则不常用`。当我们将hashCode右移16位后,原来的高位被放到了低位。然后我们通过异或操作,使得新的低位同时受到原始的高位和低位的影响,这样就`使得原来不常使用的高位信息也能参与到桶的选择中`,从而使得元素在各个桶之间的分布更均匀。

这是一个非常巧妙的设计,既利用到了原始的hashCode信息,也改善了哈希分布。



#### getNode()

`get(Object key)`方法根据指定的 `key`值返回对应的 `value`,该方法调用了 `getEntry(Object key)`得到相应的 `entry`,然后返回 `entry.getValue()`。因此 `getEntry()`是算法的核心。 **算法思想**是首先通过 `hash()`函数得到对应 `bucket`的下标,然后依次遍历冲突链表,通过 `key.equals(k)`方法来判断是否是要找的那个 `entry`
Expand Down
Loading

0 comments on commit 53027c4

Please sign in to comment.