Skip to content

Commit

Permalink
✨ feat: 更新好物推荐 & 🐞 fix: 修改部分知识内容
Browse files Browse the repository at this point in the history
1. 修正部分知识问题
2. 更新一些大厂八股文
3. 增加第一期好物推荐
  • Loading branch information
zhiyu1998 committed Jun 30, 2023
1 parent 01c0045 commit a7c14c4
Show file tree
Hide file tree
Showing 20 changed files with 1,464 additions and 186 deletions.
7 changes: 7 additions & 0 deletions src/.vuepress/sidebar/zh.ts
Original file line number Diff line number Diff line change
Expand Up @@ -94,5 +94,12 @@ export const zhSidebar = sidebar({
children: "structure",
collapsible: true,
},
{
icon: "autumn",
text: "好物推荐",
prefix: "recommend",
children: "structure",
collapsible: true,
},
],
});
64 changes: 64 additions & 0 deletions src/Java/eightpart/concurrency.md
Original file line number Diff line number Diff line change
Expand Up @@ -942,7 +942,21 @@ public ThreadPoolExecutor(int corePoolSize, //线程池的核心线程数量
```

---
Java 的 ThreadPoolExecutor 提供了几种 **BlockingQueue** 实现作为参数:
1. **ArrayBlockingQueue**:基于数组的先进先出队列, FIFO 规则。有界队列,满时会阻塞添加操作。
2. **LinkedBlockingQueue**:基于链表的先进先出队列,FIFO 规则。可选的有界队列,但默认是无界的。
3. **SynchronousQueue**:不存储元素的阻塞队列。每个插入操作都必须等待一个移除操作,反之亦然。
4. **PriorityBlockingQueue**:具有优先级的无界阻塞队列。优先级采用自然顺序或 Comparator 顺序。
5. **LinkedTransferQueue**:由链表结构组成的无界阻塞 FIFO 队列。
6. **DelayQueue**:一个无界的阻塞队列,用于放置实现 Delayed 接口的元素。
7. **LinkedBlockingDeque**:基于链表的双向阻塞队列,可以用作阻塞队列的 FIFO 和 LIFO。

常见的场景:
1. **LinkedBlockingQueue**:高吞吐量且公平的阻塞队列,一般作为 ThreadPoolExecutor 的任务队列使用。
2. **ArrayBlockingQueue**:有界阻塞队列,可以指定最大容量,超过容量时阻塞添加操作,保证不会内存溢出。
3. **SynchronousQueue**:没有存储空间的阻塞队列,用于传递性场景,比如一个线程提交任务,另一个线程立即消费的场景。

---
<details>
<summary>ThreadPoolExecutor 3 个最重要的参数:</summary>
<p>
Expand Down Expand Up @@ -983,6 +997,56 @@ public ThreadPoolExecutor(int corePoolSize, //线程池的核心线程数量
</p>
</details>

---
>这里RejectedExecutionHandler面试官可能还会问一个问题:是否可以实现自定义拒绝策略?
Java中的ThreadPoolExecutor允许你自定义拒绝策略。RejectedExecutionHandler是一个接口,你可以实现这个接口来定义自己的拒绝策略。下面是一个简单的自定义拒绝策略示例:

首先,我们创建一个实现RejectedExecutionHandler接口的类:
```java
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

public class CustomRejectedExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
// 在这里实现你的自定义拒绝策略
System.out.println("自定义拒绝策略:任务 " + r.toString() + " 被拒绝");
}
}
```
接下来,我们创建一个ThreadPoolExecutor实例,并将自定义的拒绝策略作为参数传递:
```java
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class CustomRejectedExecutionHandlerExample {
public static void main(String[] args) {
int corePoolSize = 2;
int maximumPoolSize = 4;
long keepAliveTime = 10;
TimeUnit unit = TimeUnit.SECONDS;
ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(2);

CustomRejectedExecutionHandler rejectionHandler = new CustomRejectedExecutionHandler();

ThreadPoolExecutor executor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, rejectionHandler);

// 添加任务到线程池
for (int i = 0; i < 10; i++) {
Runnable task = () -> {
System.out.println("执行任务: " + Thread.currentThread().getName());
};
executor.execute(task);
}

// 关闭线程池
executor.shutdown();
}
}
```
在这个例子中,我们创建了一个具有自定义拒绝策略的ThreadPoolExecutor。当线程池无法处理更多任务时,它将执行我们在CustomRejectedExecutionHandler类中定义的拒绝策略。在这个例子里,我们只是简单地打印出了被拒绝的任务信息。你可以根据需要实现更复杂的拒绝策略。

### JUC 包中的原子类是哪 4 类?

Expand Down
17 changes: 12 additions & 5 deletions src/Java/eightpart/foundation.md
Original file line number Diff line number Diff line change
Expand Up @@ -1719,6 +1719,13 @@ if (++size > threshold)
resize();
```

>HashMap为什么要扩展为原来的2倍而不是其他倍数?

1. **运算简单**:将容量扩展为 2 的幂次方有助于提高计算性能。在计算机内部,位操作通常比其他算术运算更快。当容量为 2 的幂次方时,对应的哈希函数可以通过简单的位操作来计算,而不需要执行更复杂的模运算。例如,如果容量为 2^n,那么计算哈希值的索引就是 index = hash & (2^n - 1),这是一个简单的位操作。
2. **分布均匀**HashMap的散列函数是(n - 1) & hash。当容量为 2 的幂数时,哈希值在分桶中的分布更均匀(如果扩容为2倍,那么n-1的低位全为1,与原来哈希值的低位进行与运算,可以保证哈希桶的分布保持相同)。如果使用其他倍数,例如 3 的幂次方,可能导致分布不均匀(低位不全为1,和原来哈希值进行与运算后,哈希桶的分布会发生变化),进而影响到 HashMap 的性能。
3. **重新散列的成本更低**:因为n-1的低位全为1,相当于只是在高位增加了1bit,那么原来的键值对只需要移动2次(原索引+0和原索引+oldCap)就可以迁移到新的table中正确的位置。如果扩容不是2倍,键值对的迁移成本会更高。
4. **链表转红黑树更合适**HashMap的链表转红黑树的阈值是8,如果扩容为2倍,那么原来的键值对迁移到新的table后,在原索引位置和原索引+oldCap位置的链表个数之和就已经达到8个,这样就可以在迁移过程中进行树化。如果扩容倍数更大,达到树化阈值就要更长的链表,影响性能。

**代码总览**
```java
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
Expand Down Expand Up @@ -1823,17 +1830,17 @@ final Node<K,V> removeNode(int hash, Object key, Object value,

我们首先可能会想到采用%取余的操作来实现。但是,重点来了:**“取余(%)操作中如果除数是 2 的幂次则等价于与其除数减一的与(&)操作(也就是说 hash%length==hash&(length-1)的前提是 length 是 2 的 n 次方;)。”** 并且 **采用二进制位操作 &,相对于%能够提高运算效率,这就解释了 HashMap 的长度为什么是 2 的幂次方。**

🙋‍♂️笔者认为Guide写的这个摸棱两可,所以找了一个StackOverFlow中描述更为清楚的翻译为下:
🙋‍♂️我认为Guide写的这个摸棱两可,所以找了一个StackOverFlow中描述更为清楚的翻译为下:

> 🧚‍♂️原帖:https://stackoverflow.com/questions/53526790/why-are-hashmaps-implemented-using-powers-of-two
>
> 当你从一个幂为2的数字中减去1得到的是一个二进制表示都是1的数字。例如162的幂。如果从中减去1,得到15,它的二进制表示是1111。现在,如果你用1111对任何一个数字进行位 AND 运算,你将得到这个数字的最后4位,换句话说,它等于这个数字乘以16的模(除法运算通常是一个昂贵的运算。因此,位操作通常比除法更受欢迎)。最后的4位将计算为015之间的任意数字,这些数字是基础数组的索引。
> 当你从一个幂为2的数字中减去1得到的是一个二进制表示都是1的数字。例如162的幂。如果从中减去1,得到15,它的二进制表示是1111。现在,如果你用1111对任何一个数字进行位 AND 运算,你将得到这个数字的最后4位,换句话说,它等于这个数字乘以16的模(除法运算通常是一个昂贵的运算。因此,位操作通常比除法更受欢迎)。最后的4位将计算为015之间的任意数字,这些数字是基础数组的索引。
>
> 你可以改成17。在这种情况下,从中减去1得到16也就是二进制的10000现在你对一个16的数做 AND(与& 运算,你会失去所有的位,除了从结尾开始的第5位。因此,无论取多少数,数组索引都是160。这意味着会有很多冲突,这反过来又意味着性能很差。您将需要 O (log n)而不是 O (1)进行检索,因为当冲突发生时,给定桶中的所有节点都将存储在一个红黑色树中。不仅如此。如果您在一个多线程环境中使用 ConcurrentHashMap,那么您将经历大量的同步,因为所有新添加的内容最终都会以非常少的存储桶(在上述情况下只有2-016个)结束,并且当您在一个已经有其他节点的存储桶中添加新节点时,存储桶将被锁定,以避免由于多个线程的修改而导致的数据不一致。因此,尝试添加新节点的其他线程需要等待当前线程释放锁。
> 改成17,从中减去1得到16也就是二进制的10000现在你对一个16的数做AND(与&)运算,你会失去所有的位,除了从结尾开始的第5位。因此,无论取多少数,数组索引都是160。这意味着会有很多冲突,这反过来又意味着性能很差。您将需要O(log n)而不是O(1)进行检索,因为当冲突发生时,给定桶中的所有节点都将存储在一个红黑树中。不仅如此。如果您在一个多线程环境中使用ConcurrentHashMap,那么您将经历大量的同步,因为所有新添加的内容最终都会以非常少的存储桶(在上述情况下只有2-016个)结束,并且当您在一个已经有其他节点的存储桶中添加新节点时,存储桶将被锁定,以避免由于多个线程的修改而导致的数据不一致。因此,尝试添加新节点的其他线程需要等待当前线程释放锁。
>
> 最后,我还应该提到,Java HashMap 实现还将键的哈希代码的16位向右移动,并在用(length-1)进行位 AND 之前对原哈希代码进行位异或操作,以确保高阶位的效果也被捕获。
> 最后,我还应该提到,Java HashMap实现还将键的哈希代码的16位向右移动,并在用(length-1)进行位AND之前对原哈希代码进行位异或操作,以确保高阶位的效果也被捕获。
>
> 因此,基本上要点是,如果大小是2的幂次方,那么与其他任何不是2的大小相比,键将更均匀地分布在整个数组中,最小的冲突将导致更好的检索性能(ConcurrentHashMap 的情况下也更少的同步)。
> 因此,基本上要点是,如果大小是2的幂次方,那么与其他任何不是2的大小相比,键将更均匀地分布在整个数组中,最小的冲突将导致更好的检索性能(ConcurrentHashMap的情况下也更少的同步)。

为什么能减少碰撞?这是因为hashmap的hash值是hashCode右移16位得到的,这么做使得hash值的低位保留了高位的信息,所以只要低位就可以了。

Expand Down
Loading

0 comments on commit a7c14c4

Please sign in to comment.