Browse Source

修改了面经的文档

seamew 1 năm trước cách đây
mục cha
commit
16bdbd052c

BIN
面经/问答/assets/image-20230509151944605.png


+ 68 - 5
面经/问答/并发编程.md

@@ -12,14 +12,14 @@ wait()和notify()必须放在synchronized块中是因为这些方法依赖于对
 
 ## volatile 关键字
 
-在Java中,volatile关键字是用于保证多线程环境下变量的可见性和有序性。主要分为两大功能。
+在Java中,volatile关键字是用于保证多线程环境下变量的可见性和有序性。
 
 1. 保证变量可见性:被volatile关键字声明代表变量是共享且不稳定的,每次使用它都到主存中进行读取,并且强制刷新到内存。
-2. 进制指令重排序:JVM 具有指令重排的特性,可以保证程序执行效率更高,但是volatile关键字会进制指令重排保证其有序性。
+2. 保证变量有序性:JVM 具有指令重排的特性,可以保证程序执行效率更高,但是volatile关键字会进制指令重排保证其有序性。
 
 ## volatile 关键字底层原理
 
-`volatile`是通过编译器在生成字节码时,在指令序列中添加“**内存屏障**”来禁止指令重排序的
+`volatile`是通过编译器在生成字节码时,在指令序列中添加“**内存屏障**”来保证变量可见性
 
 JMM层面的“**内存屏障**”:
 
@@ -57,7 +57,7 @@ JVM的实现会在volatile读写前后均加上内存屏障,在一定程度上
 
 轻量级之所以是轻量级锁,是因为它仅仅使用 CAS 进行操作来获取锁。如果获取成功那么会直接获取锁,如果失败,当前线程便尝试使用自旋来获取锁。当竞争线程的自旋次数达到界限值(`threshold`),轻量级锁将会膨胀为重量级锁。
 
-重量级锁(`heavy weight lock`),是使用操作系统互斥量(`mutex`)来实现的传统锁。 当所有对锁的优化都失效时,将退回到重量级锁。它与轻量级锁不同竞争的线程不再通过自旋来竞争线程, 而是直接进入堵塞状态,此时不消耗CPU,然后等拥有锁的线程释放锁后,唤醒堵塞的线程, 然后线程再次竞争锁。但是注意,当锁膨胀(`inflate`)为重量锁时,就不能再退回到轻量级锁。
+重量级锁,是使用操作系统互斥量(`mutex`)来实现的传统锁。 当所有对锁的优化都失效时,将退回到重量级锁。它与轻量级锁不同竞争的线程不再通过自旋来竞争线程, 而是直接进入堵塞状态,此时不消耗CPU,然后等拥有锁的线程释放锁后,唤醒堵塞的线程, 然后线程再次竞争锁。但是注意,当锁膨胀为重量锁时,就不能再退回到轻量级锁。
 
 ## synchronized 和 volatile 有什么区别?
 
@@ -91,7 +91,7 @@ AQS提供了一种通用的框架,用于实现线程间的协作和同步操
 
 ## synchronized 和 ReentrantLock 有什么区别?
 
-* synchronized 依赖于 JVM 而 `ReentrantLock` 依赖于 API
+* `synchronized` 依赖于 JVM 而 `ReentrantLock` 依赖于 API
 * `ReentrantLock`可以指定是公平锁还是非公平锁。而`synchronized`只能是非公平锁。
 * `synchronized`是以代码块形式实现的,因此只能对整个代码块进行同步操作,无法在代码块内部实现一些细粒度的控制。而`ReentrantLock`可以通过`Condition`对象实现线程间的协作和控制。
 
@@ -168,6 +168,69 @@ AQS提供了一种通用的框架,用于实现线程间的协作和同步操
 
 CPU 密集型简单理解就是利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。但凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
 
+## 线程池,如果使用future,则必须重构拒绝策略
+
+* Future类的get方法
+
+```java
+private static final int NEW          = 0;
+private static final int COMPLETING   = 1;
+private static final int NORMAL       = 2;
+private static final int EXCEPTIONAL  = 3;
+private static final int CANCELLED    = 4;
+private static final int INTERRUPTING = 5;
+private static final int INTERRUPTED  = 6;
+
+public V get() throws InterruptedException, ExecutionException {
+    int s = state;
+    // 初始状态默认为0
+    if (s <= COMPLETING)
+        // 如果小于COMPLETING则阻塞
+        s = awaitDone(false, 0L);
+    return report(s);
+}
+```
+
+如果没有调用run方法改变线程状态则future则会被一直阻塞导致OOM
+
+```java
+public void run() {
+    if (state != NEW ||
+        !UNSAFE.compareAndSwapObject(this, runnerOffset,
+                                     null, Thread.currentThread()))
+        return;
+    try {
+        Callable<V> c = callable;
+        if (c != null && state == NEW) {
+            V result;
+            boolean ran;
+            try {
+                result = c.call();
+                ran = true;
+            } catch (Throwable ex) {
+                result = null;
+                ran = false;
+                setException(ex);
+            }
+            if (ran)
+                // 改变状态
+                set(result);
+        }
+    } finally {
+        // runner must be non-null until state is settled to
+        // prevent concurrent calls to run()
+        runner = null;
+        // state must be re-read after nulling runner to prevent
+        // leaked interrupts
+        int s = state;
+        if (s >= INTERRUPTING)
+            handlePossibleCancellationInterrupt(s);
+    }
+}
+```
+
+
+
 ## ThreadLocal
 
 通常情况下,我们创建的变量是可以被任何一个线程访问并修改的,想要每一个线程都有自己私有的本地变量就就需要使用ThreadLocal类,**`ThreadLocal`类主要解决的就是让每个线程绑定自己的值,可以将`ThreadLocal`类形象的比喻成存放数据的盒子,盒子中可以存储每个线程的私有数据。**

+ 3 - 3
面经/问答/操作系统.md

@@ -139,7 +139,7 @@ TLB即为页表缓存、转址旁路缓存、快表等。有了 TLB 后,那么
 
 ### 线程的通信方式
 
-* 互斥
+* 互斥
 * 信号量
 
 ### 线程间的同步的方式有哪些?
@@ -147,7 +147,7 @@ TLB即为页表缓存、转址旁路缓存、快表等。有了 TLB 后,那么
 下面是几种常见的线程同步的方式:
 
 1. **互斥锁**:采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以可以保证公共资源不会被多个线程同时访问。
-2. **读写锁:允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。
+2. **读写锁**:允许多个线程同时读取共享资源,但只有一个线程可以对共享资源进行写操作。
 3. **信号量**:它允许同一时刻多个线程访问同一资源,但是需要控制同一时刻访问此资源的最大线程数量。
 4. **屏障**:屏障是一种同步原语,用于等待多个线程到达某个点再一起继续执行。当一个线程到达屏障时,它会停止执行并等待其他线程到达屏障,直到所有线程都到达屏障后,它们才会一起继续执行。
 5. **事件** :Wait/Notify:通过通知操作的方式来保持多线程同步,还可以方便的实现多线程优先级的比较操作。
@@ -172,7 +172,7 @@ TLB即为页表缓存、转址旁路缓存、快表等。有了 TLB 后,那么
 
 1. **互斥**:资源必须处于非共享模式,即一次只有一个进程可以使用。如果另一进程申请该资源,那么必须等待直到该资源被释放为止。
 
-2. **占有等待**:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有。
+2. **占有等待**:一个进程至少应该占有一个资源,并等待另一资源,而该资源被其他进程所占有。
 
 3. **非抢占**:资源不能被抢占。只能在持有资源的进程完成任务后,该资源才会被释放。
 

+ 20 - 18
面经/问答/计网.md

@@ -8,6 +8,17 @@ TCP 是**面向连接的、可靠的、基于字节流**的传输层通信协议
 - **可靠的**:无论的网络链路中出现了怎样的链路变化,TCP 都可以保证一个报文一定能够到达接收端;
 - **字节流**:用户消息通过 TCP 协议传输时,消息可能会被操作系统「分组」成多个的 TCP 报文,如果接收方的程序如果不知道「消息的边界」,是无法读出一个有效的用户消息的。并且 TCP 报文是「有序的」,当「前一个」TCP 报文没有收到的时候,即使它先收到了后面的 TCP 报文,那么也不能扔给应用层去处理,同时对「重复」的 TCP 报文会自动丢弃。
 
+## TCP如何保证可靠
+
+* 重传机制
+  * 超时重传
+  * 快速重传
+  * SACK-- **可以将已收到的数据的信息发送给「发送方」**
+  * D-SACK -- **使用了 SACK 来告诉「发送方」有哪些数据被重复接收了**
+* 滑动窗口
+* 流量控制 -- **TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。**
+* 拥塞控制
+
 ## tcp拥塞控制的几个策略
 
 1. 慢启动:在连接刚建立时,TCP发送方会以较小的拥塞窗口(cwnd)开始发送数据,并根据每次接收到的确认(ACK)增加cwnd的大小,从而逐渐增加发送速率,达到网络带宽的最佳利用。
@@ -24,50 +35,39 @@ TCP 是**面向连接的、可靠的、基于字节流**的传输层通信协议
    3. 如果再收到重复的 ACK,那么 cwnd 增加 1;
    4. 如果收到新数据的 ACK 后,把 cwnd 设置为第一步中的 ssthresh 的值(这里是快速重传时候的ssthresh ),原因是该 ACK 确认了新的数据,说明从 duplicated ACK 时的数据都已收到,该恢复过程已经结束,可以回到恢复之前的状态了,也即再次进入拥塞避免状态;
 
-## TCP如何保证可靠
-
-* 重传机制
-  * 超时重传
-  * 快速重传
-  * SACK-- **可以将已收到的数据的信息发送给「发送方」**
-  * D-SACK -- **使用了 SACK 来告诉「发送方」有哪些数据被重复接收了**
-* 滑动窗口
-* 流量控制 -- **TCP 提供一种机制可以让「发送方」根据「接收方」的实际接收能力控制发送的数据量,这就是所谓的流量控制。**
-* 拥塞控制
-
 ## TCP和UDP区别
 
-*1. 连接*
+1. 连接
 
 - TCP 是面向连接的传输层协议,传输数据前先要建立连接。
 - UDP 是不需要连接,即刻传输数据。
 
-*2. 服务对象*
+2. 服务对象
 
 - TCP 是一对一的两点服务,即一条连接只有两个端点。
 - UDP 支持一对一、一对多、多对多的交互通信
 
-*3. 可靠性*
+3. 可靠性
 
 - TCP 是可靠交付数据的,数据可以无差错、不丢失、不重复、按序到达。
 - UDP 是尽最大努力交付,不保证可靠交付数据。
 
-*4. 拥塞控制、流量控制*
+4. 拥塞控制、流量控制
 
 - TCP 有拥塞控制和流量控制机制,保证数据传输的安全性。
 - UDP 则没有,即使网络非常拥堵了,也不会影响 UDP 的发送速率。
 
-*5. 头部长度不同*
+5. 头部长度不同
 
 - TCP 首部长度较长,会有一定的开销,首部在没有使用「选项」字段时是 `20` 个字节,如果使用了「选项」字段则会变长的。
 - UDP 首部只有 8 个字节,并且是固定不变的,开销较小。
 
-*6. 传输方式*
+6. 传输方式
 
 - TCP 是流式传输,没有边界,但保证顺序和可靠。
 - UDP 是一个包一个包的发送,是有边界的,但可能会丢包和乱序。
 
-*7. 分片不同*
+7. 分片不同
 
 - TCP 的数据大小如果大于 MSS 大小,则会在传输层进行分片,目标主机收到后,也同样在传输层组装 TCP 数据包,如果中途丢失了一个分片,只需要传输丢失的这个分片。
 - UDP 的数据大小如果大于 MTU 大小,则会在 IP 层进行分片,目标主机收到后,在 IP 层组装完数据,接着再传给传输层。
@@ -110,6 +110,8 @@ TCP 是**面向连接的、可靠的、基于字节流**的传输层通信协议
 
 需要注意的是**第三次握手是可以携带数据的,前两次握手是不可以携带数据的**
 
+![image-20230509151944605](assets/image-20230509151944605.png)
+
 ### 第一次握手丢失了,会发生什么?
 
 * 客户端会重传 SYN 报文,也就是第一次握手就会触发「超时重传」机制,重传 SYN 报文

+ 13 - 0
面经/问答/集合.md

@@ -10,6 +10,8 @@
 * 减少hash冲突
 * 每次扩容只用移动一半的元素
 
+这是因为hashmap缓存了key的hash值,每次移动元素只需要新的高位与1相与就行。
+
 ## get方法
 
 判断该元素时候为空,如果为空直接返回,如果不为空则返回value
@@ -18,6 +20,17 @@
 
 红黑树相比avl树,在检索的时候效率其实差不多,都是通过平衡来二分查找。但对于插入删除等操作效率提高很多。红黑树不像avl树一样追求绝对的平衡,他允许局部很少的不完全平衡,这样对于效率影响不大,但省去了很多没有必要的调平衡操作,avl树调平衡有时候代价较大,所以效率不如红黑树。
 
+Java 中的 HashMap 底层原理使用数组和链表/红黑树实现,而不使用 B 树,原因有以下几点:
+
+1. 红黑树的查找、插入、删除操作复杂度相对稳定,不会像 B 树 那样随着树高逐渐变差,平均时间复杂度为 O(log n)。B 树虽然有更快的查找速度,但在 Java 中,HashMap 的数据量一般都在1K以下,单机内存完全可以满足红黑树的需求。
+2. 空间利用率方面,B 树中非叶子节点需要存储下一级节点的指针,因此每个节点占用的空间相对较大,而红黑树只需要保存左右子节点和父节点的指针,所以相对来说更加节省空间。
+3. 红黑树相对于 B 树 更容易实现。
+4. 在实际应用中,虽然B树比红黑树有更优异的性能表现,但其实现比较复杂,在使用时需要结合具体场景来考虑。相比之下,红黑树实现简单,维护成本低,且平均情况下的性能也能够满足HashMap的需求,因此选择红黑树作为HashMap的底层数据结构更为合适。
+
+综上所述,HashMap 选择红黑树而不是 B 树 作为其底层数据结构,主要是出于对效率和实现的考虑。
+
+
+
 红黑树特征:
 
 1. 每个节点或者是黑色,或者是红色。