• 欢迎访问本站,本站记录博主日常编程遇到的问题,知识,惊奇软件等。如有问题还请留言


    Deprecated: strip_tags(): Passing null to parameter #1 ($string) of type string is deprecated in /www/wwwroot/gschaos.club/wp-content/themes/Git-alpha-6h0SRk/header.php on line 294

知识整理

置顶 mysticalycc 4年前 (2022-02-13) 1900次浏览 已收录 0个评论
文章目录[隐藏]

基础部分

一、java 基础与语言特性(语法/编译相关)

类加载

🌱 类加载 → 验证 → 准备 → 解析:每一步干了什么?
1. 加载(Loading)
  • JVM 根据类的全限定名(FQCN),通过类加载器读取 .class 文件字节流;

  • 解析 class 文件格式,构造 Class 对象;

  • 读取常量池(此时符号引用还未解析);

  • 得到一个类的内部结构(Class 文件的结构转为 JVM 中的 Class<?> 实例);

    • 📦 1. .class 文件中的结构(编译后)

      当你写了如下 java 类并编译后:

      public class A {
      int x = 1;
      public void doSomething() {}
      }

      编译后生成的 .class 文件是一个平台无关的二进制格式,其中包含了 JVM 执行所需的全部元信息。比如你提到的:

      ✅ 常量池(Constant Pool)

      这是 class 文件中的重中之重,里面存储了:

      常量类型 含义
      UTF-8 字符串 方法名、字段名、类名、描述符
      Class 引用 类或接口的名字
      NameAndType 方法名+方法签名
      Methodref / Fieldref 方法或字段的符号引用
      数值常量 int、float、String 等字面量

      这些是“符号引用”,它们的含义是:“我想引用一个叫 doSomething 的方法,签名是 ()V,定义在类 A 中。”

  • 类的字节流内容在此刻就常驻 JVM 内存中。


2. 验证(Verification)
  • 确保 class 文件合法、安全、不被篡改;
  • 包括文件结构校验、语义校验、字节码验证等;
  • 与直接引用无关。

3. 准备(Preparation)
  • 为类的静态字段分配内存(注意:不是对象字段);
  • 并对非 final 的 static 变量赋默认值(不是赋初值!赋初值在初始化阶段做);
  • 这些字段是在方法区(或 metaspace)中分配的。

这一步主要做的是内存分配,但分配的是“类级别”的静态变量,而不是“对象”。

4. 解析(Resolution)
  • 将常量池中的类符号引用(ClassRef)解析成 java.lang.Class 对象;

  • 将方法符号引用解析为虚方法表中的实际入口;

  • 将字段符号引用解析为字段的内存偏移。

      • 这一阶段会把“常量表 + 方法表”组合起来,生成 JVM 所需的结构。

      • 将 class 文件中的符号引用(Methodref、Fieldref)解析为直接引用,比如指向一个内存地址、vtable slot

      • 建立方法表、虚方法表(vtable)、接口方法表(itable)

      • 建立字段表,记录字段偏移和类型

      • 将常量池转换为 JVM 层的 ConstantPool 对象(runtime constant pool

      • 这些结构组成了在元数据空间(jdk8以后)的InstanceKlass 结构

🌱 一、什么是符号引用?

.class 文件中,java 并不直接使用内存地址或者指针来表示类、字段、方法等之间的关系,而是使用符号引用(Symbolic Reference)来表示。

比如:

Foo foo = new Foo();

在字节码中表示 Foo 的时候,不是直接给一个 Foo 的地址,而是:

  • 在常量池(constant_pool)中记录了类的全限定名(如 com/example/Foo),这就是符号引用。

  • 对方法的调用、字段的访问等,也都通过常量池中的索引来表示,而不是直接的内存引用。

举例:

#17 = Methodref #10.#22 // com/example/Foo."":()V

这里的 #10Class_info,里面记录的是 "com/example/Foo"
#22NameAndType_info,记录的是方法名 "<init>" 和方法描述符 "()V"

这些就是符号引用,它们都是对“名字”的引用,还没有绑定到实际的内存结构上。

  • 🌱 一、什么是符号引用?

    .class 文件中,java 并不直接使用内存地址或者指针来表示类、字段、方法等之间的关系,而是使用符号引用(Symbolic Reference)来表示。

    比如:

    Foo foo = new Foo();

    在字节码中表示 Foo 的时候,不是直接给一个 Foo 的地址,而是:

    • 在常量池(constant_pool)中记录了类的全限定名(如 com/example/Foo),这就是符号引用。
  • 对方法的调用、字段的访问等,也都通过常量池中的索引来表示,而不是直接的内存引用。

举例:

#17 = Methodref #10.#22 // com/example/Foo."":()V

这里的 #10Class_info,里面记录的是 "com/example/Foo"
#22NameAndType_info,记录的是方法名 "<init>" 和方法描述符 "()V"

这些就是符号引用,它们都是对“名字”的引用,还没有绑定到实际的内存结构上。

知识整理


🌿 二、什么是直接引用?

在类被加载、链接之后,JVM 会把这些符号引用解析成“直接引用”:

  • 指向方法区中已经加载好的类结构(如 Class 对象、方法表、字段表);
    • 或者是指向运行时数据结构的地址、偏移量、方法入口等。

直接引用就是可以直接用于运行的内存指针或偏移

解析会:

  • 查找符号引用所指向的类、字段、方法等是否已经被加载;
  • 如果没有,则触发加载;
  • 然后将这些符号引用替换成运行时结构的直接引用

比如:

  • ClassRef → 变成 java.lang.Class 对象的指针;
  • FieldRef → 变成偏移量或者字段表中的地址;
  • MethodRef → 变成方法表中的地址或 native 方法入口。

总结一句话:解析就是让编译期的“名字”,在运行期指向真实的结构


🌾 四、解析举例(图解)

比如下面 java 代码:

Person p = new Person();
p.sayHello();

在字节码中可能是这样:

new #2  // #2 是类 com/example/Person 的符号引用
invokespecial #3 // #3 是方法 <init>:()V 的符号引用
invokevirtual #4 // #4 是 sayHello:()V 的符号引用

在解析阶段:

  • #2 被解析成方法区中的 Person 类结构的指针;
  • #3 被解析成 Person.<init>() 构造函数的入口;
  • #4 被解析成 sayHello() 方法表中的偏移或函数指针。

🌻 五、何时解析发生?

JVM 规范允许解析发生在类加载时,也允许懒解析(即用到再解析)。

JVM 有一些策略:

  • 主动解析:类初始化时把所有常量池里的引用都解析掉;
  • 懒解析(延迟绑定):用到某个符号引用时再解析。这个策略用得比较多(提升启动性能)。

例如调用某个类的静态方法时,如果还没解析该方法符号引用,则会在这个时刻进行解析。\image-20250804162539998.png)


🌿 二、什么是直接引用?

在类被加载、链接之后,JVM 会把这些符号引用解析成“直接引用”:

  • 指向方法区中已经加载好的类结构(如 Class 对象、方法表、字段表);
    • 或者是指向运行时数据结构的地址、偏移量、方法入口等。

直接引用就是可以直接用于运行的内存指针或偏移

解析会:

  • 查找符号引用所指向的类、字段、方法等是否已经被加载;
  • 如果没有,则触发加载;
  • 然后将这些符号引用替换成运行时结构的直接引用

比如:

  • ClassRef → 变成 java.lang.Class 对象的指针;
  • FieldRef → 变成偏移量或者字段表中的地址;
  • MethodRef → 变成方法表中的地址或 native 方法入口。

总结一句话:解析就是让编译期的“名字”,在运行期指向真实的结构


🌾 四、解析举例(图解)

比如下面 java 代码:

Person p = new Person();
p.sayHello();

在字节码中可能是这样:

new #2  // #2 是类 com/example/Person 的符号引用
invokespecial #3 // #3 是方法 <init>:()V 的符号引用
invokevirtual #4 // #4 是 sayHello:()V 的符号引用

在解析阶段:

  • #2 被解析成方法区中的 Person 类结构的指针;
  • #3 被解析成 Person.<init>() 构造函数的入口;
  • #4 被解析成 sayHello() 方法表中的偏移或函数指针。

🌻 五、何时解析发生?

JVM 规范允许解析发生在类加载时,也允许懒解析(即用到再解析)。

JVM 有一些策略:

  • 主动解析:类初始化时把所有常量池里的引用都解析掉;
  • 懒解析(延迟绑定):用到某个符号引用时再解析。这个策略用得比较多(提升启动性能)。

例如调用某个类的静态方法时,如果还没解析该方法符号引用,则会在这个时刻进行解析。

✅ 整体流程理解图示(类 → 对象):
                +-----------------+
  class文件 --> | 符号引用(常量池)|
                +-----------------+
                         ↓ 加载
                +-----------------+
                | Class<?>结构     |  ← 类加载器构造的类元数据结构
                +-----------------+
                         ↓ 准备
                +-----------------+
                | 静态变量内存     |
                +-----------------+
                         ↓ 解析
                +--------------------------+
                | 常量池中的引用 → 指针/入口 |
                +--------------------------+
                         ↓ 初始化
                +--------------------------+
                | 赋初值 / 执行<clinit>()   |
                +--------------------------+
                         ↓ new
                +-------------------+
                | 堆中分配对象空间   | ← 这一步才是创建实例
                +-------------------+

二、JVM 与内存管理

1. 内存结构 & 对象模型


思考

1.volatile 在java中起到lock的作用,就是读取对应内存值的时候不读取寄存器的值,根据mesi协议,如果遇到这个内存的值,是不是一定读取内存的值,而不是直接读取1,2级缓存的值
✅ 一、volatile 是不是等价于 lock?

不是。

特性 volatile synchronized / lock
可见性 ✅ 有 ✅ 有
有序性(禁止指令重排) ✅ 有(只针对该变量) ✅ 更强(全域)
原子性 ❌ 没有 ✅ 有
对应机器指令 lock 前缀指令(如 lock cmpxchg lock 伪指令+monitor

volatile 只保证对该变量的 读写是有序且立即可见的,但不保证原子性。


🧠 二、volatile 的作用是啥?

java 编译器和 JVM 在遇到 volatile 变量时,会生成特殊的汇编指令来保证 内存可见性禁止指令重排序

  • 写操作后,会插入 store-storestore-load 内存屏障(Memory Barrier);
  • 读操作前,会插入 load-loadload-store 屏障。

而这些指令,通常在底层 x86 CPU 上使用类似于:

lock xchg

或者是强制使用 MFENCE/LFENCE/SFENCE 指令。


🔄 三、MESI 协议和缓存一致性

####### ✅ 什么是 MESI?

CPU 的缓存一致性协议,四种状态:

状态 名称 描述
M Modified 缓存为修改态,值未写回内存,独占
E Exclusive 缓存为独占态,值与内存一致,只有自己有
S Shared 缓存为共享态,与其他核心共享,未修改
I Invalid 缓存为无效态,不可读

🎯 你的核心问题:

当有 volatile 修饰变量时,如果某个线程读取这个变量,是不是一定会从主内存读取,而不会使用 L1/L2 缓存?

####### ❌ 答案是否定的 —— 不是一定读主内存


✅ 真相:会读缓存(但要确保是最新的)

####### volatile 并不会让每次访问都跳过缓存直接去主内存。

那样太慢,根本无法接受。

####### 正确做法是:

  • 通过内存屏障 + MESI 协议,确保你读取的缓存内容是「最新的」。

🧵 举个真实场景:

线程 A 写了 volatile x = 1,线程 B 读它。

  1. A 写的时候触发:
    • lock 指令或 store-load barrier
    • 让该缓存行进入 M 状态,并广播 Invalidate 到其他核心,强制 B 的缓存行变为 I 状态。
  2. B 读的时候:
    • 发现自己缓存中 x 的行是 I 状态(Invalid);
    • 触发从其他核心或主内存中拉取最新数据;
    • 拉取后放入自己的缓存行,可能以 S 状态或 E 状态存储。

####### 关键点:

B 最终读取的数据是最新的,但它最终仍然是存在 L1/L2 缓存中的,只是这个数据是有效且可见的。


✅ 总结你的问题答案:
问题 回答
volatile 是否起到 lock 的作用? ❌ 不起到原子性的 lock 作用,只保证可见性和有序性
使用 volatile 变量时是否会跳过缓存? ❌ 不会直接跳过,而是通过缓存一致性机制使缓存行最新
是否每次都从主内存读取? ❌ 否,读取仍是从缓存,但缓存必须是最新有效的(非 Invalid)
volatile 如何实现内存可见性? ✅ 通过内存屏障 + MESI 协议使缓存失效或同步

📌 补充小结(一图概括)
java volatile -> 内存屏障 + 禁止重排序
           ↓
CPU 执行 lock 或 fence 指令
           ↓
使其他核心的缓存行失效(MESI协议)
           ↓
其他线程读取时重新拉取最新值(缓存or内存)
           ↓
✅ 保证可见性,不等于总是读主内存

2.java 内置锁 在锁定静态方法的时候锁定的Class对象,那么Class对象是否存在堆中,如果是,那Class里面的数据是怎样的
1. Class 对象在 JVM 内存中的位置
  • 类元数据(method area / metaspace)
    存放的是:

    • 常量池(runtime constant pool)
    • 字段信息(field metadata)
    • 方法信息(method metadata)
    • 方法字节码(code array)
    • 静态变量(static fields)
    • 类加载器引用、父类引用等结构
  • java 堆中有一个对应的 java.lang.Class 实例
    • 每当 JVM 加载一个类(通过类加载器),都会在上创建一个 Class 类型的对象来代表它。
    • 这个 Class 实例里并不直接保存所有元数据,而是持有一个指针/引用指向元空间中的类结构
    • 程序中 MyClass.classobj.getClass() 拿到的就是这个 Class 对象。

换句话说:

堆中: Class对象(锁的实际对象)
       ↓
元空间:类的结构化信息(字段、方法、常量池等)

2. 锁定静态方法时锁的是哪儿

静态同步方法:

public static synchronized void foo() { ... }

编译后字节码会在 ACC_SYNCHRONIZED 标志位上注明需要同步,执行时:

  • 进入方法前,JVM 会使用 MyClass.class 这个对象作为锁的监视器(monitor)。
  • 因为 Class 对象是堆中的一个普通对象,所以锁定操作是通过堆对象的对象头(mark word)来完成的。
  • 这样同一个类的静态方法竞争时,都是竞争同一个 Class 实例的监视器。

3. Class 对象内部数据

Class 对象的 java 层属性(java.lang.Class)包含:

  • private final ClassLoader classLoader — 关联类加载器
  • private final Class<?> componentType — 如果是数组类,这里存数组元素类型
  • private final int modifiers — 访问修饰符
  • private final String name — 类的全限定名
  • 其他反射用的结构(方法、字段、构造器缓存)

但是这些信息大部分并不是直接存 Java 字段里,而是通过JNI / JVM 本地代码去访问元空间中的类元数据结构(Klass 结构)

Klass 结构(HotSpot 中的实现)里存放:

  • 常量池指针
  • 字段表指针
  • 方法表指针
  • 接口表
  • 类加载器引用
  • 超类引用
  • 静态字段的存储位置指针
  • vtable(虚方法表)

4. 总结
  • Class 对象本身在中,是 Java 程序可以访问到的对象。
  • 它内部的“真正的类定义信息”在元空间(JDK 8+)或方法区(JDK 7 及以前)。
  • 锁定静态方法时,锁的就是堆上的 Class 对象,通过这个对象的对象头实现 monitor 进入/退出。
  • 元数据(方法、字段、常量池等)不在堆上,而是被 Class 对象间接引用。
5.示意图
                ┌───────────────────────┐
                │        java 堆         │
                └───────────────────────┘
                         │
                         ▼
              ┌─────────────────────┐
              │  Class 对象实例      │   ←─  MyClass.class / obj.getClass()
              │  (java.lang.Class)   │
              ├─────────────────────┤
              │ classLoader →───────┼──► ClassLoader 实例
              │ name        → "MyClass"
              │ modifiers   → public
              │ ...         → 反射缓存
              │              (方法/字段信息的引用缓存)
              │
              │ nativePtr   →─────────────┐
              └─────────────────────┘     │
                                          │
                ┌─────────────────────────┘
                ▼
        ┌────────────────────────────┐
        │     元空间 (Metaspace)     │
        └────────────────────────────┘
                   │
                   ▼
        ┌────────────────────────────┐
        │        Klass 结构           │  ← HotSpot 内部类元数据对象
        ├────────────────────────────┤
        │ 常量池指针 (ConstantPool*) │
        │ 字段表(Field Info Table)   │
        │ 方法表(Method Info Table)  │
        │ 接口表(Interfaces)         │
        │ 父类引用(SuperKlass*)      │
        │ 静态字段存储区(Statics)    │
        │ 虚方法表(vtable)           │
        └────────────────────────────┘

解释
    • JVM 为每个被加载的类生成一个 java.lang.Class 对象,存在堆中。
    • 静态方法 synchronized 时,锁的就是这个对象(对象头里的 monitor)。
  1. 元空间 (Metaspace)
    • 存放类的“真正定义”,即 HotSpot 内部的 Klass 结构
    • 包含:常量池、字段信息、方法信息、接口表、继承信息、静态变量的存放地址等。
  2. Class 对象与 Klass 的关系
    • Class 对象里有一个 nativePtr(或类似的 JVM 内部字段),指向元空间中的 Klass 结构。
    • 程序通过 Class 访问方法/字段时,其实是去 Klass 中查找,然后可能缓存到 Class 对象里。
  3. 锁定静态方法时
    • JVM 找到 Class 对象(堆中实例),对它的 对象头(mark word) 做 monitor enter/exit 操作。
    • 不会直接锁元空间里的 Klass 结构。

2. GC 垃圾回收

思考:jvm在做垃圾回收的时候使用的是GC Root的方式,那么能够作为GC Root的对象或者引用是在执行GC的时候遍历得到的还是存在某个地方
GC Roots 的集合本身是 JVM 内部固定管理的一些特殊引用点,它们一直都“存在于某些特定区域中”,在 GC 开始时作为起点被遍历。

一、哪些是 GC Roots?

GC Roots 并不是“运行时动态计算出来的”,而是 JVM 在设计时就规定好的一些特殊位置中的引用,只要对象被这些引用直接或间接关联,就会被认为是“可达对象”。

常见的 GC Roots 包括:

类型 说明
栈帧中的本地变量表(Local Variables) 所有线程正在执行的方法调用栈中,局部变量表引用的对象。
方法区中的类静态属性引用的对象 比如 static 引用的对象。
方法区中常量引用的对象 比如字符串常量池中的引用。
本地方法栈中 JNI 引用的对象 native 方法使用的对象引用。
虚拟机内部的一些系统类引用的对象 比如 ClassLoader、线程对象等。
被同步锁(monitor)持有的对象 即进入 synchronized 的 monitor 对象。

这些地方都是 JVM 内部结构中预定义的“根节点集合”,它们

  • 不会被 GC 回收
  • 在 GC 开始时 由 GC Root Set 构成“起始点”,从这些点开始做“可达性分析”。

二、GC Root 本身是怎么被 JVM 管理的?

它们 不是“临时生成”或“遍历所有对象得出” 的,而是:

  • 存在于 JVM 内部的线程栈、方法区、堆栈边界、JNI 表等结构中;
  • JVM 一开始就知道哪里能找到这些引用点;
  • 比如每个线程的栈帧、方法区中的静态表、常量池表等,都是固定结构,JVM 实现里知道如何访问。

三、GC 的过程简化流程:
  1. 构建 GC Root Set:从 JVM 结构中直接获取“GC Roots”集合,比如所有线程的栈帧、本地方法引用等。
  2. 进行可达性分析(mark):从这些 GC Root 出发,遍历引用链,把所有可达对象打上标记。
  3. 清理不可达对象(sweep):没有被标记的就会被认定为垃圾,进行回收。

四、一个形象类比:

可以把 GC Roots 理解为一个“森林里所有的井口”,这些井口是 JVM 早就规划好的,GC 时从这些井往下打水(遍历对象图),能接到水的对象是活的,接不到的就是垃圾。


总结:
  • GC Root 并不是遍历所有对象得到的,而是 存在于 JVM 固定结构中的引用点集合
  • JVM 在 GC 时 直接访问这些已知的位置,作为遍历起点。
  • GC 的“可达性分析”是从这些 GC Root 出发,构建出“活对象集合”的过程。

3. JVM 工具 & 调优实例

👉 延伸阅读: Arthasarthas使用

4. 高并发相关

三、并发与ThreadLocal机制

🧵 ThreadLocal 原理与内存泄漏机制

ThreadLocal 是 java 提供的一种线程隔离机制,它通过为每个线程维护变量的副本,实现变量在多线程中的独立性和线程安全性,无需显式加锁。

✅ 原理概述:线程实例副本

每个线程 Thread 对象内部都维护着一个 ThreadLocalMap 实例,该 Map 的 key 是 ThreadLocal 对象本身,value 是线程为该变量存储的副本值。因此:

  • 不同线程访问同一个 ThreadLocal 变量,实际访问的是不同线程中的独立副本;
  • 各线程之间相互隔离,互不干扰。

🚨 为什么会内存泄漏?如何解决?

由于 ThreadLocalMap 的 key 是对 ThreadLocal弱引用(WeakReference),一旦没有外部强引用指向某个 ThreadLocal 实例,它就会被 GC 回收。

ThreadLocalMap.Entry 中的 value 是强引用,所以:

当 key 被回收后,value 仍被当前线程持有,无法被 GC 清除 —— 这就导致了内存泄漏

🔍 示例:ThreadLocalMap 的 set() 方法

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(); // 弱引用 get()
    if (k == key) {
      e.value = value; // 更新
      return;
    }
    if (k == null) {
      // key 已被 GC 回收,此时 Entry 已过期,进行替换清理
      replaceStaleEntry(key, value, i);
      return;
    }
  }

  tab[i] = new Entry(key, value); // 新增 Entry
  int sz = ++size;
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash(); // 扩容并触发过期 Entry 的清理
}

💡 如何防止内存泄漏?

ThreadLocalMap 通过以下机制自动清理过期 Entry:

  1. 弱引用机制:key 被 GC 回收后,get() 返回 null,可识别为过期;
  2. 清理策略
    • replaceStaleEntry():替换和清除已过期的 Entry;
    • cleanSomeSlots()expungeStaleEntry():批量扫描并清理无效 Entry;
  3. 建议: 显式调用 ThreadLocal.remove() 清除变量,尤其在使用线程池等线程复用场景中,避免数据残留。

📍 使用场景一:每个线程需要一个独立的实例

例如数据库连接、用户上下文、事务信息等需要线程隔离的数据。ThreadLocal 提供了优雅的方式,无需每次手动创建实例。

📍 使用场景二:线程内多方法共享数据,但线程间隔离

在框架设计中非常常见,如:

  • Spring 中的 RequestContextHolder
  • MyBatis 中的事务管理器
  • 日志记录中的 traceId 传递

使用 ThreadLocal 可避免通过方法参数层层传递,提升代码可读性与复用性。


🧠 总结

特性 说明
隔离性 每个线程有独立副本,互不影响
弱引用 key 是 WeakReference,防止强引用泄漏
内存泄漏风险 key 回收但 value 未清理,需主动调用 remove()
应用场景 用户上下文、数据库连接、traceId 等线程独立数据

ThreadLocal 是一种优秀的线程封装机制,但如果使用不当,容易引发隐式内存泄漏问题。理解底层结构与使用约束,有助于更安全、高效地应用它。


java 静态锁的实现机制:Class 对象作为锁标记

✅ 本质:使用 Class 对象作为锁

在 java 中,静态同步方法或静态代码块的锁对象是对应类的 Class 对象

示例说明:

public class Demo {
    public static synchronized void staticMethod() {
        // synchronized(Demo.class)
    }
}

等价于:

synchronized(Demo.class) {
    // 静态方法锁,作用于类级别
}

✅ 锁粒度说明:

  • 静态方法加锁 → 锁的是类(Class 对象) → 全部实例共享;
  • 实例方法加锁 → 锁的是当前对象实例 → 每个实例互不影响;

Class 对象是类加载到 JVM 时创建的唯一对象,天然具备互斥标识能力,适合作为静态锁。


wait()方法为什么要放在Object类中

简单说:因为synchronized中的这把锁可以是任意对象,所以任意对象都可以调用wait()和notify();所以wait和notify属于Object。

专业说:因为这些方法在操作同步线程时,都必须要标识它们操作线程的锁,只有同一个锁上的被等待线程,可以被同一个锁上的notify唤醒,不可以对不同锁中的线程进行唤醒。

也就是说,等待和唤醒必须是同一个锁。而锁可以是任意对象,所以可以被任意对象调用的方法是定义在object类中


为什么 MESI 协议并不足以保证 java 中的“可见性”语义?


四、网络与I/O相关


五、设计模式


六、JVM相关图解 & 书籍笔记

  • JVM 图解

知识整理


Spring系列

1. IoC 原理与实现机制详解

Spring 的 IoC(控制反转)机制核心在于:通过容器统一管理对象的生命周期和依赖关系,实现应用层与对象创建过程的解耦。

整个 IoC 容器的初始化和 Bean 的创建过程主要包括以下几个关键阶段:


(1)创建容器实例

Spring 首先通过 BeanFactory 或其实现类(如 DefaultListableBeanFactory)创建一个核心容器,负责管理 Bean 的注册、创建与销毁等。


(2)注册 BeanDefinition(元数据)

Spring 会通过注解或 XML 配置,将每个 Bean 的元数据信息(BeanDefinition)注册到容器中。

这些元数据包括:

  • Bean 的类类型
  • 是否为单例
  • 依赖的其他 Bean 名称
  • 初始化和销毁方法等

(3)配置 BeanFactory 的扩展能力

接下来,容器会执行一系列 BeanFactoryPostProcessor 接口的实现类,用于在 Bean 实例化之前BeanDefinition 做进一步的处理。

例如:

  • PropertySourcesPlaceholderConfigurer:处理 ${} 占位符,加载配置文件;
  • ConfigurationClassPostProcessor:处理 @Configuration@ComponentScan 等注解,注册额外的 Bean。

这一步非常关键,它实现了容器的“自我增强”和动态 Bean 注册。


(4)注册 BeanPostProcessor(对象级扩展)

在实例化 Bean 之前,容器会注册一系列 BeanPostProcessor 实例,这些处理器负责在 Bean 初始化前后做一些增强工作,如:

  • Aware 接口(如 BeanNameAware, ApplicationContextAware)注入容器上下文;
  • 自动代理(如 AOP);
  • @Autowired/@Value 等依赖注入注解解析。

(5)实例化 Bean(反射构造)

此时容器会使用反射(调用构造器)将 BeanDefinition 转换为具体的 Bean 实例。

如果有构造函数参数或依赖注入,也会在这一步完成解析并注入。


(6)初始化过程(增强与生命周期回调)

在 Bean 被构造完成后,Spring 会依次执行以下步骤完成初始化:

  1. 属性填充(依赖注入)
  2. 调用 Aware 接口(注入容器信息)
  3. 调用 BeanPostProcessor.postProcessBeforeInitialization()
  4. 调用初始化方法(如 @PostConstructInitializingBean.afterPropertiesSet()
  5. 调用 BeanPostProcessor.postProcessAfterInitialization()

(7)完成 Bean 的注册

容器将创建完成、初始化完毕的 Bean 缓存到容器中(如单例池),完成一次完整的生命周期初始化。


(8)销毁流程(容器关闭时)

当容器关闭时,会:

  • 调用 DisposableBean.destroy()
  • 或执行用户配置的 @PreDestroy / destroy-method
  • 清理 Bean 缓存及相关资源。

2. Spring Boot 启动时加载热点数据到 Redis 的实现机制

在理解了 IoC 原理之后,我们可以进一步讨论一个非常实际的场景:如何在 Spring Boot 启动时将数据库中的热点数据加载到 Redis 中

这个问题看似简单,实际上需要准确掌握 Spring 容器启动流程和 Bean 生命周期的执行顺序。特别是要确保:

加载 Redis 的逻辑在数据源已初始化之后,并且尽可能晚一点执行,避免依赖未准备好。


2.1 常见实现方式及时机选择

机制 / 注解 执行时机 是否适合加载数据
@PostConstruct Bean 创建完成后立即执行 ❌ 数据源可能未准备好
InitializingBean.afterPropertiesSet() @PostConstruct 相同 ❌ 同样过早
ApplicationRunner / CommandLineRunner 所有 Bean 初始化完成后,容器启动最后阶段 ✅ 最推荐
@DependsOn 指定依赖的 Bean 必须提前实例化 ✅ 控制顺序辅助用
@Order Runner 执行顺序控制(数字越小越先执行) ✅ 多个 Runner 时使用
延迟 Bean 注入 在执行逻辑中通过注入方式触发依赖实例化 ✅ 防止提前初始化

2.2 推荐方式:使用 ApplicationRunner 加载热点数据

@Component
@Order(1) // 决定多个 Runner 的执行顺序
public class CachePreloadRunner implements ApplicationRunner {

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private HotDataService hotDataService;

    @Override
    public void run(ApplicationArguments args) throws Exception {
        List<HotItem> hotItems = hotDataService.loadFromDb();
        for (HotItem item : hotItems) {
            redisTemplate.opsForValue().set(item.getKey(), item);
        }
        System.out.println("热点数据已加载到 Redis");
    }
}

ApplicationRunnerCommandLineRunner 的唯一区别是参数类型不同,前者提供解析后的参数结构。


2.3 使用 @DependsOn 控制初始化顺序(可选)

如果你的热点数据加载逻辑依赖某个特定的 Bean(如数据源、配置中心初始化),可以配合使用 @DependsOn 来确保顺序:

@Component
@DependsOn("dataSource")
public class CachePreloadRunner implements ApplicationRunner {
    // ...
}

2.4 加深理解:它与 IoC 有何关系?

Spring Boot 启动时的整个数据加载流程,其实是 IoC 控制的一个典型延伸

  • 所有对象(如 RedisTemplate、Service、DataSource)都交由容器统一创建;
  • 加载逻辑的执行时机,也不是我们主动调用,而是容器在合适时机反转控制权来执行
  • 每一步都体现出容器主导 → 应用被动响应的 IoC 精神。

👉 延伸阅读:


3. Spring 中 Bean 的完整生命周期解析

在 Spring 容器的管理下,每一个 Bean 的创建、初始化、使用直至销毁都遵循着一套明确的生命周期流程。理解这一过程对于掌握 IoC 容器的原理和扩展点至关重要。

下面是 Spring 容器中 Bean 生命周期的完整流程解析:


(1)实例化 Bean(Instantiation)

Spring 使用反射机制调用无参构造方法或指定构造器,创建一个空的 Bean 实例。此时对象已经存在于内存中,但尚未注入任何属性。

  • 关键方法instantiateBean() 或构造器反射
  • 目的:创建原始对象,不带依赖和配置

(2)属性填充(Dependency Injection)

容器调用 populateBean() 方法,根据 BeanDefinition 中的依赖信息为 Bean 注入属性:

  • 注入其他 Bean(构造函数 / setter / 字段)
  • 注入配置值(如 @Value
  • 此步骤中,Spring 会使用 三级缓存机制 处理 循环依赖,即:
    1. 一级缓存:单例池(完全初始化好的 Bean)
    2. 二级缓存:早期暴露的 Bean(提前暴露的对象引用)
    3. 三级缓存:用于创建代理对象(如 AOP)

循环依赖能被解决的前提:必须是“单例 + 非构造器注入”


(3)调用 Aware 接口(容器感知)

如果 Bean 实现了以下接口之一,容器会在此阶段调用对应的方法:

  • BeanNameAware:注入当前 Bean 的名称
  • BeanClassLoaderAware:注入类加载器
  • BeanFactoryAware:注入 BeanFactory 引用

这些接口提供了 让 Bean 感知自身在容器中的运行环境 的能力。


(4)执行 BeanPostProcessor 的前置处理

在 Bean 初始化之前,Spring 会遍历所有实现了 BeanPostProcessor 接口的类,执行其 postProcessBeforeInitialization() 方法。

此处可以做很多事情,例如:

  • 注入 ApplicationContextEnvironment 等核心容器信息(由 ApplicationContextAwareProcessor 实现)
  • 自定义处理逻辑,比如修改属性、记录日志等

(5)执行初始化逻辑

Spring 会检查 Bean 是否实现了以下两种初始化方式:

  1. 是否实现 InitializingBean 接口,如果有,则调用其 afterPropertiesSet() 方法;
  2. 是否配置了自定义 init-method,若有则反射调用。

通常推荐使用 @PostConstruct 注解作为标准初始化入口。


(6)执行 BeanPostProcessor 的后置处理(核心扩展点)

这是最重要的扩展点之一。

Spring 会调用所有 BeanPostProcessorpostProcessAfterInitialization() 方法。此阶段是 Spring AOP 织入代理的关键位置,如:

  • AbstractAutoProxyCreator 及其子类会通过 wrapIfNecessary() 判断是否需要为当前 Bean 创建代理对象;
  • 如果需要,会使用 JDK 或 CGLIB 生成动态代理对象替换原始 Bean。

这一步决定了我们平时看到的 @Transactional@Async 等是否生效。


(7)完成初始化,放入容器

此时 Bean 已被完全初始化,容器会将其加入单例池(如果是 singleton),随后可以通过 getBean() 正常获取。


(8)销毁(Destruction)

当容器关闭(如 Spring 应用关闭、Context 关闭)时,会执行 Bean 的销毁流程。

Spring 会依次检查:

  • 是否实现 DisposableBean 接口,调用其 destroy() 方法;
  • 是否配置了 destroy-method,则反射调用;
  • 是否使用 @PreDestroy 注解标记了销毁方法。

此外,还会调用注册在 DestructionAwareBeanPostProcessor 中的销毁回调钩子。


🔁 生命周期流程简图

实例化
  ↓
依赖注入(populateBean)
  ↓
Aware 接口回调
  ↓
BeanPostProcessor 前置处理
  ↓
初始化(init-method / afterPropertiesSet)
  ↓
BeanPostProcessor 后置处理(AOP代理)
  ↓
完成初始化 → 加入单例池 → getBean
  ↓
(容器关闭时)
  ↓
销毁方法执行

知识整理

知识整理

知识整理


4. Spring 如何解决循环依赖?

在 Spring 中,循环依赖是指 Bean A 依赖 Bean B,而 B 又依赖 A,形成一个“闭环”。如果不加以处理,系统在初始化 Bean 时就会陷入死循环。Spring 通过 三级缓存机制 有效解决了“非构造器注入的循环依赖问题”。

4.1 循环依赖的关键点

  • 场景:A → B → A,setter 或字段注入时触发
  • 限制:只能解决 singleton 且非构造器注入的依赖
  • 核心机制:三级缓存 + 提前暴露 + AOP 延迟代理

4.2 三级缓存详解

知识整理

Spring 在 DefaultSingletonBeanRegistry 中维护三层缓存,分别管理 Bean 的不同状态:

缓存名称 描述
singletonObjects(一级) 完整初始化后的 Bean 实例
earlySingletonObjects(二级) 允许提前暴露的“不完整 Bean”(未被 AOP 代理)
singletonFactories(三级) 用于延迟创建 Bean 实例的 ObjectFactory,可用于创建代理对象

4.3 生命周期中三级缓存的作用流程:

  1. createBeanInstance 阶段:将 ObjectFactory 放入三级缓存;
  2. populateBean 阶段:若依赖对象未完成初始化 → 从三级缓存获取对象 → 判断是否需要 AOP → 放入二级缓存;
  3. initializeBean 阶段:完成初始化后 → 放入一级缓存,清除二级、三级缓存。

ObjectFactory 是一个函数式接口,用于延迟创建 Bean,有利于解决代理对象还未生成但又需要引用自身的情况。

👉 延伸阅读:


5. Spring AOP 的实现原理

Spring 的 AOP 是 IOC 容器的自然拓展,依赖于 Bean 的生命周期,在创建过程中通过 动态代理机制 实现方法级别的增强。

5.1 AOP 主要机制:

  • 切面(Aspect):横切逻辑,如日志、事务、安全等
  • 切点(Pointcut):定义在哪些方法上增强
  • 通知(Advice):增强逻辑
  • 织入(Weaving):增强逻辑与目标类的结合过程

5.2 关键实现流程:

  • Bean 初始化过程中,AbstractAutoProxyCreator 作为 BeanPostProcessor 的实现类,在 postProcessAfterInitialization() 中执行代理逻辑;
  • 判断该 Bean 是否符合切点 → 如果是,使用 JDK 或 CGLIB 生成代理;
  • 代理执行逻辑由 DynamicAdvisedInterceptorintercept() 方法驱动。

👉 延伸阅读:


6. Spring 事务的回滚原理

Spring 的声明式事务功能本质上是通过 AOP 实现的,其核心实现类是 TransactionInterceptor,用于拦截事务方法并进行事务控制。

6.1 执行流程如下:

  1. 拦截方法调用
  2. 获取事务属性配置(如传播级别、回滚规则等)
  3. 开启事务(关闭自动提交,保存状态)
  4. 执行目标方法
  5. 方法抛异常时回滚doRollback(),通过 completeTransactionAfterThrowing() 实现;
  6. 方法正常返回时提交doCommit(),通过 completeTransactionAfterReturning() 实现;
  7. 清理资源cleanupTransactionInfo(),解绑线程变量

7. 事务传播机制(Propagation)

Spring 允许定义方法间事务行为的传播方式,控制事务边界的传递与隔离,常见传播行为包括:

  • REQUIRED:默认,当前有事务则加入,没有则新建
  • REQUIRES_NEW:无论是否有事务,都新建一个
  • NESTED:嵌套事务,依赖保存点实现回滚
  • SUPPORTSNOT_SUPPORTED

事务传播行为是多模块协作时的重要保障,避免事务混乱或丢失。


8. BeanFactory 与 FactoryBean 的区别

对比项 BeanFactory FactoryBean
作用 Spring 的核心容器接口 用于用户自定义生成 Bean 的工厂类
复杂性 高,涉及完整生命周期 简洁,适合封装复杂对象创建逻辑
使用场景 IoC 容器顶层控制接口 封装第三方对象(如 MyBatis 的 SqlSessionFactory)
接口方法说明 getObject()isSingleton()getObjectType()

9.spring 注入

知识整理

10.Spring请求过程

知识整理


11. Spring 中常见设计模式

Spring 本质上是“设计模式的集大成者”,框架本身大量使用并暴露以下经典设计模式:

设计模式 应用示例
单例模式 所有默认 Bean 为单例,统一管理实例
工厂模式 BeanFactory、FactoryBean
模板方法 AbstractBeanFactory 定义创建流程并允许拓展
观察者模式 ApplicationEvent + Listener
策略模式 PropertyEditor、Resource 解析器
适配器模式 HandlerAdapter(Spring MVC 请求适配)
代理模式 JDK / CGLIB 动态代理,用于实现 AOP 与事务

12. 多系统数据库字段加密的实践方案

在实际开发中,可能会遇到以下场景:多个系统共用或独立的数据库中存在敏感字段(如身份证、手机号、银行卡号等),需要在不影响业务功能的前提下完成批量加密。推荐使用 MyBatis 的插件机制进行无侵入式处理。

✅ 技术方案:MyBatis 拦截器

  • 核心思路:在执行 INSERTUPDATE 操作时拦截 SQL 参数,对敏感字段进行加密处理;
  • 实现方式
    1. 实现 Interceptor 接口;
    2. 拦截 Executor.update() 方法;
    3. 通过 MetaObject 操作参数对象中的字段;
    4. 判断字段是否需要加密,如 @Sensitive 注解标识;
    5. 使用 AES、RSA 等算法加密;
    6. 替换入参后继续执行。

🧩 注意事项:

  • 建议配合字段注解 + 加密组件(如 Hutool、Jasypt);
  • 大批量加密推荐脱离业务主流程,在启动时批处理或工具脚本;
  • 解密可以在查询拦截器中处理,或者封装在 service 层逻辑中。

🌐 服务发现与远程调用

在微服务架构中,各个服务往往是解耦部署的,需要通过 HTTP 或 RPC 方式相互调用。Spring Cloud 提供了多个组件来支撑这一能力,典型代表包括:Feign(声明式调用)、Ribbon/LoadBalancer(负载均衡)、Sentinel(熔断限流)等。


配置中心架构设计:Pull vs Push

在大型系统中,配置中心承担着下发和统一管理配置的职责,Pull 与 Push 是两种核心的下发模型,各有适用场景。

✅ Pull(拉取)模型:

  • 特点:客户端主动定时或按需从配置中心获取配置;
  • 优点
    • 实现简单,易于控制;
    • 容错性强:服务端宕机不会影响已获取配置;
  • 缺点
    • 不实时;
    • 配置更新后需要等待下一个周期才能生效。

✅ Push(推送)模型:

  • 特点:配置中心在配置变更后立即推送到客户端;
  • 优点
    • 实时性强,配置修改可秒级生效;
  • 缺点
    • 服务端压力大;
    • 容错性差,客户端需维护长连接、重连等逻辑。

✅ 选择依据:

维度 推荐模型
配置变更频繁、需要秒级生效 Push
系统规模较小、变更少 Pull
高并发系统(如支付) Pull + 本地缓存
混合方案(如 Nacos) 支持 Push + Failover Pull

实践建议:在设计配置中心时,可以采用 Push 为主、Pull 为辅(兜底) 的混合模式,兼顾实时性与稳定性。


🔗 @FeignClient:声明式 HTTP 客户端

上一节已详细介绍 @FeignClient 注解,这里作为模块首节重点展开,帮助读者理解如何通过注解实现“远程服务调用的本地封装”。

关键特性:

  • 自动构建接口代理对象
  • 自动集成负载均衡与熔断(如果引入 Ribbon、Sentinel)
  • 可通过 fallback/fallbackFactory 进行容错处理
  • 支持 URL 调试与配置解耦

🎯 负载均衡机制:Ribbon vs LoadBalancerClient

✅ Ribbon(已过时):

  • Netflix 提供的客户端负载均衡器,已停止维护;

  • 被 Spring Cloud LoadBalancer 模块所替代;

  • 配合 Feign 使用非常常见(早期 Feign 默认集成 Ribbon);

  • 核心配置包括:

    user-service:
    ribbon:
      NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule

✅ Spring Cloud LoadBalancer(推荐):

  • Spring 官方提供的新一代客户端负载均衡组件;
  • 依赖 spring-cloud-starter-loadbalancer
  • 支持更强扩展性,基于 Reactor 构建,支持响应式编程;
  • Feign 在 Spring Cloud 2020+ 中默认集成它而非 Ribbon。

🛡️ Sentinel:熔断限流保护

Sentinel 是阿里开源的流量控制组件,集成到 Feign 可实现远程调用的熔断与自定义处理。

整合方式:

  1. 引入依赖:

    
     com.alibaba.cloud
     spring-cloud-starter-alibaba-sentinel
    
  2. 在配置文件开启支持:

    feign:
     sentinel:
       enabled: true
  3. 实现 fallbackfallbackFactory 来提供熔断降级逻辑。

  4. Sentinel 控制台可用于实时观察流量与熔断策略。


📋 调用过程总览

应用启动
 ↓
FeignClient 代理创建(动态代理)
 ↓
封装为 HTTP 请求(RestTemplate/WebClient)
 ↓
由 LoadBalancerClient 选择服务实例
 ↓
发起请求 → 服务响应
 ↓
异常? → Sentinel 接管熔断 → fallbackFactory 执行

👉 延伸阅读:


☁ Nacos vs. Eureka:服务注册与发现对比

🧩 1. 核心功能对比

特性 Nacos Eureka
服务注册与发现 ✅ 支持 ✅ 支持
配置中心 ✅ 内置,支持动态配置 ❌ 需要借助 Spring Cloud Config
健康检查 ✅ 支持 TCP/HTTP/MySQL ✅ 支持(基于心跳机制)
注册信息一致性 支持 AP 和 CP 模式选择 AP(最终一致性)
动态DNS ✅ 内建 DNS-Based 服务发现 ❌ 不支持
UI 控制台 ✅ 直观强大 ✅ 简洁,功能有限
社区 & 维护 阿里开源,活跃维护中 Netflix 已停止维护(2.x 后弃用)
支持协议 HTTP/gRPC、Dubbo、REST HTTP(Spring Cloud 封装)

🔍 2. 原理上的核心区别

✅ Eureka 原理:
  • 采用 AP(Availability + Partition tolerance) 模型;
  • 注册中心不强制数据一致性,依赖服务端与客户端之间的心跳机制;
  • 客户端缓存注册表,即使注册中心短暂不可用,也能完成服务调用(容错性强);
  • Spring Cloud 封装使用最广。

知识整理

✅ Nacos 原理:
  • 支持 AP 和 CP 两种一致性协议模式(默认 AP,选项可切);
  • 使用 raft/CP 模式下配置和服务数据会强一致;
  • 集成配置中心功能,一站式解决注册与配置问题;
  • 支持多协议(如 gRPC、Dubbo、HTTP)服务注册发现;
  • 提供 命名空间、分组、权重、标签等更丰富的管理功能

👉 延伸阅读:Nacos


🧪 3. 实践中的选型建议

场景/需求 推荐选型
项目小型,Spring Cloud 全家桶 Eureka(快速轻便)
配置中心 + 注册中心统一管理 Nacos
使用 Dubbo 或多语言通信场景 Nacos
需要强一致性(如金融) Nacos(CP 模式)
要求极高的高可用性容错 Eureka(AP 容忍断连)
想要统一注册 + 配置 +动态权重等功能 Nacos
不再使用被弃用组件 推荐 Nacos(Eureka 已停止维护)

数据库

关系数据库

Mysql 一图流

图中Undo Log不会持久化的说法是错误的

我们知道undo 是作为MVCC实现必不可缺少的一部分,在RR,RC级别下要实现MVCC必须使用Undo Log记录回滚数据链,根据这篇文章RR级别下的幻读问题我们知道Mysql修改数据是原始数据行上修改,利用Undo Log来回滚数据的,所以Undo Log有必要持久化才能防止出现问题。

  1. 事务崩溃恢复需要它
    如果事务执行到一半崩溃(比如断电),在事务还没提交的情况下,MySQL 重启时需要 undo log 来回滚这些未完成的事务。
  2. MVCC 的一致性读需要它
    一个事务在执行时,可能需要访问旧版本的行(历史版本链)。这些历史版本就存储在 undo log 中(准确说是在 undo tablespace 中的回滚段)。
    如果 undo log 不落盘,长事务或系统崩溃后,历史版本就丢了,MVCC 就无法工作。

    知识整理


非关系型数据库


微服务技术相关

基础类库架构

知识整理


HTTPS

知识整理


SpringCloud


链路追踪实现原理


熔断与降级

Nginx


虚拟化&容器&调度

Docker


K8S

拓展阅读

netty内存管理


拓展问题

我的一个软件发起了一次http请求,这个请求最终收到了返回,但是被调用的服务被执行了三次,我不小心被调用服务没有做幂等,但是我的调用服务日志显示只调用了一次,我想问如果我使用的是httpclient调用,它在调用内核方法发起网络请求超时会自己重试么

单靠应用层日志无法解释多次调用的问题

你的应用可能只发起了一次:

logger.info("准备发送请求");
httpClient.execute(request); // 你以为只发了一次
logger.info("请求结束");

但:

  • 请求发出去了,响应没收到;
  • HttpClient 底层判断异常(如超时),再发;
  • 你应用层日志“看不见”第二次发请求的动作,除非你显式开启了底层日志或抓包

    必须结合内核/网络层/传输层等才能定位根因

    你的请求路径可能经历了这些阶段:

    HttpClient → Java Socket API → OS TCP Stack → 网卡驱动 → 网线 → 路由 → 服务端 → 应用 → 响应返回

    可能出问题的地方很多:

层级 可能现象 对你不可见
Java Socket connect 成功但写入后超时 你以为没发
OS TCP 请求包已发,响应丢失 你没收到 response
网络链路 丢包、抖动、代理层 timeout 重放 服务被多次触发
服务端 没做幂等,重复执行业务逻辑 非预期副作用
客户端 自动重试,服务多次执行 应用以为只一次
如果是偶发现象,很大概率不是你的应用代码直接引起的

这是关键:

  • 只出现 极低概率、多数时候正常 的情况;
  • 多半是某层发生了 “可恢复性错误”或“轻微的不一致”
  • 导致了 下游服务副作用执行多次

    也就是说,你的应用代码“逻辑上”没问题,但没有做好 网络异常下的防御设计(幂等性 + retry 控制),才让这种偶发问题造成真实伤害。

    你该怎么应对这种问题?
措施 描述
✅ 服务端做幂等性设计 如插入前校验是否已存在、幂等Token、防重复提交
✅ 客户端关闭 POST 重试机制 或者只重试 GET
✅ 所有调用加 traceId(全链路日志) 用于定位哪一次请求出问题
✅ 使用抓包 + 网络日志工具 如 Wireshark、tcpdump、-Djavax.net.debug=all
✅ 使用熔断 + 重试机制而非隐式 retry 比如 Hystrix / Resilience4j 等有“请求隔离”的中间件
✅ 配置合理 timeout 避免超时过早导致不必要的重试

Mysql和Pgsql在隔离级别是RR的时候策略是不同的,或者说它们本来实现MVCC的逻辑就不一样,Mysql使用的undo log,Pgsql使用的元组(我不知道我说的对不对),当Mysql做修改的时候修改的原始数据,根据undo Log来实现事务,而Pgsql是新增元组来实现的,所以Pgsql允许多个修改同时进行(不知道这里对不对),而Mysql一个修改操作会阻塞其他所有的修改操作。我这些理解正确么?

✅ 一、MVCC 的基本差异
项目 MySQL (InnoDB) PostgreSQL
MVCC 实现方式 通过 Undo Log 回滚段(undo segment) 通过元组版本(Tuple Versioning)
版本存储位置 Undo Log + 数据页(数据页中仅存当前版本) 每次更新生成新的 Tuple,旧版本也保留在表中
事务视图(snapshot) 基于当前事务启动时的 Undo Log 生成 基于当前事务的 TxID Snapshot

✅ 二、你说的几个重点解释
1. 「MySQL 使用 undo log,PostgreSQL 使用元组」

完全正确

  • MySQL/InnoDB:每条记录只有一个当前版本,旧版本通过 undo log 存在于 rollback segment 中,读取时从 undo log 重建旧版本。
  • PostgreSQL:更新/删除会产生新版本(tuple),老版本仍存在(直到被 vacuum 清理),每个版本都有 xmin/xmax 来标示其生命周期。

2. 「MySQL 修改是修改原始数据,PgSQL 是新增元组」

✅ 也是正确的说法,但再具体一点:

  • MySQL(InnoDB)
    • 对一条记录进行更新时,是在原地修改当前记录,并生成对应的 undo log,用于快照读取和回滚。
    • 所以,写操作需要先拿到锁,防止写写冲突。
  • PostgreSQL
    • 更新时是插入一条新记录(新 tuple),旧的 tuple 保留,并打上 xmax 表示它被谁删除。
    • 这样就避免了多个事务读取/写入同一条记录的直接冲突(MVCC 下读写不阻塞,写写冲突才处理)。

3. 「PgSQL 允许多个修改同时进行,MySQL 会阻塞所有修改」

✅ 你的结论部分正确但需要澄清

####### ✅ PostgreSQL 的写操作并不是完全“并发”地都能成功:

  • PostgreSQL 允许多个事务尝试并发更新同一行数据,但最终只有一个事务能成功,其它会 检测到冲突并回滚 或进入重试(通过序列化器或 application 逻辑)
  • 所以虽然它看起来允许“多个修改同时进行”,但 最终只有一个生效,其它被检测到冲突(如 Tuple 已更新)时会报错或失败。

####### ❌ MySQL 的阻塞也有前提:

  • RR(Repeatable Read) 隔离级别下,MySQL 的更新操作默认是获取行级锁一个事务修改某行时,会阻塞其它想要修改该行的事务,直到事务提交或回滚。
  • 所以你说的“阻塞所有的修改操作”是指对同一行,这是对的。
  • 但不是全表的阻塞,也不是所有写都会互相阻塞(对不同的行是可以并发写的)。

✅ 三、事务隔离级别:RR 的区别
特性 MySQL (InnoDB) PostgreSQL
默认隔离级别 Repeatable Read Read Committed
在 RR 下是否避免幻读 ✅ 避免幻读(gap lock) ❌ 不能完全避免,需要 Serializable
可重复读视图实现 基于 undo log 的 consistent snapshot 基于 xmin/xmax 的 snapshot

✅ 小结

你说的概念整体是非常准确的,细化下来结论如下:

结论 是否正确 说明
MySQL 使用 undo log 实现 MVCC 正确
PostgreSQL 使用元组版本实现 MVCC 正确
MySQL 的修改是修改原数据,PgSQL 是新增元组 正确
PostgreSQL 允许多个并发修改操作 ✅但需注意冲突检测 多个事务可以尝试修改,但不能都成功
MySQL 一个修改会阻塞其它所有修改 ✅但仅限于同一行 同一行上的行锁,阻塞其它写事务

✅ 延伸思考
  • PostgreSQL 的 MVCC 实现虽然支持高并发和非阻塞读写,但需要 VACUUM 机制 定期清理无效版本,否则表膨胀。
  • MySQL 的 undo log 基于回滚段,控制更集中,但 undo log 的使用和清理(purge)策略也会影响性能。

Mysql查询学科前三名

在 MySQL 中,如果你有一个学生成绩表,想要按学科查询每个学科的前三名成绩,常用的解决方案是使用 窗口函数,如 ROW_NUMBER()RANK()DENSE_RANK()(MySQL 8.0 及以上支持)。


✅ 假设你的表结构如下:
成绩表: scores
+----+--------+----------+--------+
| id | name   | subject  | score  |
+----+--------+----------+--------+
| 1  | 张三   | 数学     | 98     |
| 2  | 李四   | 数学     | 89     |
| 3  | 王五   | 数学     | 92     |
| 4  | 赵六   | 数学     | 76     |
| 5  | 张三   | 英语     | 88     |
| 6  | 李四   | 英语     | 90     |
| 7  | 王五   | 英语     | 86     |
| 8  | 赵六   | 英语     | 91     |
+----+--------+----------+--------+

📌 查询每个学科的前三名(按分数从高到低):
SELECT id, name, subject, score
FROM (
    SELECT *, 
           ROW_NUMBER() OVER (PARTITION BY subject ORDER BY score DESC) as rn
    FROM scores
) AS ranked
WHERE rn <= 3;

🔍 说明:
  • ROW_NUMBER() OVER (PARTITION BY subject ORDER BY score DESC)
    • 将数据按 subject 分组,并在每组中按 score 降序排序。
    • 给每行数据一个序号 rn
  • 最外层查询筛选出 rn <= 3 的,即每个学科的前三名。

📝 如果有并列名次怎么办?

可以使用 RANK()DENSE_RANK() 替换 ROW_NUMBER()

  • RANK():并列名次会跳过名次(比如两个第一名,下一个是第三名)
  • DENSE_RANK():并列名次不会跳过(两个第一名,下一个是第二名)
🧠 如果你用的是 MySQL 5.7 或以下(不支持窗口函数),可以用 相关子查询 方式实现:
SELECT s1.*
FROM scores s1
WHERE (
  SELECT COUNT(*) 
  FROM scores s2
  WHERE s2.subject = s1.subject
    AND s2.score > s1.score
) < 3;

📌 说明(旧版语法逻辑):
  • 对每条记录 s1,用一个子查询 s2 统计比它成绩高、同学科的记录数;
  • 若这个数量 < 3,说明它是该学科的前3名。

📝 注意事项:
  • 这种方式在成绩并列时可能返回多于3条(即出现“并列第三”情况);
  • 如果你希望严格只取3个名额(不管并列),请使用 ROW_NUMBER()
  • 如果你希望处理“并列”,用 RANK()DENSE_RANK() 替代 ROW_NUMBER()

什么是闭包

闭包(Closure)是编程语言中的一个重要概念,尤其在 javaScript、Python 等支持一等函数(First-class Function)的语言中非常常见。它可以简洁地定义为:


闭包是函数与其定义时所处的词法作用域(Lexical Environment)的绑定组合,即使这个作用域在函数执行时已经超出了其作用范围,函数仍然可以访问这个作用域中的变量。


一、为什么需要闭包?

闭包主要用于:

  1. 函数内部保存“外部”变量的状态(数据持久化)
  2. 实现封装与数据私有化
  3. 避免全局变量污染
  4. 函数式编程中的高阶函数应用(如柯里化)

二、javaScript 中闭包示例(经典)
function outer() {
  let count = 0;
  return function inner() {
    count++;
    console.log(count);
  };
}

const counter = outer(); // outer执行后返回inner函数,并绑定了其作用域
counter(); // 输出 1
counter(); // 输出 2
解释:
  • outer 执行后返回了 inner 函数;
  • inner 函数引用了 outer 中的 count 变量;
  • 即使 outer 已经执行完返回了,inner 依然可以访问 count,因为闭包保持了 outer 的词法作用域

三、闭包的本质(深入理解)
  1. 函数是一等对象:可以作为参数传递或作为返回值返回;
  2. 词法作用域(Lexical Scope):函数定义的位置决定了它能访问哪些变量;
  3. 闭包持有引用:闭包不是拷贝了变量,而是持有对变量的引用
  4. 内存泄漏风险:闭包可能会导致某些变量长期驻留在内存中。

四、Python 中的闭包(例子)
def outer():
    count = 0
    def inner():
        nonlocal count
        count += 1
        print(count)
    return inner

counter = outer()
counter()  # 输出 1
counter()  # 输出 2
  • nonlocal 表示使用外部非全局作用域中的变量。
  • 同样,inner 闭包“记住了” count

五、判断是否形成了闭包?

在 javaScript 中:

  1. 函数 A 内部定义并返回了函数 B;
  2. B 使用了 A 中的局部变量;
  3. 即使 A 执行完毕,B 仍然能使用这些变量。

👉 满足上述条件就形成了闭包。


六、使用闭包实现数据私有化
function createCounter() {
  let count = 0;
  return {
    increment() {
      count++;
      return count;
    },
    decrement() {
      count--;
      return count;
    }
  };
}

const counter = createCounter();
console.log(counter.increment()); // 1
console.log(counter.decrement()); // 0
  • count 变量对外不可见,形成了私有变量
  • 这种用法是模块化编程常见技巧。

七、闭包的弊端与注意点
  • 容易导致 内存泄漏(引用未释放)
  • 不小心会造成 变量共享问题
  • 不能滥用闭包,否则代码可读性和调试性下降

总结一句话:

闭包是函数+定义时作用域的组合,使函数可以“记住”并访问它定义时的变量,即使这些变量在其词法作用域之外。


hotspot虚拟机不是基于寄存器的架构,那么读取数据的时候是否会有延迟

🌟 问题背景简述

HotSpot 虚拟机采用的是 基于栈的架构(stack-based VM),而非寄存器架构(register-based VM)。这意味着大多数指令都以操作数栈作为操作对象,而不是寄存器。


✅ 栈架构 vs 寄存器架构:读取数据效率比较
栈架构(如 HotSpot):
  • 每条指令操作栈顶元素,如 iload_0 表示将局部变量表中第0个整型值压栈。
  • 优点:
    • 指令集更紧凑,指令长度更短(因为无需显式指定寄存器)。
    • 更容易跨平台(java 设计初衷)。
  • 缺点:
    • 操作需要频繁地进行压栈、出栈,中间结果频繁进出栈,可能引入额外开销
    • 理论上,执行同一逻辑需要更多的指令次数。
寄存器架构(如 Dalvik、LLVM IR):
  • 每条指令直接操作寄存器,如 ADD r0, r1, r2
  • 优点:
    • 更少的内存访问,中间结果保存在寄存器中(如果寄存器够用)。
    • 性能通常优于栈架构
  • 缺点:
    • 指令更复杂,指令长度更长。
    • 不容易跨平台(硬件相关性强)。

❓HotSpot 使用栈架构会导致访问延迟吗?
从字节码层面看:确实存在更多的数据操作指令,中间值需要频繁入栈/出栈。

但:

✅ 真正运行时,HotSpot 并不是直接解释字节码,而是会进行一系列优化:

🚀 HotSpot 的运行时优化
1. 解释器(Interpreter)阶段
  • 初始运行时确实采用栈架构解释执行。
  • 所以在解释模式下,频繁入栈/出栈可能带来一定开销和延迟
2. 即时编译(JIT)阶段:如 C1/C2 编译器

HotSpot 会将热点代码编译为本地机器码,且:

🔥 在 JIT 编译后,HotSpot 会将栈式字节码转换为寄存器分配的本地代码!

也就是说:

  • 编译后的代码在运行时已经 不再使用操作数栈,而是尽可能使用物理寄存器
  • 并且使用了复杂的寄存器分配算法(如线性扫描或图着色)
  • 此时的访问效率是高度优化的,延迟远低于解释模式

🧠 总结回答你的问题:

HotSpot 虚拟机不是基于寄存器的架构,那么读取数据的时候是否会有延迟?

答:

  • 在解释执行阶段,确实由于基于栈架构,需要频繁压栈出栈,会有一定的指令层面延迟不如寄存器架构高效
  • 但在JIT编译后(尤其是C2编译器输出的代码),HotSpot会将栈架构优化为寄存器分配的机器码,运行效率极高,读取数据非常快,延迟问题基本不存在

volatile 在java中起到lock的作用,就是读取对应内存值的时候不读取寄存器的值,根据mesi协议,如果遇到这个内存的值,是不是一定读取内存的值,而不是直接读取1,2级缓存的值

✅ 一、volatile 是不是等价于 lock?

不是。

特性 volatile synchronized / lock
可见性 ✅ 有 ✅ 有
有序性(禁止指令重排) ✅ 有(只针对该变量) ✅ 更强(全域)
原子性 ❌ 没有 ✅ 有
对应机器指令 lock 前缀指令(如 lock cmpxchg lock 伪指令+monitor

volatile 只保证对该变量的 读写是有序且立即可见的,但不保证原子性。


🧠 二、volatile 的作用是啥?

java 编译器和 JVM 在遇到 volatile 变量时,会生成特殊的汇编指令来保证 内存可见性禁止指令重排序

  • 写操作后,会插入 store-storestore-load 内存屏障(Memory Barrier);
  • 读操作前,会插入 load-loadload-store 屏障。

而这些指令,通常在底层 x86 CPU 上使用类似于:

lock xchg

或者是强制使用 MFENCE/LFENCE/SFENCE 指令。


🔄 三、MESI 协议和缓存一致性
✅ 什么是 MESI?

CPU 的缓存一致性协议,四种状态:

状态 名称 描述
M Modified 缓存为修改态,值未写回内存,独占
E Exclusive 缓存为独占态,值与内存一致,只有自己有
S Shared 缓存为共享态,与其他核心共享,未修改
I Invalid 缓存为无效态,不可读

🎯 你的核心问题:

当有 volatile 修饰变量时,如果某个线程读取这个变量,是不是一定会从主内存读取,而不会使用 L1/L2 缓存?

❌ 答案是否定的 —— 不是一定读主内存

✅ 真相:会读缓存(但要确保是最新的)
volatile 并不会让每次访问都跳过缓存直接去主内存。

那样太慢,根本无法接受。

正确做法是:
  • 通过内存屏障 + MESI 协议,确保你读取的缓存内容是「最新的」。

####### 🧵 举个真实场景:

线程 A 写了 volatile x = 1,线程 B 读它。

  1. A 写的时候触发:
    • lock 指令或 store-load barrier
    • 让该缓存行进入 M 状态,并广播 Invalidate 到其他核心,强制 B 的缓存行变为 I 状态。
  2. B 读的时候:
    • 发现自己缓存中 x 的行是 I 状态(Invalid);
    • 触发从其他核心或主内存中拉取最新数据;
    • 拉取后放入自己的缓存行,可能以 S 状态或 E 状态存储。
关键点:

B 最终读取的数据是最新的,但它最终仍然是存在 L1/L2 缓存中的,只是这个数据是有效且可见的。


✅ 总结你的问题答案:
问题 回答
volatile 是否起到 lock 的作用? ❌ 不起到原子性的 lock 作用,只保证可见性和有序性
使用 volatile 变量时是否会跳过缓存? ❌ 不会直接跳过,而是通过缓存一致性机制使缓存行最新
是否每次都从主内存读取? ❌ 否,读取仍是从缓存,但缓存必须是最新有效的(非 Invalid)
volatile 如何实现内存可见性? ✅ 通过内存屏障 + MESI 协议使缓存失效或同步

📌 补充小结(一图概括)
java volatile -> 内存屏障 + 禁止重排序
           ↓
CPU 执行 lock 或 fence 指令
           ↓
使其他核心的缓存行失效(MESI协议)
           ↓
其他线程读取时重新拉取最新值(缓存or内存)
           ↓
✅ 保证可见性,不等于总是读主内存

MysticalYcc , 版权所有丨如未注明 , 均为原创丨本网站采用BY-NC-SA协议进行授权
转载请注明原文链接:知识整理
喜欢 (1)
mysticalycc
关于作者:
简短的个人签名

Warning: Attempt to read property "comment_author_email" on null in /www/wwwroot/gschaos.club/wp-content/themes/Git-alpha-6h0SRk/comments.php on line 47
发表我的评论
取消评论
表情 贴图 加粗 删除线 居中 斜体 签到