diff --git a/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md b/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md index c402fcff..1fe8ea3c 100644 --- a/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md +++ b/docs/database/mysql/mysql-high-performance-optimization-specification-recommendations.md @@ -198,9 +198,9 @@ InnoDB 是按照主键索引的顺序来组织表的 建立索引的目的是:希望通过索引进行数据查找,减少随机 IO,增加查询性能 ,索引能过滤出越少的数据,则从磁盘中读入的数据也就越少。 -- 区分度最高的放在联合索引的最左侧(区分度=列中不同值的数量/列的总行数) -- 尽量把字段长度小的列放在联合索引的最左侧(因为字段长度越小,一页能存储的数据量越大,IO 性能也就越好) -- 使用最频繁的列放到联合索引的左侧(这样可以比较少的建立一些索引) +- **区分度最高的列放在联合索引的最左侧:** 这是最重要的原则。区分度越高,通过索引筛选出的数据就越少,I/O 操作也就越少。计算区分度的方法是 `count(distinct column) / count(*)`。 +- **最频繁使用的列放在联合索引的左侧:** 这符合最左前缀匹配原则。将最常用的查询条件列放在最左侧,可以最大程度地利用索引。 +- **字段长度:** 字段长度对联合索引非叶子节点的影响很小,因为它存储了所有联合索引字段的值。字段长度主要影响主键和包含在其他索引中的字段的存储空间,以及这些索引的叶子节点的大小。因此,在选择联合索引列的顺序时,字段长度的优先级最低。 对于主键和包含在其他索引中的字段,选择较短的字段长度可以节省存储空间和提高 I/O 性能。 ### 避免建立冗余索引和重复索引(增加了查询优化器生成执行计划的时间) diff --git a/docs/database/mysql/mysql-index.md b/docs/database/mysql/mysql-index.md index bde4bf5d..ab0dc7f6 100644 --- a/docs/database/mysql/mysql-index.md +++ b/docs/database/mysql/mysql-index.md @@ -155,6 +155,7 @@ B 树也称 B-树,全称为 **多路平衡查找树** ,B+ 树是 B 树的一 - 覆盖索引:一个索引包含(或者说覆盖)所有需要查询的字段的值。 - 联合索引:多列值组成一个索引,专门用于组合搜索,其效率大于索引合并。 - 全文索引:对文本的内容进行分词,进行搜索。目前只有 `CHAR`、`VARCHAR` ,`TEXT` 列上可以创建全文索引。一般不会使用,效率较低,通常使用搜索引擎如 ElasticSearch 代替。 +- 前缀索引:对文本的前几个字符创建索引,相比普通索引建立的数据更小,因为只取前几个字符。 MySQL 8.x 中实现的索引新特性: diff --git a/docs/java/basis/java-basic-questions-01.md b/docs/java/basis/java-basic-questions-01.md index 43bfa1f0..bbd96e8c 100644 --- a/docs/java/basis/java-basic-questions-01.md +++ b/docs/java/basis/java-basic-questions-01.md @@ -97,7 +97,10 @@ JRE 是运行已编译 Java 程序所需的环境,主要包含以下两个部 我们需要格外注意的是 `.class->机器码` 这一步。在这一步 JVM 类加载器首先加载字节码文件,然后通过解释器逐行解释执行,这种方式的执行速度会相对比较慢。而且,有些方法和代码块是经常需要被调用的(也就是所谓的热点代码),所以后面引进了 **JIT(Just in Time Compilation)** 编译器,而 JIT 属于运行时编译。当 JIT 编译器完成第一次编译后,其会将字节码对应的机器码保存下来,下次可以直接使用。而我们知道,机器码的运行效率肯定是高于 Java 解释器的。这也解释了我们为什么经常会说 **Java 是编译与解释共存的语言** 。 -> 🌈 拓展:[有关 JIT 的实现细节: JVM C1、C2 编译器](https://mp.weixin.qq.com/s/4haTyXUmh8m-dBQaEzwDJw) +> 🌈 拓展阅读: +> +> - [基本功 | Java 即时编译器原理解析及实践 - 美团技术团队](https://tech.meituan.com/2020/10/22/java-jit-practice-in-meituan.html) +> - [基于静态编译构建微服务应用 - 阿里巴巴中间件](https://mp.weixin.qq.com/s/4haTyXUmh8m-dBQaEzwDJw) ![Java程序转变为机器代码的过程](https://oss.javaguide.cn/github/javaguide/java/basis/java-code-to-machine-code-with-jit.png) diff --git a/docs/java/collection/java-collection-questions-01.md b/docs/java/collection/java-collection-questions-01.md index f006917e..417a2d10 100644 --- a/docs/java/collection/java-collection-questions-01.md +++ b/docs/java/collection/java-collection-questions-01.md @@ -255,13 +255,12 @@ public interface RandomAccess { 详见笔主的这篇文章: [ArrayList 扩容机制分析](https://javaguide.cn/java/collection/arraylist-source-code.html#_3-1-%E5%85%88%E4%BB%8E-arraylist-%E7%9A%84%E6%9E%84%E9%80%A0%E5%87%BD%E6%95%B0%E8%AF%B4%E8%B5%B7)。 -### 说说集合中的fail-fast和fail-safe是什么 +### 说说集合中的 fail-fast 和 fail-safe 是什么 关于`fail-fast`引用`medium`中一篇文章关于`fail-fast`和`fail-safe`的说法: > Fail-fast systems are designed to immediately stop functioning upon encountering an unexpected condition. This immediate failure helps to catch errors early, making debugging more straightforward. - 快速失败的思想即针对可能发生的异常进行提前表明故障并停止运行,通过尽早的发现和停止错误,降低故障系统级联的风险。 在`java.util`包下的大部分集合是不支持线程安全的,为了能够提前发现并发操作导致线程安全风险,提出通过维护一个`modCount`记录修改的次数,迭代期间通过比对预期修改次数`expectedModCount`和`modCount`是否一致来判断是否存在并发操作,从而实现快速失败,由此保证在避免在异常时执行非必要的复杂代码。 @@ -269,43 +268,38 @@ public interface RandomAccess { 对应的我们给出下面这样一段在示例,我们首先插入`100`个操作元素,一个线程迭代元素,一个线程删除元素,最终输出结果如愿抛出`ConcurrentModificationException`: ```java - ArrayList list = new ArrayList<>(); - CountDownLatch countDownLatch = new CountDownLatch(2); - //添加几个元素 - for (int i = 0; i < 100; i++) { - list.add(i); - } +// 使用线程安全的 CopyOnWriteArrayList 避免 ConcurrentModificationException +List list = new CopyOnWriteArrayList<>(); +CountDownLatch countDownLatch = new CountDownLatch(2); - Thread t1 = new Thread(() -> { - //迭代元素 - for (Integer i : list) { - i++; - } - countDownLatch.countDown(); - }); +// 添加元素 +for (int i = 0; i < 100; i++) { + list.add(i); +} +Thread t1 = new Thread(() -> { + // 迭代元素 (注意:Integer 是不可变的,这里的 i++ 不会修改 list 中的值) + for (Integer i : list) { + i++; // 这行代码实际上没有修改list中的元素 + } + countDownLatch.countDown(); +}); - Thread t2 = new Thread(() -> { - System.out.println("删除元素1"); - list.remove(1); - countDownLatch.countDown(); - }); +Thread t2 = new Thread(() -> { + System.out.println("删除元素1"); + list.remove(Integer.valueOf(1)); // 使用 Integer.valueOf(1) 删除指定值的对象 + countDownLatch.countDown(); +}); - t1.start(); - t2.start(); - countDownLatch.await(); +t1.start(); +t2.start(); +countDownLatch.await(); ``` +我们在初始化时插入了`100`个元素,此时对应的修改`modCount`次数为`100`,随后线程 2 在线程 1 迭代期间进行元素删除操作,此时对应的`modCount`就变为`101`。 +线程 1 在随后`foreach`第 2 轮循环发现`modCount` 为`101`,与预期的`expectedModCount(值为100因为初始化插入了元素100个)`不等,判定为并发操作异常,于是便快速失败,抛出`ConcurrentModificationException`: - - - - -我们在初始化时插入了`100`个元素,此时对应的修改`modCount`次数为`100`,随后线程2在线程1迭代期间进行元素删除操作,此时对应的`modCount`就变为`101`。 -线程1在随后`foreach`第2轮循环发现`modCount` 为`101`,与预期的`expectedModCount(值为100因为初始化插入了元素100个)`不等,判定为并发操作异常,于是便快速失败,抛出`ConcurrentModificationException`: - -![](https://qiniuyun.sharkchili.com/202411172341886.png) - +![](https://oss.javaguide.cn/github/javaguide/java/collection/fail-fast-and-fail-safe-insert-100-values.png) 对此我们也给出`for`循环底层迭代器获取下一个元素时的`next`方法,可以看到其内部的`checkForComodification`具有针对修改次数比对的逻辑: @@ -326,16 +320,13 @@ final void checkForComodification() { ``` - 而`fail-safe`也就是安全失败的含义,它旨在即使面对意外情况也能恢复并继续运行,这使得它特别适用于不确定或者不稳定的环境: - > Fail-safe systems take a different approach, aiming to recover and continue even in the face of unexpected conditions. This makes them particularly suited for uncertain or volatile environments. 该思想常运用于并发容器,最经典的实现就是`CopyOnWriteArrayList`的实现,通过写时复制的思想保证在进行修改操作时复制出一份快照,基于这份快照完成添加或者删除操作后,将`CopyOnWriteArrayList`底层的数组引用指向这个新的数组空间,由此避免迭代时被并发修改所干扰所导致并发操作安全问题,当然这种做法也存缺点即进行遍历操作时无法获得实时结果: -![](https://qiniuyun.sharkchili.com/202411172352477.png) - +![](https://oss.javaguide.cn/github/javaguide/java/collection/fail-fast-and-fail-safe-copyonwritearraylist.png) 对应我们也给出`CopyOnWriteArrayList`实现`fail-safe`的核心代码,可以看到它的实现就是通过`getArray`获取数组引用然后通过`Arrays.copyOf`得到一个数组的快照,基于这个快照完成添加操作后,修改底层`array`变量指向的引用地址由此完成写时复制: diff --git a/docs/java/concurrent/aqs.md b/docs/java/concurrent/aqs.md index 91bcf32f..71a0865a 100644 --- a/docs/java/concurrent/aqs.md +++ b/docs/java/concurrent/aqs.md @@ -333,8 +333,7 @@ semaphore.release(5);// 释放5个许可 [issue645 补充内容](https://github.com/Snailclimb/JavaGuide/issues/645): -> `Semaphore` 与 `CountDownLatch` 一样,也是共享锁的一种实现。它默认构造 AQS 的 `state` 为 `permits`。当执行任务的线程数量超出 `permits`,那么多余的线程将会被放入等待队列 `Park`,并自旋判断 `state` 是否大于 0。只有当 `state` 大于 0 的时候,阻塞的线程才能继续执行,此时先前执行任务的线程继续执行 `release()` 方法,`release()` 方法使得 state 的变量会加 1,那么自旋的线程便会判断成功。 -> 如此,每次只有最多不超过 `permits` 数量的线程能自旋成功,便限制了执行任务线程的数量。 +> `Semaphore` 基于 AQS 实现,用于控制并发访问的线程数量,但它与共享锁的概念有所不同。`Semaphore` 的构造函数使用 `permits` 参数初始化 AQS 的 `state` 变量,该变量表示可用的许可数量。当线程调用 `acquire()` 方法尝试获取许可时,`state` 会原子性地减 1。如果 `state` 减 1 后大于等于 0,则 `acquire()` 成功返回,线程可以继续执行。如果 `state` 减 1 后小于 0,表示当前并发访问的线程数量已达到 `permits` 的限制,该线程会被放入 AQS 的等待队列并阻塞,**而不是自旋等待**。当其他线程完成任务并调用 `release()` 方法时,`state` 会原子性地加 1。`release()` 操作会唤醒 AQS 等待队列中的一个或多个阻塞线程。这些被唤醒的线程将再次尝试 `acquire()` 操作,竞争获取可用的许可。因此,`Semaphore` 通过控制许可数量来限制并发访问的线程数量,而不是通过自旋和共享锁机制。 ### CountDownLatch (倒计时器) diff --git a/docs/java/jvm/jvm-garbage-collection.md b/docs/java/jvm/jvm-garbage-collection.md index 13dd6f09..54c5e13b 100644 --- a/docs/java/jvm/jvm-garbage-collection.md +++ b/docs/java/jvm/jvm-garbage-collection.md @@ -451,7 +451,7 @@ JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX: 从名字中的**Mark Sweep**这两个词可以看出,CMS 收集器是一种 **“标记-清除”算法**实现的,它的运作过程相比于前面几种垃圾收集器来说更加复杂一些。整个过程分为四个步骤: -- **初始标记:** 暂停所有的其他线程,并记录下直接与 root 相连的对象,速度很快 ; +- **初始标记:** 短暂停顿,标记直接与 root 相连的对象(根对象); - **并发标记:** 同时开启 GC 和用户线程,用一个闭包结构去记录可达对象。但在这个阶段结束,这个闭包结构并不能保证包含当前所有的可达对象。因为用户线程可能会不断的更新引用域,所以 GC 线程无法保证可达性分析的实时性。所以这个算法里会跟踪记录这些发生引用更新的地方。 - **重新标记:** 重新标记阶段就是为了修正并发标记期间因为用户程序继续运行而导致标记产生变动的那一部分对象的标记记录,这个阶段的停顿时间一般会比初始标记阶段的时间稍长,远远比并发标记阶段时间短 - **并发清除:** 开启用户线程,同时 GC 线程开始对未标记的区域做清扫。 @@ -468,7 +468,7 @@ JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX: ### G1 收集器 -**G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征.** +**G1 (Garbage-First) 是一款面向服务器的垃圾收集器,主要针对配备多颗处理器及大容量内存的机器. 以极高概率满足 GC 停顿时间要求的同时,还具备高吞吐量性能特征。** 被视为 JDK1.7 中 HotSpot 虚拟机的一个重要进化特征。它具备以下特点: @@ -479,10 +479,10 @@ JDK1.8 默认使用的是 Parallel Scavenge + Parallel Old,如果指定了-XX: G1 收集器的运作大致分为以下几个步骤: -- **初始标记** -- **并发标记** -- **最终标记** -- **筛选回收** +- **初始标记**: 短暂停顿(Stop-The-World,STW),标记从 GC Roots 可直接引用的对象,即标记所有直接可达的活跃对象 +- **并发标记**:与应用并发运行,标记所有可达对象。 这一阶段可能持续较长时间,取决于堆的大小和对象的数量。 +- **最终标记**: 短暂停顿(STW),处理并发标记阶段结束后残留的少量未处理的引用变更。 +- **筛选回收**:根据标记结果,选择回收价值高的区域,复制存活对象到新区域,回收旧区域内存。这一阶段包含一个或多个停顿(STW),具体取决于回收的复杂度。 ![G1 收集器](https://oss.javaguide.cn/github/javaguide/java/jvm/g1-garbage-collector.png) @@ -512,7 +512,11 @@ java -XX:+UseZGC className java -XX:+UseZGC -XX:+ZGenerational className ``` -关于 ZGC 收集器的详细介绍推荐阅读美团技术团队的 [新一代垃圾回收器 ZGC 的探索与实践](https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html) 这篇文章。 +关于 ZGC 收集器的详细介绍推荐看看这几篇文章: + +- [从历代 GC 算法角度剖析 ZGC - 京东技术](https://mp.weixin.qq.com/s/ExkB40cq1_Z0ooDzXn7CVw) +- [新一代垃圾回收器 ZGC 的探索与实践 - 美团技术团队](https://tech.meituan.com/2020/08/06/new-zgc-practice-in-meituan.html) +- [极致八股文之 JVM 垃圾回收器 G1&ZGC 详解 - 阿里云开发者](https://mp.weixin.qq.com/s/Ywj3XMws0IIK-kiUllN87Q) ## 参考