6w字汇总下最近背过的Java服务端面试题笔记

0 阅读34分钟

一、基础知识部分

Q:java面向对象三大特性?

A:封装、继承、多态

Q:反射原理以及使用场景

Q:Java中的浅拷贝和深拷贝

  • 浅拷贝:只复制对象本身的基本数据类型字段,对于引用类型的字段,只复制内存地址(引用),新旧对象共享同一个子对象
  • 深拷贝:不仅复制对象本身,还递归复制所有引用的子对象,新旧对象完全独立,互不影响。

使用场景,对一个List users集合进行拷贝,如果执行浅拷贝后生成新集合 新集合的元素和老的是共用对象引用的,修改新集合某个对象元素的内容,会导致老集合一起被更新

Q: String、StringBuffer、StringBuilder 的区别?

源码分析:csp1999.blog.csdn.net/article/det…

  • String:被final关键字修饰不可变,线程安全;每次对 String 类型进行改变的时候,都会生成一个新的 String 对象,然后将指针指向新的 String 对象。
  • StringBuilder:字符串对象通过“+”的字符串拼接方式,实际上是通过 StringBuilder 调用 append() 方法实现的,拼接完成之后调用 toString() 得到一个 String 对象 。线程不安全
  • StringBuffer:线程安全,append()、length()等关键方法都被 synchronized 加锁。

Q:String#equals() 和 Object#equals() 有何区别?

String 中的 equals 方法是被重写过的,比较的是 String 字符串的值是否相等。 Object 的 equals 方法是比较的对象的内存地址。

Q: JDK 动态代理和 CGLIB 动态代理有什么区别?

对比维度JDK动态代理CGLIB动态代理
实现原理基于接口,实现目标接口基于继承,生成目标类的子类
要求目标类必须实现接口目标类无需接口,但不能是final类
生成方式反射机制生成代理类ASM字节码框架生成子类
性能创建快,调用稍慢(反射)创建稍慢,调用较快(直接字节码)
依赖JDK内置,无需额外依赖需要引入cglib库
final方法不影响(接口无final方法)无法代理final方法
┌─────────────────────────────────────
│  有接口  →  两者都能用,JDK是默认选择        
│  无接口  →  只能用 CGLIB                  
│  final类 →  两者都不能代理                 
│  final方法→ CGLIB无法代理,JDK无此问题      
└─────────────────────────────────────
    
JDK代理调用链:
proxy.save() → InvocationHandler.invoke() → Method.invoke()【反射】→ 目标方法

CGLIB调用链:
proxy.save() → MethodInterceptor.intercept() → MethodProxy.invokeSuper()【字节码】→ 目标方法
    

Q:BIO、NIO 和 AIO 的区别?

TODO

Q:IO多路复用

TODO


二、Java集合部分

Java集合体系

graph TB
    A[Java集合框架] --> B[Collection\n单列集合]
    A --> C[Map\n双列集合]

    B --> D[List\n有序可重复]
    B --> E[Set\n无序不重复]
    B --> F[Queue\n队列]

    D --> D1[ArrayList]
    D --> D2[LinkedList]
    D --> D3[Vector]
    D3 --> D4[Stack]

    E --> E1[HashSet]
    E --> E2[LinkedHashSet]
    E --> E3[TreeSet]
    E --> E4[EnumSet]

    F --> F1[ArrayDeque]
    F --> F2[LinkedList]
    F --> F3[PriorityQueue]
    F --> F4[BlockingQueue]
    F4 --> F5[ArrayBlockingQueue]
    F4 --> F6[LinkedBlockingQueue]
    F4 --> F7[PriorityBlockingQueue]
    F4 --> F8[SynchronousQueue]
    F4 --> F9[DelayQueue]

    C --> C1[HashMap]
    C --> C2[LinkedHashMap]
    C --> C3[TreeMap]
    C --> C4[Hashtable]
    C --> C5[EnumMap]
    C --> C6[ConcurrentHashMap]
    C4 --> C7[Properties]

    style A fill:#4A90D9,color:#fff
    style B fill:#7B68EE,color:#fff
    style C fill:#7B68EE,color:#fff
    style D fill:#E8A838,color:#fff
    style E fill:#E8A838,color:#fff
    style F fill:#E8A838,color:#fff

1.ArrayList

1. 数据结构

ArrayList 底层是动态数组,本质上维护了一个 Object[] 数组。它支持随机访问,查询快,但插入删除慢。

2. 初始容量

在 JDK 8 中,new ArrayList() 时并不会立刻创建长度为 10 的数组,而是先使用容量为 0 的空数组。第一次添加元素时才扩容到默认容量 10。

如果使用 new ArrayList(int initialCapacity),则初始容量为指定值。

2. 扩容机制

  • 当add新元素时,如果当前数组容量不足以容纳新元素,就会触发扩容。即当 size + 1 > elementData.length 时扩容。

  • 每次扩容为原容量的 1.5 倍,即:

// >>1 右移一位相当于除 2    
newCapacity = oldCapacity + (oldCapacity >> 1)
  • 扩容出触发后,创建一个新的数组,然后把老的数据拷贝过去(Arrays.copyOf())

2.LinkedList

数据结构

LinkedList 基于双向链表实现,插入、删除、更新快,查询慢,不支持随机访问。

LinkedList 底层数据结构是链表,内存地址不连续,只能通过指针来定位,不支持随机快速访问,不能实现 RandomAccess 接口。

3. HashMap

1. 数据结构

JDK1.7 及之前底层是数组和链表(链表散列)。

JDK1.8 之后底层是数组和链表加红黑树。

image.png

为什么JDK8链表改为尾插法?

  • 扩容时保持链表顺序
  • 避免头插法并发扩容时遇到死循环问题

2. 初始容量

image.png

// 为什么容量必须是2的幂次方?
// 方便用位运算代替取模:hash % capacity = hash & (capacity-1)
// 位运算效率远高于取模运算!

// tableSizeFor:找到大于等于cap的最小2的幂次方
static final int tableSizeFor(int cap) {
    int n = cap - 1;
    n |= n >>> 1;
    n |= n >>> 2;
    n |= n >>> 4;
    n |= n >>> 8;
    n |= n >>> 16;
    return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
}
// 传入10 → 返回16
// 传入17 → 返回32
    

3. 扩容机制

  • HashMap扩容时每次容量变为原来的两倍;新的扩容阈值为新容量*0.75

  • 当桶的数量小于64时不会进行树化,只会扩容;

  • 当桶的数量大于64且单个桶中元素的数量大于8时,进行树化;

  • 当单个桶中元素数量小于6时,进行反树化;

为什么树化阈值是8?

链表长度符合泊松分布长度为8的概率约为 0.00000006(极低)

红黑树查询O(logn) vs 链表O(n),转换有额外内存开销(TreeNode是Node的2倍大小)长度8时收益才大于成本。

为什么退化阈值是6而不是8?

避免频繁在8附近增删导致树与链表反复转换,6和8之间留有缓冲区。

扩容后旧数组迁移过程?

graph TB
    A[遍历旧数组每个桶] --> B{桶中元素}
    B -->|空桶| C[跳过]
    B -->|单节点| D[直接rehash放入新桶]
    B -->|链表| E[拆分低位链/高位链\n分别放入新桶]
    B -->|红黑树| F[split拆分\n节点<=6转链表\n节点>6保持红黑树]

PS:搬移元素,原链表分化成两个链表,低位链表节点存储在原来桶的位置,高位链表搬移到原来桶的位置加旧容量的位置;目的是在扩容数组上,将原来链表上的hash冲突打散。

4. HashMap如何解决Hash冲突?如何定位桶位

graph LR
    A[key] --> B[key.hashCode\n原始hash值]
    B --> C[扰动函数\nhash ^ hash>>>16\n高16位异或低16位]
    C --> D[桶位计算\nhash & n-1\nn为数组长度]
    D --> E[定位到具体桶]

    style C fill:#E8A838,color:#fff
    style D fill:#4A90D9,color:#fff

// 第一步:计算hash(扰动函数)
static final int hash(Object key) {
    int h;
    // null的hash固定为0,放在桶0
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

// 为什么要高16位异或低16位?
// hashCode是32位,桶位计算只用低位(hash & n-1)
// 高位信息被浪费,容易产生冲突
// 扰动函数让高位参与运算,降低hash碰撞概率

// 第二步:计算桶位
int index = hash & (n - 1);
// 等价于 hash % n,但位运算更快
// 前提:n必须是2的幂次方!

// 举例:
// n=16,n-1=15=0000 1111
// hash=1010 1100 & 0000 1111 = 0000 1100 = 桶12
    

5. HashMap中put方法的执行流程?

image.png

6. HashMap vs Hashtable vs ConcurrentHashMap?

对比维度HashMapHashtableConcurrentHashMap
线程安全❌ 不安全✅ 安全(方法级synchronized)✅ 安全(CAS+synchronized)
锁粒度无锁整个对象锁(锁全表)JDK7:分段锁 / JDK8:锁单个桶
性能最高最低(全表锁竞争激烈)较高(细粒度锁)
null Key✅ 允许1个❌ 不允许❌ 不允许
null Value✅ 允许❌ 不允许❌ 不允许
底层结构数组+链表+红黑树数组+链表数组+链表+红黑树
初始容量161116
扩容倍数2倍2倍+12倍
扩容时机容量*0.75容量*0.75容量*0.75
链表转红黑树✅ 链表长度≥8❌ 不支持✅ 链表长度≥8

PS:HashSet底层就是基于HashMap实现,不需要额外做比较

HashMap和TreeMap的区别?

7. HashMap vs TreeMap

对比维度HashMapTreeMap
底层结构数组+链表+红黑树红黑树
是否有序❌ 无序✅ 按Key自然排序或自定义排序
查询性能O(1)O(log n)
插入性能O(1)O(log n)
null Key✅ 允许1个❌ 不允许
null Value✅ 允许✅ 允许
排序方式不支持自然排序/Comparator自定义
适用场景大多数键值对存储需要范围查询/排序场景

4.CurrentHashMap

1. 数据结构

JDK1.7 的 ConcurrentHashMap 底层采用分段的数组 + 链表实现,固定位16分段,每一段都是一个独立的HashMap结构,可以独立进行扩容。

flowchart LR
    M[ConcurrentHashMap]
    M --> SA[Segment 数组]

    SA --> S0[Segment 0]
    SA --> S1[Segment 1]
    SA --> S3[Segment 16]

    S1 --> T0[HashEntry 数组]

    T0 --> B00[桶0]
    T0 --> B01[桶1]
    T0 --> B02[桶2]

    B01 --> E1[HashEntry]
    E1 --> E2[HashEntry]
    E2 --> E3[HashEntry]

JDK1.8 的 ConcurrentHashMap 内部的 map 结构和 HashMap 是一致的,都是由:数组 + 链表 + 红黑树构成。

ConcurrentHashMap 和 HashMap 区别就在于支持并发扩容,其内部通过加锁(CAS + synchronized)来保证线程安全。

flowchart LR
    M[ConcurrentHashMap]
    M --> T[Node 数组 table]

    T --> B0[桶0]
    T --> B1[桶1]
    T --> B2[桶2]
    T --> B3[桶3]
    T --> B4[桶4]

    B1 --> N1[Node]
    N1 --> N2[Node]
    N2 --> N3[Node]

    B3 --> TB[TreeBin]
    TB --> R[TreeNode 根节点]
    R --> L[左子树]
    R --> RR[右子树]

    B4 --> N4[Node]
    N4 --> N5[Node]

2. 初始容量

ConcurrentHashMap 默认初始容量是 16。在 JDK 8 中,默认构造不会立刻创建数组,而是在第一次 put 时延迟初始化,最终容量会调整为不小于指定值的 2 的幂。

3. 扩容机制

1.7与1.8版本扩容机制对比

对比维度JDK7JDK8
并发性能低,单线程扩容阻塞写高,多线程协同缩短扩容时间
扩容范围单个Segment内扩容整个Node数组扩容
扩容线程数单线程多线程协同并发扩容
锁机制ReentrantLock锁住SegmentCAS+synchronized锁住桶头节点
锁粒度Segment级别(1/16数组)单个桶头节点
其他线程行为其他Segment不受影响,正常读写发现MOVED标记,加入协助扩容
数据迁移方式链表头插法重建链表尾插法拆分低位链/高位链
读操作扩容期间正常读旧Segment发现ForwardingNode转向新数组读
并发性能低,单线程扩容阻塞写高,多线程协同缩短扩容时间

4. ConcurrentHashMap 为什么 key 和 value 不能为 null?

ConcurrentHashMap 不允许 null key 和 null value。因为在并发环境下,get 返回 null 时无法区分到底是 key 不存在,还是 value 本身为 null,这会导致语义不明确。

5. ConcurrentHashMap 如何保证线程安全?

  • JDK1.7版本是通过锁住分段数组的一个分段桶位,不允许其他线程进入,这个分段桶里的扩容和put流程与HashMap一致。分段数组最多有16个桶位,支持16个并发写入。
  • JDK1.8以后细化锁粒度,通过CAS+synchronized锁住table数组的一个桶位来保证写入线程安全,通过 volatile 关键字保证内存可见性,读操作直接读主内存最新值,无需加锁。

6. ConcurrentHashMap 的 get() 需要加锁吗?为什么?

不需要加锁! 通过 volatile 关键字保证内存可见性,读操作直接读主内存最新值,无需加锁。

7. ConcurrentHashMap 的 put() 操作流程?

put 流程总共分为 7步:

第一步 校验,key 或 value 为 null 直接抛 NPE

第二步 用 spread() 计算 hash 值

第三步 进入 for 自旋,保证 put 最终一定成功

第四步 table 未初始化则 initTable,用 CAS 保证只有一个线程完成初始化

第五步 定位桶位,分三种情况:

  • 空桶 → CAS 写入,成功结束,失败继续自旋
  • MOVED → 协助扩容,完成后继续自旋重新 put
  • 桶不为空 → synchronized 锁头节点,链表尾插或红黑树插入

第六步 检查链表长度,满足条件则树化或扩容

第七步 addCount 更新 size,超过阈值触发扩容

put核心链路流程图

flowchart LR
    A([put]) --> B{key/value为空?}
    B -->|是| C([抛出NPE])
    B -->|否| D[计算hash]

    D --> E{table已初始化?}
    E -->|否| F[初始化table]
    E -->|是| G[定位桶]
    F --> G

    G --> H{桶为空?}
    H -->|是| I[CAS插入]
    H -->|否| J[锁桶头]

    I --> K([完成])

    J --> L{链表或红黑树}
    L -->|链表| M[插入或更新]
    L -->|红黑树| N[插入或更新]

    M --> O[树化检查]
    N --> P[更新计数]
    O --> P

    P --> Q{需要扩容?}
    Q -->|是| R[触发扩容]
    Q -->|否| K
    R --> K

    style A fill:#2563eb,color:#fff,stroke:#1e3a8a,stroke-width:2px
    style K fill:#16a34a,color:#fff,stroke:#166534,stroke-width:2px
    style C fill:#dc2626,color:#fff,stroke:#991b1b,stroke-width:2px

    style D fill:#3b82f6,color:#fff,stroke:#1d4ed8
    style F fill:#3b82f6,color:#fff,stroke:#1d4ed8
    style I fill:#3b82f6,color:#fff,stroke:#1d4ed8

    style J fill:#f59e0b,color:#fff,stroke:#b45309
    style M fill:#f59e0b,color:#fff,stroke:#b45309
    style N fill:#f59e0b,color:#fff,stroke:#b45309
    style O fill:#f59e0b,color:#fff,stroke:#b45309

    style R fill:#7c3aed,color:#fff,stroke:#5b21b6

put时触发协助扩容流程图

flowchart LR
    A[put定位到桶] --> B{桶头是否为 MOVED}

    B -->|否| C[按正常 put 流程处理]
    B -->|是| D[进入 helpTransfer]

    D --> E{还有迁移任务吗}
    E -->|有| F[认领一段桶区间]
    F --> G[迁移旧桶数据到新 table]
    G --> H[旧桶标记为 ForwardingNode]
    H --> E

    E -->|无| I[结束协助扩容]
    I --> J[回到 put 主流程]

    style A fill:#3b82f6,color:#fff,stroke:#1d4ed8,stroke-width:2px
    style C fill:#60a5fa,color:#fff,stroke:#2563eb,stroke-width:2px

    style D fill:#8b5cf6,color:#fff,stroke:#6d28d9,stroke-width:2px
    style F fill:#a78bfa,color:#fff,stroke:#7c3aed,stroke-width:2px
    style G fill:#a78bfa,color:#fff,stroke:#7c3aed,stroke-width:2px
    style H fill:#a78bfa,color:#fff,stroke:#7c3aed,stroke-width:2px

    style I fill:#22c55e,color:#fff,stroke:#15803d,stroke-width:2px
    style J fill:#16a34a,color:#fff,stroke:#166534,stroke-width:2px

    style B fill:#f3f4f6,color:#111827,stroke:#9ca3af,stroke-width:1.5px
    style E fill:#f3f4f6,color:#111827,stroke:#9ca3af,stroke-width:1.5px

8. ConcurrentHashMap 的 size 如何计算?

JDK1.7时锁住每个分段槽位,逐个每个槽位上的size和

JDK1.8时参考LongAdder原子类的计数设计,基于baseCount + cells数组方式实现,二者都被volatile关键词修饰(线程可见性)

具体原理:

flowchart LR
    T1[线程1] -->|CAS| B[baseCount]

    T2[线程2] -->|CAS| C1[CounterCell]
    T3[线程3] -->|CAS| C2[CounterCell]
    T4[线程4] -->|CAS| C3[CounterCell]
    TN[线程N] -->|CAS| CN[CounterCell]

    subgraph BOX1[低竞争 直接更新]
        B
    end

    subgraph BOX2[高竞争 分段计数]
        C1
        C2
        C3
        CN
    end

    B --> S[sumCount]
    BOX2 --> S
    S --> R[总数 = baseCount + 所有CounterCell之和]

    style T1 fill:#cfe8d5,stroke:#6b8f71,color:#222
    style T2 fill:#cfe8d5,stroke:#6b8f71,color:#222
    style T3 fill:#cfe8d5,stroke:#6b8f71,color:#222
    style T4 fill:#cfe8d5,stroke:#6b8f71,color:#222
    style TN fill:#cfe8d5,stroke:#6b8f71,color:#222

    style B fill:#cfe7eb,stroke:#6d8d94,color:#222
    style C1 fill:#ecd8c5,stroke:#8b7355,color:#222
    style C2 fill:#ecd8c5,stroke:#8b7355,color:#222
    style C3 fill:#ecd8c5,stroke:#8b7355,color:#222
    style CN fill:#ecd8c5,stroke:#8b7355,color:#222

    style S fill:#f7f1d5,stroke:#a59b63,color:#222
    style R fill:#f7f1d5,stroke:#a59b63,color:#222

举例:

flowchart LR
    T1[线程1] --> B0[baseCount = 10]
    T2[线程2] --> B0
    T3[线程3] --> B0

    B0 --> D{CAS执行加1}

    D -->|线程1成功| B1[baseCount = 11]
    D -->|线程2失败| C1[CounterCell1 = 1]
    D -->|线程3失败| C2[CounterCell2 = 1]

    subgraph BOX[CounterCell数组 counterCells]
        C1
        C2
    end

    B1 --> S[sumCount]
    BOX --> S
    S --> R[总数 = 11 + 1 + 1 = 13]

    style T1 fill:#cfe8d5,stroke:#6b8f71,color:#222
    style T2 fill:#cfe8d5,stroke:#6b8f71,color:#222
    style T3 fill:#cfe8d5,stroke:#6b8f71,color:#222

    style B0 fill:#bff0b4,stroke:#6f9d64,color:#222
    style B1 fill:#bff0b4,stroke:#6f9d64,color:#222

    style C1 fill:#79d8e8,stroke:#3d8c98,color:#222
    style C2 fill:#79d8e8,stroke:#3d8c98,color:#222

    style S fill:#f7f1d5,stroke:#a59b63,color:#222
    style R fill:#f7f1d5,stroke:#a59b63,color:#222

这样设计的目的:是分散线程竞争,低线程竞争情况下优先使用CAS,线程竞争多的情况下,则通过cells数组分散线程竞争。

9. ConcurrentHashMap的sizeCtl 有哪些含义?

private transient volatile int sizeCtl;

分别对应四种状态:

image.png

sizeCtl状态流转流程图:

flowchart LR
    A([sizeCtl初始状态]) --> B{构造时是否指定容量}

    B -->|否| C[sizeCtl = 0]
    B -->|是| D[sizeCtl = 指定容量]

    C --> E[首次put触发初始化]
    D --> E

    E --> F[CAS修改sizeCtl为 -1]
    F --> G{是否抢到初始化权}

    G -->|否| H[其他线程让步等待]
    G -->|是| I[初始化table]

    I --> J[sizeCtl = 扩容阈值]
    H --> J

    J --> K{元素个数是否超过阈值}

    K -->|否| L[正常读写]
    K -->|是| M[进入扩容]

    M --> N[sizeCtl < -1]
    N --> O{其他线程是否发现扩容中}

    O -->|是| P[helpTransfer协助扩容]
    O -->|否| Q[当前线程继续迁移]

    P --> R{扩容是否完成}
    Q --> R

    R -->|否| N
    R -->|是| S[sizeCtl = 新扩容阈值]

    S --> L

    style A fill:#3b82f6,color:#fff,stroke:#1d4ed8,stroke-width:2px
    style C fill:#93c5fd,color:#1e3a8a,stroke:#3b82f6
    style D fill:#93c5fd,color:#1e3a8a,stroke:#3b82f6

    style F fill:#f59e0b,color:#fff,stroke:#b45309,stroke-width:2px
    style I fill:#06b6d4,color:#fff,stroke:#0f766e,stroke-width:2px
    style J fill:#22c55e,color:#fff,stroke:#15803d,stroke-width:2px

    style M fill:#ef4444,color:#fff,stroke:#991b1b,stroke-width:2px
    style N fill:#ef4444,color:#fff,stroke:#991b1b,stroke-width:2px
    style P fill:#8b5cf6,color:#fff,stroke:#6d28d9,stroke-width:2px
    style Q fill:#8b5cf6,color:#fff,stroke:#6d28d9,stroke-width:2px
    style S fill:#16a34a,color:#fff,stroke:#166534,stroke-width:2px

10. 描述一下ConcurrentHashMap中的hash寻址算法? 节点的 Node.hash 字段一般情况下必须 >=0 这是为什么?

寻址算法:

// 1)先取 key 的 hashCode
int h = key.hashCode();
// 2)做 spread 扰动运算, 减少哈希冲突概率
(h ^ (h >>> 16)) & HASH_BITS
// 3)通过 (n - 1) & hash 定位桶下标
int idx = (n - 1) & hash
graph LR
    A([key]) --> B[key.hashCode\n原始hashCode]
    B --> C[spread方法\n扰动处理]
    C --> D[hash & n-1\n定位桶位i]
    D --> E([桶位i])

    style B fill:#6366f1,color:#fff
    style C fill:#E8A838,color:#fff
    style D fill:#52C41A,color:#fff

节点hash值对照表:

hash值常量名节点类型含义
>= 0-普通Node正常链表节点,spread保证最高位=0
-1MOVEDForwardingNode扩容占位,读写转向新数组
-2TREEBINTreeBin红黑树根节点包装类
-3RESERVEDReservationNodecompute()方法占位节点

PS:RESERVED,防止并发操作同一个key,用来占位的。

11. ConcurrentHashMap 能完全保证线程安全吗?

ConcurrentHashMap 不能完全保证线程安全。

  • ConcurrentHashMap 只保证自己API的原子性
  • 业务代码的复合操作需要自己自己加锁控制!

例如:单独用ConcurrentHashMap的get/put这些API是线程安全的,但是在业务代码里如果,做了非原子操作,例如先get下,如果存在元素,然后在put更新下,这种场景下并发处理就可能出现线程不安全

12. ConcurrentHashMap 的并发扩容原理?

ConcurrentHashMap 并发扩容时,会先创建一个容量为table数组 2 倍的新数组nextTable;将 sizeCtl 设置为 -(1+n),表示已经处于并发扩容状态,其他线程执行put时,会根据 sizeCtl 值判断是否需要进来协助扩容。

协助扩容的流程是根据transferIndex,把旧table数组拆分成多个段,每个线程认领一段区间,将旧数组上的数据迁移到数组nextTable。旧数组上每个桶位执行迁移的时候,会标记为ForwardingNode。当每个线程完成自己的迁移任务后,再去执行自己的put逻辑,执行完后推出。

扩容结束后更新sizeCtl为新数组容量*0.75,表示下次扩容阈值。

几个关键变量:

  • sizeCtl:作为扩容开关标识
  • transferIndex:作为迁移任务分发器,记录还有哪些桶没迁移,哪些区间分给哪个线程
  • ForwardingNode:作为桶迁移完成标记 + 访问转发标志,告诉其他线程这个桶已经搬走了,去新表查询

流程图:

graph TD
    A([触发扩容]) --> B[创建新数组\n容量=旧数组*2]
    B --> C[计算每个线程\n负责迁移的桶数stride]
    C --> D{是否有空闲线程\n可以参与?}

    D -->|第一个扩容线程| E[sizeCtl编码\n记录扩容线程数+1]
    D -->|其他线程协助| F[helpTransfer\nsizeCtl+1注册]

    E & F --> G[从transferIndex\n从后往前认领一段桶]

    G --> H{遍历负责的\n每个桶}

    H --> I{桶是否为空?}
    I -->|是| J[CAS放入\nForwardingNode占位]

    I -->|否| K[synchronized\n锁住桶头节点]
    K --> L[链表/红黑树\n数据迁移到新数组]
    L --> M[旧桶放入\nForwardingNode]

    J & M --> N{负责的桶\n是否全部完成?}
    N -->|否| H
    N -->|是| O{是否还有\n未认领的桶?}
    O -->|有| G
    O -->|无| P[sizeCtl-1\n退出扩容]

    P --> Q{是否最后\n一个线程?}
    Q -->|否| R([线程退出])
    Q -->|是| S[全部迁移完成\n更新table=新数组\nsizeCtl=新阈值]
    S --> R

    style A fill:#6366f1,color:#fff
    style J fill:#4A90D9,color:#fff
    style K fill:#E8A838,color:#fff
    style S fill:#52C41A,color:#fff
    style R fill:#52C41A,color:#fff

13. ConcurrentHashMap 和 HashTable区别?

并发锁粒度不同:

  • HashTable 用 synchronized 锁住整张表,同一时刻只允许一个线程操作,性能差。
  • ConcurrentHashMap 用 CAS + synchronized 锁单个桶,并发度高于 HashTable
graph TB
    subgraph HashTable 锁整张表
        T1[线程1 put] --> LOCK[锁住整个HashTable]
        T2[线程2 get] --> WAIT[等待...]
        T3[线程3 put] --> WAIT2[等待...]
        LOCK --> UN[释放锁]
        UN --> T2
    end

    subgraph ConcurrentHashMap 锁单个桶
        P1[线程1 操作桶3] --> L1[锁桶3]
        P2[线程2 操作桶7] --> L2[锁桶7 互不影响]
        P3[线程3 操作桶12] --> L3[锁桶12 互不影响]
    end

    style LOCK fill:#E8A838,color:#fff
    style WAIT fill:#999,color:#fff
    style WAIT2 fill:#999,color:#fff
    style L1 fill:#4A90D9,color:#fff
    style L2 fill:#4A90D9,color:#fff
    style L3 fill:#4A90D9,color:#fff

初始容量与扩容倍数不同:

  • HashTable 初始11,扩容倍数2n+1
  • ConcurrentHashMap 初始16,扩容倍数 2n,容量必须是2的幂次方

数据结构不同:

  • HashTable 数组+链表
  • ConcurrentHashMap 数组+链表+红黑树

二者都不允许null为key/value


三、JUC部分

1. 线程基础知识

Q: Thread#sleep() 方法和 Object#wait() 方法对比?

共同点:两者都可以暂停线程的执行。

对比项Thread.sleep()Object.wait()
所属类ThreadObject
是否释放锁
是否必须在同步块中调用
主要用途让当前线程休眠一段时间线程间通信 / 条件等待
唤醒方式到时间自动恢复notify/notifyAll、中断、超时

Q:什么是线程死锁?如何避免死锁?如何检测死锁?

死锁场景:两个或多个线程互相持有对方需要的锁,都在等待对方释放,导致所有线程永久阻塞,程序无法继续执行!

graph LR
    T1([线程1]) -->|持有| L1[锁A]
    T1 -->|等待| L2[锁B]
    T2([线程2]) -->|持有| L2[锁B]
    T2 -->|等待| L1[锁A]

    style T1 fill:#6366f1,color:#fff
    style T2 fill:#E8A838,color:#fff
    style L1 fill:#ef4444,color:#fff
    style L2 fill:#ef4444,color:#fff

代码案例:

public class DeadLockDemo {
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            synchronized (lockA) {
                System.out.println("线程1拿到 lockA");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lockB) {
                    System.out.println("线程1拿到 lockB");
                }
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (lockB) {
                System.out.println("线程2拿到 lockB");
                try { Thread.sleep(100); } catch (InterruptedException e) {}
                synchronized (lockA) {
                    System.out.println("线程2拿到 lockA");
                }
            }
        });

        t1.start();
        t2.start();
    }
}

如何避免:

  • 尽量不要嵌套加锁
  • ReentrantLock#tryLock() 抢锁超时放弃

如何检测和排查死锁?

  • jstack工具
jstack <pid>
如果有死锁,通常会直接看到类似:能看到哪些线程死锁了,持有那些锁

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor ...
  which is held by "Thread-2"

"Thread-2":
  waiting to lock monitor ...
  which is held by "Thread-1"

根据下面的标识去看持有什么锁
locked <0x000000...>

根据下面的标识去看当前线程在等待那些锁
waiting to lock <0x000000...>

根据下面的命令可以看出当前死锁卡在那行代码:
at com.xxx.OrderService.doXxxx(OrderService.java:123)


Q:什么是乐观锁、悲观锁?有哪些例子

  • 悲观锁:先加锁,然后再判断是否有线程冲突,例如 synchronized、ReentrantLock
  • 乐观锁:先提交执行,如果有冲突在加锁。例如 CAS、版本号控制

Q:什么是CAS?如何解决ABA问题?

Java中的CAS在Unsafe类中有实现。

CAS操作包含三个操作数————内存位置(V)、期望值(A)和新值(B)。

如果内存位置的值与期望值匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不作任何操作。

参考:blog.csdn.net/weixin_4359…

2. Unsafe相关

Q:Unsafe有哪些功能

分类核心方法典型应用
堆外内存allocateMemory / freeMemoryNetty、DirectByteBuffer
CAS操作compareAndSwapInt / compareAndSwapObjectAtomicXxx、AQS
线程调度park / unparkLockSupport
对象操作allocateInstance / objectFieldOffset反序列化框架
类操作defineClass / defineAnonymousClassLambda、动态代理
数组操作arrayBaseOffset / arrayIndexScaleAtomicIntegerArray
内存屏障loadFence / storeFence / fullFenceDisruptor、volidate
系统信息addressSize / pageSize内存对齐

堆外内存使用场景

在 I/O 通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。例如Netty就是这样做的。

内存屏障是什么?有什么作用

内存屏障是一种 CPU 指令,用来解决多线程下的可见性和有序性问题。 它通过禁止指令重排序、强制写操作刷回主内存、强制读操作从主内存加载,来保证多线程间数据的正确性。在 Java 中 volatile 关键字底层就是通过插入内存屏障来实现的。

  • 解决指令重排问题对多线程场景下的影响

  • Unsafe涉及的内存屏障方法如下:

graph TB
    A[Unsafe 内存屏障] --> B[loadFence\n读屏障]
    A --> C[storeFence\n写屏障]
    A --> D[fullFence\n全屏障]

    B --> B1[屏障前所有读操作必须完成\n禁止Load指令越过屏障重排]
    C --> C1[屏障前所有写操作必须完成\n禁止Store指令越过屏障重排]
    D --> D1[读写屏障的综合\n最强保证 开销最大]

    style B fill:#4A90D9,color:#fff
    style C fill:#E8A838,color:#fff
    style D fill:#7B68EE,color:#fff

内存屏障如何解决指令重排问题:

sequenceDiagram
    participant Code as 代码指令
    participant Barrier as 内存屏障
    participant Mem as 主内存

    Note over Code,Mem: loadFence() 读屏障
    Code->>Barrier: 屏障前所有读操作
    Barrier->>Mem: 强制从主内存加载,不使用缓存
    Barrier->>Code: 屏障后的指令才能继续执行

    Note over Code,Mem: storeFence() 写屏障
    Code->>Barrier: 屏障前所有写操作
    Barrier->>Mem: 强制将缓存中的写刷回主内存
    Barrier->>Code: 屏障后的指令才能继续执行

    Note over Code,Mem: fullFence() 全屏障
    Code->>Barrier: 屏障前所有读写操作
    Barrier->>Mem: 读从主内存加载 + 写刷回主内存
    Barrier->>Code: 屏障后的指令才能继续执行

Unsafe的CAS操作?

CAS(Compare And Swap)比较并交换,是一种无锁原子操作。 如果内存中的值等于期望值,则将其更新为新值,否则不做任何操作,整个过程是原子的。

graph LR
    A[开始CAS操作] --> B{主内存值 == 期望值?}
    B -->|是| C[更新为新值\n返回true]
    B -->|否| D[不做任何操作\n返回false]
    C --> E[结束]
    D --> E

Unsafe的线程调度操作有哪些?

核心方法:

graph TB
    A[Unsafe 线程调度] --> B[park\n阻塞线程]
    A --> C[unpark\n唤醒线程]
    A --> D[已废弃方法]

    B --> B1[park false 0\n无限期阻塞]
    B --> B2[park false ns\n相对时间阻塞 纳秒]
    B --> B3[park true ms\n绝对时间阻塞 毫秒]

    C --> C1[unpark thread\n唤醒指定线程]

    D --> D1[monitorEnter\n获取锁 已废弃]
    D --> D2[monitorExit\n释放锁 已废弃]
    D --> D3[tryMonitorEnter\n尝试获取锁 已废弃]

    style B fill:#4A90D9,color:#fff
    style C fill:#E8A838,color:#fff
    style D fill:#999,color:#fff

3. volatile关键词

volatile 两大作用

  • 保证可见性

    • 写:修改volatile变量 → 立即刷新到主内存
    • 读:读取volatile变量 → 从主内存重新加载
  • 禁止指令重排序(通过内存屏障实现)

    • 写屏障:写操作前后插入屏障,保证之前操作不会重排到写之后
    • 读屏障:读操作前后插入屏障,保证之后操作不会重排到读之前

注意:volatile不保证原子性!

volatile int count = 0;
count++; // 非原子!读-改-写三步,volatile救不了!

4. synchronized关键字

synchronized 是 Java 内置的互斥同步锁,基于 JVM 层面实现,保证同一时刻只有一个线程执行同步代码块,解决多线程原子性、可见性、有序性问题!

1. synchronized锁的是什么?

  • 修饰成员方法时,锁的是对象实例
public synchronized void my_method() {}
  • 修饰静态方法时,锁的是类Class
public static synchronized void my_method() {}

2. synchronized锁信息存放在哪里?

synchronized 锁信息存储在对象头的 Mark Word 中!JVM 在字节码层面会通过 Monitor(对象监视器): monitorenter、monitorexit 2个字节码命令实现同步代码块;

synchronized 锁信息存储在对象头的 Mark Word 中!

对象在内存中的结构:
┌─────────────────────────────────┐
│          对象头 Header           │
│  ┌───────────────────────────┐  │
│  │  Mark Word(8字节)        │  │  ← 存锁状态/hashCode/GC年龄
│  │  Klass Pointer(4/8字节)  │  │  ← 指向Class对象
│  └───────────────────────────┘  │
├─────────────────────────────────┤
│          实例数据 Fields         │
├─────────────────────────────────┤
│          对齐填充 Padding        │
└─────────────────────────────────┘

Mark Word 在不同锁状态下的内容:
锁信息中包含了,持有锁线程ID、锁状态、锁冲入次数、自选情况等等。

┌──────────────┬────────────────────────────────┬──────┐
│   锁状态     │           Mark Word内容         │标志位│
├──────────────┼────────────────────────────────┼──────┤
│   无锁       │  hashCode│GC年龄│偏向位=001  │
│   偏向锁     │  线程ID  │epoch│GC年龄│偏向位=101  │
│  轻量级锁    │     指向栈帧中Lock Record的指针  │  00  │
│  重量级锁    │     指向Monitor对象的指针        │  10  │
│   GC标记     │                                 │  11  │
└──────────────┴────────────────────────────────┴──────┘



3. synchronized 锁升级的过程?

JDK1.6后引入锁升级过程。

flowchart LR
    A[无锁状态] -->|第一个线程进入同步块| B[偏向锁]
    B -->|其他线程参与竞争<br/>偏向锁撤销| C[轻量级锁]
    C -->|竞争加剧<br/>自旋超过阈值| D[重量级锁]

    B -.特点.-> B1[Mark Word 记录线程ID<br/>同一线程再次进入时<br/>几乎无额外开销]
    C -.特点.-> C1[通过 CAS + 自旋尝试抢锁<br/>尽量避免线程阻塞]
    D -.特点.-> D1[竞争失败线程进入阻塞队列<br/>需要 OS 参与挂起与唤醒<br/>开销最大]

    style A fill:#22c55e,color:#fff,stroke:#15803d,stroke-width:2px
    style B fill:#3b82f6,color:#fff,stroke:#1d4ed8,stroke-width:2px
    style C fill:#f59e0b,color:#fff,stroke:#b45309,stroke-width:2px
    style D fill:#ef4444,color:#fff,stroke:#991b1b,stroke-width:2px

    style B1 fill:#dbeafe,color:#1e3a8a,stroke:#60a5fa,stroke-width:1.5px
    style C1 fill:#fef3c7,color:#92400e,stroke:#f59e0b,stroke-width:1.5px
    style D1 fill:#fee2e2,color:#991b1b,stroke:#ef4444,stroke-width:1.5px


5. ReentrantLock锁

ReentrantLock 底层基于 AQS(AbstractQueuedSynchronizer) 实现,AQS 通过一个 volatile int state 表示同步状态,通过 CAS 修改 state,并维护一个 CLH 变体的双向等待队列来管理获取锁失败的线程。

ReentrantLock和synchronized关键词的区别?

对比项synchronizedReentrantLock
类型Java 关键字JDK 锁类
实现层面JVMJDK(AQS)
加锁/释放自动手动
是否可重入
是否支持公平锁
是否支持可中断
是否支持超时获取锁
条件队列wait/notify,单一隐式队列Condition,支持多个条件队列
性能JDK6 后优化很好高灵活性场景更有优势

6. AQS相关

AQS(AbstractQueuedSynchronizer) 是 Java 并发包的核心基础框架,Doug Lea 大神设计,提供了一套基于 FIFO 等待队列的同步器实现框架,ReentrantLock、Semaphore、CountDownLatch 等都基于它实现!

2. AQS锁的是什么?

AQS 本身不锁任何东西,它只是一个框架!它提供两个核心能力:

  • 通过 CAS 原子管理 state 变量

  • 通过 CLH 队列管理等待线程的 park/unpark

AQS 锁的本质:

graph TB
    A[AQS到底锁的是什么?] --> B[AQS本身不锁任何东西]
    B --> C[AQS只提供两个能力]
    C --> D[能力1\n管理state变量\nCAS原子修改]
    C --> E[能力2\n管理等待队列\npark/unpark线程]

    D --> F[谁抢到state\n谁就算获取了同步器]
    E --> G[抢不到的线程\n进队列挂起等待]

    F & G --> H[具体state代表什么\n完全由子类定义]

    style A fill:#6366f1,color:#fff
    style H fill:#52C41A,color:#fff

不同子类实现中,state的含义不同:

graph LR
    subgraph state语义对比
        A[ReentrantLock\nstate=重入次数\n0无锁\n大于0已锁]
        B[Semaphore\nstate=剩余许可证\n大于0可获取\n等于0阻塞]
        C[CountDownLatch\nstate=计数器\n大于0阻塞等待\n等于0全部放行]
        D[ReadWriteLock\nstate高16位=读锁\nstate低16位=写锁]
    end

    style A fill:#4A90D9,color:#fff
    style B fill:#E8A838,color:#fff
    style C fill:#52C41A,color:#fff
    style D fill:#ef4444,color:#fff

3. AQS的主要组成部分,核心结构,AQS 核心变量有哪些?

AQS主要由三部分组成:

  • state 同步状态
  • CLH先进先出双向队列(包含头head和尾tail),线程从尾部入队,通过CAS方式修改tail
  • Condition条件队列
/**

AQS
├── state:资源状态
├── Sync Queue:同步队列
│   ├── head
│   ├── tail
│   └── Node
│       ├── waitStatus
│       ├── prev
│       ├── next
│       ├── thread
│       └── nextWaiter
├── CAS:修改 state / 入队
└── park/unpark:阻塞与唤醒

*/

public abstract class AbstractQueuedSynchronizer
    extends AbstractOwnableSynchronizer
    implements java.io.Serializable {

    // 同步状态
    private volatile int state;

    // 同步队列头节点
    private transient volatile Node head;

    // 同步队列尾节点
    private transient volatile Node tail;

    static final class Node {
        // 等待状态
        volatile int waitStatus;

        // 前驱节点
        volatile Node prev;

        // 后继节点
        volatile Node next;

        // 当前线程
        volatile Thread thread;

        // 条件队列 / 模式标记
        Node nextWaiter;
    }
}

4. CLH 等待队列是什么?有哪些作用?为什么是双向链表而不是单向链表?

CLH 队列是 AQS 中用来管理获取锁失败的等待线程的双向链表队列,线程获取锁失败后封装成 Node 入队并 park 挂起,锁释放后 unpark 唤醒队头线程重新竞争!用双向链表是因为取消节点、唤醒后继、从尾往前遍历这些操作都需要访问前驱节点,单向链表做不到!

CLH队列结构:

flowchart LR
    LOCK[当前持锁线程 X] -->|unlock / release| WAKE[唤醒 head 的后继节点]
    WAKE --> N1

    subgraph Q[CLH 双向等待队列]
        H[head<br/>哑节点]
        N1[Node A<br/>线程A<br/>首个有效等待节点]
        N2[Node B<br/>线程B]
        N3[Node C<br/>线程C]

        H -->|next| N1 -->|next| N2 -->|next| N3
        N3 -.prev.-> N2
        N2 -.prev.-> N1
        N1 -.prev.-> H
    end

    style LOCK fill:#ef4444,color:#fff,stroke:#991b1b,stroke-width:2px
    style WAKE fill:#22c55e,color:#fff,stroke:#15803d,stroke-width:2px
    style H fill:#9ca3af,color:#fff,stroke:#6b7280,stroke-width:2px
    style N1 fill:#2563eb,color:#fff,stroke:#1d4ed8,stroke-width:2px
    style N2 fill:#60a5fa,color:#fff,stroke:#2563eb,stroke-width:2px
    style N3 fill:#93c5fd,color:#1e3a8a,stroke:#3b82f6,stroke-width:2px

CHL队列的作用:

  • 作用一:管理等待线程,保证有序,线程等待超时或被中断,需要从队列移除
  • 作用二:实现公平性,队列中挂起的线程按照先来后到等待被唤醒

为什么用双向链表?

双向链表的话,当某个待被唤醒的节点线程是取消或者中断状态,需要从链表中移除,只需要移动前后指针即可。如果是单向链表的话需要遍历一次,拿到前或者后节点然后移动指针

如下图所示:

image.png

5.AQS 中 Node 的 waitStatus 有哪些值,分别表示什么?

waitStatus 共有 5个值:0(初始)、-1 SIGNAL、-2 CONDITION、-3 PROPAGATE、1 CANCELLED,记住规律:大于0就是取消,小于0都是有效状态!

static final class Node {
    static final int CANCELLED =  1; // 已取消
    static final int SIGNAL    = -1; // 后继需要唤醒
    static final int CONDITION = -2; // 条件队列等待
    static final int PROPAGATE = -3; // 共享模式传播
    //              0               // 初始状态
    volatile int waitStatus;
}

图示如下:

graph TB
    A[waitStatus] --> B[0\n初始状态]
    A --> C[-1 SIGNAL\n最重要]
    A --> D[-2 CONDITION\n条件队列]
    A --> E[-3 PROPAGATE\n共享传播]
    A --> F[1 CANCELLED\n已取消]

    B --> B1[节点刚创建\n默认值]
    C --> C1[后继节点需要被唤醒\npark之前必须设置]
    D --> D1[节点在条件队列等待\nawait后变为此状态]
    E --> E1[共享模式下\n传播唤醒后续节点]
    F --> F1[超时或中断\n节点废弃不再竞争]

    style B fill:#9ca3af,color:#fff
    style C fill:#52C41A,color:#fff
    style D fill:#E8A838,color:#fff
    style E fill:#4A90D9,color:#fff
    style F fill:#ef4444,color:#fff

6.Condition 条件队列的作用是什么?Condition 如何精确唤醒线程?Condition 原理?wait和notify、park和unpark?以及 Condition 和 CLH 两个队列的关系?

Condition 是什么?作用是什么?

Condition 的核心作用是线程间通信,解决 Object.wait/notify 只有一个等待集合、无法精确唤醒指定类型线程的问题!一个 Lock 可以创建多个 Condition,每个对应独立等待队列,实现精确唤醒!

Condition 是通过两个单向链表实现:

// ConditionObject 是 AQS 的内部类
public class ConditionObject implements Condition {

    // 条件队列头节点
    private transient Node firstWaiter;

    // 条件队列尾节点
    private transient Node lastWaiter;

    // 核心方法
    void await();           // 释放锁,进入条件队列等待
    void signal();          // 唤醒条件队列头节点
    void signalAll();       // 唤醒条件队列所有节点
    void awaitUninterruptibly(); // 不响应中断的等待
    boolean await(long time, TimeUnit unit); // 超时等待
}

Condition 和 CLH 两个队列的关系?

CLH队列是用来解决锁竞争问题的,所有线程都在抢同一把锁!但实际业务中,线程等待的原因不同,需要在不同条件满足时唤醒不同类型的线程,CLH队列做不到这个!Condition就是为了解决按条件精确唤醒的问题!

CLH 是等锁,Condition 是等条件,两种等待原因完全不同,所以需要两个队列分别管理!

Condition 是如何精确唤醒某个线程的?

每个 Condition 对象内部有一个独立的条件等待队列,不同 Condition 的队列完全隔离互不干扰!

流程与代码案例:

await():线程调用哪个 Condition 的 await(),就进哪个 Condition 的队列,同时释放锁 park 挂起。

signal():只操作当前 Condition 自己队列的 firstWaiter,把它转移到 CLH 同步队列,然后 unpark 唤醒。

public class BoundedBuffer {

    private final ReentrantLock lock = new ReentrantLock();

    // 两个独立条件队列
    private final Condition notFull  = lock.newCondition(); // 生产者等待队列
    private final Condition notEmpty = lock.newCondition(); // 消费者等待队列

    private final Queue<Integer> queue = new LinkedList<>();
    private final int capacity = 5;

    // ===== 生产者 =====
    public void produce(int item) throws InterruptedException {
        lock.lock();
        try {
            // 队列满了,生产者去 notFull 队列等待
            while (queue.size() == capacity) {
                System.out.println(Thread.currentThread().getName()
                    + " 队列满了,生产者等待...");
                notFull.await(); // ← 进入 notFull 条件队列💤
            }

            queue.offer(item);
            System.out.println(Thread.currentThread().getName()
                + " 生产:" + item + " 队列大小:" + queue.size());

            // 生产了一个,通知消费者可以消费了
            notEmpty.signal(); // ← 只唤醒 notEmpty 队列的消费者!

        } finally {
            lock.unlock();
        }
    }

    // ===== 消费者 =====
    public void consume() throws InterruptedException {
        lock.lock();
        try {
            // 队列空了,消费者去 notEmpty 队列等待
            while (queue.isEmpty()) {
                System.out.println(Thread.currentThread().getName()
                    + " 队列空了,消费者等待...");
                notEmpty.await(); // ← 进入 notEmpty 条件队列💤
            }

            int item = queue.poll();
            System.out.println(Thread.currentThread().getName()
                + " 消费:" + item + " 队列大小:" + queue.size());

            // 消费了一个,通知生产者可以生产了
            notFull.signal(); // ← 只唤醒 notFull 队列的生产者!

        } finally {
            lock.unlock();
        }
    }

    public static void main(String[] args) {
        BoundedBuffer buffer = new BoundedBuffer();

        // 3个生产者
        for (int i = 0; i < 3; i++) {
            int item = i;
            new Thread(() -> {
                for (int j = 0; j < 5; j++) {
                    try {
                        buffer.produce(item * 10 + j);
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }, "生产者-" + i).start();
        }

        // 3个消费者
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                for (int j = 0; j < 5; j++) {
                    try {
                        buffer.consume();
                        Thread.sleep(150);
                    } catch (InterruptedException e) {
                        Thread.currentThread().interrupt();
                    }
                }
            }, "消费者-" + i).start();
        }
    }
}

输出结果:

输出结果:
生产者-0 生产:0  队列大小:1
生产者-1 生产:10 队列大小:2
生产者-2 生产:20 队列大小:3
消费者-0 消费:0  队列大小:2
消费者-1 消费:10 队列大小:1
生产者-0 生产:1  队列大小:2
...
生产者-0 队列满了,生产者等待...  ← 进notFull队列
消费者-0 消费:xx 队列大小:4      ← 消费后signal通知生产者
生产者-0 生产:xx 队列大小:5      ← 生产者被精确唤醒!不会唤醒消费者!

流程图:

graph TB
    LOCK[一把Lock]
    LOCK --> NF[notFull条件队列\n专门放生产者]
    LOCK --> NE[notEmpty条件队列\n专门放消费者]

    subgraph notFull队列
        P1([生产者A]) -->|nextWaiter| P2([生产者C])
    end

    subgraph notEmpty队列
        C1([消费者B]) -->|nextWaiter| C2([消费者D])
    end

    NF --> P1
    NE --> C1

    OPS1[消费了一个元素] -->|notFull.signal\n精确唤醒生产者| P1
    OPS2[生产了一个元素] -->|notEmpty.signal\n精确唤醒消费者| C1

    style P1 fill:#E8A838,color:#fff
    style P2 fill:#E8A838,color:#fff
    style C1 fill:#4A90D9,color:#fff
    style C2 fill:#4A90D9,color:#fff
    style OPS1 fill:#52C41A,color:#fff
    style OPS2 fill:#52C41A,color:#fff

本质上是用业务代码控制线程去哪个队列:生产者调 notFull.await() 进生产者队列,消费者调 notEmpty.await() 进消费者队列,signal 时各取各的,天然精确!

7.AQS 获取锁失败后线程怎么处理?

获取锁失败后,线程被封装成 Node 节点 CAS 加入 CLH 队列尾部,然后进入自旋:如果前驱是 head 则再次 tryAcquire 尝试抢锁;抢不到则检查前驱的 waitStatus,如果前驱是 SIGNAL=-1 说明前驱释放锁时会唤醒自己,就放心调用 LockSupport.park 挂起;如果前驱已取消(CANCELLED=1)则跳过找有效前驱;如果前驱是 0 则 CAS 设为 SIGNAL 再重新判断。线程挂起后等待前驱节点释放锁时 unpark 唤醒,醒来后重新自旋竞争锁!

具体流程如下:

graph LR

A[获取锁失败] --> B[封装 Node]
B --> C[CAS 入队]
C --> D[进入自旋]
D --> E{前驱是否为 head}

E -->|是| F[再次 tryAcquire]
F --> G{是否成功}
G -->|成功| H[获取锁成功]
G -->|失败| I[检查前驱状态]

E -->|否| I

I --> J{waitStatus}
J -->|SIGNAL| K[park 挂起]
K --> L[unpark 后继续竞争]
L --> D

J -->|CANCELLED| M[跳过失效前驱]
M --> D

J -->|0| N[设为 SIGNAL]
N --> D

style A fill:#ef4444,color:#fff
style B fill:#3b82f6,color:#fff
style C fill:#3b82f6,color:#fff
style D fill:#6366f1,color:#fff
style E fill:#f3f4f6,color:#111827
style F fill:#2563eb,color:#fff
style G fill:#f3f4f6,color:#111827
style H fill:#22c55e,color:#fff
style I fill:#f59e0b,color:#fff
style J fill:#f3f4f6,color:#111827
style K fill:#ef4444,color:#fff
style L fill:#22c55e,color:#fff
style M fill:#f97316,color:#fff
style N fill:#eab308,color:#fff

8.AQS加锁和释放锁的流程?

加锁流程

graph TD
    A([线程调用lock]) --> B[tryAcquire\n尝试获取锁]

    B --> C([获取锁成功\nowner=当前线程\nstate=1])
    B --> D([同一线程重入\nstate++])
    B --> E[获取锁失败\naddWaiter\n封装Node加入CLH队列尾部]

    E --> F[acquireQueued\n进入自旋]
    F --> G[检查前驱节点是否是head]

    G --> H[前驱是head\ntryAcquire再次尝试获取锁]
    G --> I[前驱不是head\n检查前驱waitStatus]
    H --> C
    H --> I

    I --> J[SIGNAL=-1\n前驱释放锁会唤醒我\n可以安心挂起]
    I --> K[CANCELLED=1\n前驱已取消\n跳过找有效前驱]
    I --> L[waitStatus=0\nCAS设前驱为SIGNAL=-1]

    K --> F
    L --> F

    J --> M[LockSupport.park\n线程挂起 WAITING状态]
    M --> N[线程被唤醒]

    N --> O[正常unpark唤醒\n继续自旋竞争锁]
    N --> P[interrupt中断唤醒\n记录中断标记继续自旋]

    O --> F
    P --> F

    style C fill:#52C41A,color:#fff
    style D fill:#52C41A,color:#fff
    style M fill:#ef4444,color:#fff
    style J fill:#4A90D9,color:#fff
    style K fill:#E8A838,color:#fff
    style L fill:#9ca3af,color:#fff

释放锁流程

graph TD
    A([线程调用unlock]) --> B[tryRelease\nstate--]

    B --> C[state是否等于0\n判断是否完全释放]

    C --> D([state大于0\n存在重入\n继续持有锁])
    C --> E[state等于0\n完全释放\nowner=null\n清空持有线程]

    E --> F[检查CLH队列\nhead是否为空]

    F --> G([head为空\n队列无等待线程\n释放完成])
    F --> H[head不为空\n检查head的waitStatus]

    H --> I[head.waitStatus=0\n无需唤醒\n没有等待线程]
    H --> J[head.waitStatus=SIGNAL=-1\n有线程等待\n需要唤醒后继]
    H --> K[head.waitStatus=PROPAGATE=-3\n共享模式\n需要传播唤醒]

    I --> G

    J --> L[检查head.next\n后继节点是否有效]
    K --> L

    L --> M[head.next有效\nwaitStatus小于等于0\n直接unpark唤醒]
    L --> N[head.next无效\nCANCELLED=1已取消\n从tail往前遍历\n找最近有效节点]

    N --> M

    M --> O([LockSupport.unpark\n唤醒目标线程])
    O --> P([线程醒来\n重新自旋竞争锁])

    style D fill:#E8A838,color:#fff
    style G fill:#52C41A,color:#fff
    style O fill:#4A90D9,color:#fff
    style P fill:#6366f1,color:#fff
    style J fill:#4A90D9,color:#fff
    style K fill:#9ca3af,color:#fff
    style I fill:#9ca3af,color:#fff

9.AQS如何实现可重入锁?如何实现公平锁?

AQS实现可重入锁

graph TD
    subgraph 释放锁流程
        E([线程调用unlock]) --> F[tryRelease\nstate--]
        F --> G[state是否等于0]
        G --> H([state大于0\n还有重入未释放\n继续持有锁])
        G --> I[state等于0\n完全释放\nowner=null]
        I --> J([unpark唤醒\nCLH队列下一个等待线程])
    end

    subgraph 加锁流程
        A([线程调用lock]) --> B[tryAcquire\n获取state值]
        B --> C[state等于0\n无锁状态]
        B --> D[state大于0\n已有线程持有锁]
        C --> CA[CAS state 0到1\n设置owner=当前线程]
        CA --> CB([获取锁成功\nstate=1])
        D --> DA[判断owner\n是否是当前线程]
        DA --> DB([不是当前线程\n获取锁失败\n进入CLH队列等待])
        DA --> DC[是当前线程\n可以重入\nstate++]
        DC --> DD([重入成功\nstate=n])
    end

    style CB fill:#52C41A,color:#fff
    style DD fill:#52C41A,color:#fff
    style DB fill:#ef4444,color:#fff
    style H fill:#E8A838,color:#fff
    style J fill:#52C41A,color:#fff

AQS实现公平锁

所有等待被唤醒的线程在CLH队列中按照先来后到顺序依次被唤醒,而不需要每次重新竞争抢锁。

graph TD
    subgraph 非公平锁 NonfairSync
        A1([线程调用lock]) --> B1[CAS直接抢锁\n不检查队列]
        B1 --> C1[成功]
        B1 --> D1[失败\n再走tryAcquire]
        C1 --> E1([获取锁成功\n允许插队])
        D1 --> F1[tryAcquire\n再次直接CAS]
        F1 --> G1([成功获取锁])
        F1 --> H1([失败入队等待])
    end

    subgraph 公平锁 FairSync
        A2([线程调用lock]) --> B2[tryAcquire\n先检查队列]
        B2 --> C2[hasQueuedPredecessors\n队列有等待线程?]
        C2 --> D2[有等待线程\n不允许插队]
        C2 --> E2[无等待线程\nCAS抢锁]
        D2 --> F2([入队排队等待])
        E2 --> G2([获取锁成功])
    end

    style E1 fill:#52C41A,color:#fff
    style G1 fill:#52C41A,color:#fff
    style G2 fill:#52C41A,color:#fff
    style F2 fill:#4A90D9,color:#fff
    style H1 fill:#ef4444,color:#fff
    style D2 fill:#ef4444,color:#fff

10.AQS独占模式和共享模式区别?

独占模式:同一时刻只有一个线程能持有锁,其他线程全部等待,如 ReentrantLock!

共享模式:同一时刻允许多个线程同时持有,如 Semaphore、CountDownLatch!核心区别在于 state 的语义和锁释放后是否传播唤醒后续节点!

graph TD
    A[独占模式] --> A1[ReentrantLock\n同一时刻只有一个线程\n适合写操作互斥场景]
    A --> A2[ReentrantReadWriteLock写锁\n写时不允许任何读写\n保证写操作原子性]

    B[共享模式] --> B1[Semaphore信号量\n控制同时访问资源的线程数\n适合限流场景]
    B --> B2[CountDownLatch\n等待多个线程都完成\n适合线程协调场景]
    B --> B3[ReentrantReadWriteLock读锁\n多个读线程同时读\n适合读多写少场景]
    B --> B4[CyclicBarrier\n等待所有线程到达屏障\n适合分阶段任务场景]

    style A fill:#4A90D9,color:#fff
    style B fill:#52C41A,color:#fff


7. TreadLocal

ThreadLocal是一个全局对象,ThreadLocal是线程范围内变量共享的解决方案。

1. ThreadLocal 数据结构,ThreadLocal、Thread、ThreadLocalMap之间的关系?

一个Thread里面,有一份自己的threadLocalMap,他的key是某个ThreadLocal对象实例的弱引用,value是ThreadLocal对象中包含的数据(强引用)

TreadLocal自己没有table数组,而是用的Thread里的threadLocalMap中的table数组。

数据结构图解

graph TB
    subgraph Thread1["🧵 同一个 Thread"]
        TLMap["threadLocals\n(只有一个ThreadLocalMap)"]
    end

    subgraph ThreadLocalMap["📦 ThreadLocalMap (一个线程只有一个)"]
        subgraph Table["Entry[] table"]
            E1["Entry[i]\n✅ 有数据"]
            E2["Entry[j]\n✅ 有数据"]
            E3["Entry[k]\n✅ 有数据"]
            E4["Entry[m]\n空 null"]
        end
    end

    subgraph TLObjects["🔑 多个ThreadLocal实例 (可以有很多个)"]
        TLA["ThreadLocal-A\n存储用户信息\nhashCode=1111"]
        TLB["ThreadLocal-B\n存储事务ID\nhashCode=2222"]
        TLC["ThreadLocal-C\n存储链路追踪ID\nhashCode=3333"]
    end

    subgraph Values["📦 各自存储的Value"]
        VA["UserInfo\n{'name':'Tom'}"]
        VB["TransactionId\n'TX-001'"]
        VC["TraceId\n'TRACE-ABC'"]
    end

    %% Thread → Map
    TLMap -->|"持有唯一Map"| ThreadLocalMap

    %% Entry → Value
    E1 -->|"value"| VA
    E2 -->|"value"| VB
    E3 -->|"value"| VC

    %% ThreadLocal(key弱引用) → Entry
    TLA -.->|"弱引用 key"| E1
    TLB -.->|"弱引用 key"| E2
    TLC -.->|"弱引用 key"| E3

    style Thread1 fill:#4A90D9,color:#fff
    style ThreadLocalMap fill:#7B68EE,color:#fff
    style TLA fill:#50C878,color:#fff
    style TLB fill:#50C878,color:#fff
    style TLC fill:#50C878,color:#fff
    style E1 fill:#E8A838,color:#fff
    style E2 fill:#E8A838,color:#fff
    style E3 fill:#E8A838,color:#fff
    style E4 fill:#ccc,color:#666
    style Values fill:#fff8e1,stroke:#f0ad4e

ThreadLocal 为什么线程安全?

ThreadLocal中存放的数据是使用Thread下的threadlocalMap作为容器的,即线程独享。

ThreadLocalMap 的 key 和 value 分别是什么?

key是某个ThreadLocal对象实例的弱引用,value是ThreadLocal对象中包含的数据(强引用)。

例如UserContext内定义了一个TreadLocal对象

UserInfo user = UserContext.get();

即向Thread获取TreadLocalMap,key为UserInfo内TreadLocal实例弱引用,value为UserInfo

图解关系:

graph TB
    subgraph UserContext["📦 UserContext 类 (工具类)"]
        TL["🔑 USER_THREAD_LOCAL\n= new ThreadLocal#lt;UserInfo#gt;()\n← ThreadLocal定义在这里!"]
    end

    subgraph UserInfo["📋 UserInfo 类 (纯数据类)"]
        F1["String name = 'Tom'"]
        F2["Integer age = 18"]
        F3["String role = 'Admin'"]
        Note["❌ UserInfo里没有ThreadLocal\n它只是普通的数据对象"]
    end

    subgraph Thread["🧵 Thread"]
        subgraph TLMap["ThreadLocalMap"]
            subgraph Entry["Entry[i]"]
                K["🔑 key\n= WeakRef(USER_THREAD_LOCAL)\n弱引用指向UserContext中的TL实例"]
                V["💾 value\n= UserInfo对象\n{ name='Tom',age=18 }"]
            end
        end
    end

    TL -.->|"弱引用 作为key"| K
    V -->|"强引用 指向"| UserInfo

    style UserContext fill:#4A90D9,color:#fff
    style UserInfo fill:#ccc,color:#333
    style Note fill:#FFE5E5,color:#FF4444
    style Thread fill:#7B68EE,color:#fff
    style K fill:#50C878,color:#fff
    style V fill:#E8A838,color:#fff
    style TL fill:#50C878,color:#fff

2. 初始容量,扩容机制

Tread中的TreadLocalMap初始table数组容量为16,必须为2的n次幂。扩容阈值为容量的2/3。扩容后新table数组大小为旧数组的2倍。

3. set、get核心流程

set流程

flowchart TD
    Start(["🚀 threadLocal.set(value)"])
    --> GetThread["① 获取当前线程\nThread t = Thread.currentThread()"]
    --> GetMap["② 获取线程的Map\nmap = t.threadLocals"]
    --> MapNull{"③ map 是否为null?\n首次使用?"}

    MapNull -->|"是 首次使用"| CreateMap["createMap(t, value)\n创建ThreadLocalMap\ncapacity=16, threshold=10\n直接创建第一个Entry"]
    MapNull -->|"否 已存在"| CalcIndex["④ 计算槽位\ni = hashCode & table.length-1"]

    CreateMap --> SetEnd(["✅ set完成"])

    CalcIndex --> LoopStart{"⑤ 检查 table[i]"}

    LoopStart -->|"Entry.key == this\n命中当前ThreadLocal"| Update["直接更新 value\ne.value = value"]
    LoopStart -->|"Entry == null\n找到空槽"| Insert["插入新 Entry\ntable[i] = new Entry(this,value)\nsize++\n检查是否扩容"]
    LoopStart -->|"Entry.key != this\n哈希冲突"| NextIndex["线性探测\ni = (i+1) & len-1"]
    LoopStart -->|"Entry.key == null\n脏Entry"| Clean["replaceStaleEntry()\n清理脏Entry后插入"]

    NextIndex --> LoopStart
    Update --> SetEnd
    Insert --> CheckResize{"size >= threshold?"}
    Clean --> SetEnd

    CheckResize -->|"是"| Rehash["rehash()\n清理脏Entry\n必要时resize()"]
    CheckResize -->|"否"| SetEnd
    Rehash --> SetEnd

    style Start fill:#4A90D9,color:#fff
    style SetEnd fill:#50C878,color:#fff
    style CreateMap fill:#50C878,color:#fff
    style Clean fill:#FF8C00,color:#fff
    style Rehash fill:#FF8C00,color:#fff
    style Update fill:#4A90D9,color:#fff
    style Insert fill:#4A90D9,color:#fff

get流程

flowchart TD
    Start(["🚀 threadLocal.get()"])
    --> GetThread["① 获取当前线程\nThread t = Thread.currentThread()"]
    --> GetMap["② 获取线程的Map\nmap = getMap(t)\n即 t.threadLocals"]
    --> MapNull{"③ map 是否为 null?\n线程从未调用过set()"}

    MapNull -->|"是\n从未初始化"| InitValue["⑥ setInitialValue()\n调用 initialValue()\n默认返回 null\n可Override自定义"]

    MapNull -->|"否\n已初始化"| GetEntry["④ 获取Entry\ne = map.getEntry(this)\nthis=当前ThreadLocal实例"]

    GetEntry --> CalcIndex["计算槽位\ni = hashCode & table.length-1"]

    CalcIndex --> DirectHit{"⑤ 直接命中?\ntable[i] != null\n且 key == this"}

    DirectHit -->|"✅ 命中\nO(1)最优情况"| ReturnValue(["🎉 返回 entry.value"])

    DirectHit -->|"❌ 未命中\n哈希冲突或key不同"| AfterMiss["getEntryAfterMiss()\n开始线性探测"]

    AfterMiss --> ProbeLoop{"探测 table[i]"}

    ProbeLoop -->|"Entry==null\n空槽 说明不存在"| ReturnNull(["返回 null\n或初始值"])

    ProbeLoop -->|"key==this\n✅ 找到了"| ReturnValue

    ProbeLoop -->|"key==null\n脏Entry"| Expunge["expungeStaleEntry(i)\n清理脏Entry\n重新哈希后续Entry"]

    ProbeLoop -->|"key!=this\n其他ThreadLocal"| NextProbe["线性探测\ni=(i+1)&len-1"]

    Expunge --> NextProbe
    NextProbe --> ProbeLoop

    InitValue --> CreateMap{"map是否为null"}
    CreateMap -->|"是"| NewMap["createMap(t, initialVal)\n创建新ThreadLocalMap"]
    CreateMap -->|"否"| SetInit["map.set(this, initialVal)"]
    NewMap --> ReturnInit(["返回 initialValue"])
    SetInit --> ReturnInit

    style Start fill:#4A90D9,color:#fff
    style ReturnValue fill:#50C878,color:#fff
    style ReturnNull fill:#ccc,color:#333
    style ReturnInit fill:#7B68EE,color:#fff
    style Expunge fill:#FF8C00,color:#fff
    style InitValue fill:#7B68EE,color:#fff

ThreadLocalMap 如何解决 hash 冲突?

Thread下的TreadLocalMap虽然是一个Map结构,但是实际上遇到hash冲突时不会形成链表,而是线形探测转移到另一个桶去,如果还是冲突就继续探测。

线形探测算法:

i = (i + 1) & (len - 1)

Q:如果线形探测到table数组末尾或者边界,怎么办?

A:自动回到数组头部,形成环形;

Q:如果探测了整个数组还没找到空桶位怎么办?

A:不可能出现这个情况,因为数组始终是不满状态,容量占用到临界阈值就自动扩容了,所以始终会有充裕,不会出现数组全满情况。

4. key 为什么设计成弱引用?

为什么key要为弱引用?

线程方法执行结束,可以使ThreadLocal的key引用在GC时被主动回收,不需要像强引用那样要主动清除key引用。当key变为null的时候,下次set/get时,会通过TreadLocal自己的清理逻辑将value也回收。

弱引用能完全解决内存泄漏吗?为什么?

不能。例如线程池场景、静态变量修饰等。参考下面问题的解释。

5. ThreadLocal 为什么会内存泄漏?如何正确避免内存泄漏?

什情况下会出现内存泄露?

  • 1.线程池中使用TreadLocal,在线程任务完结的时候没有主动remove销毁。线程池回收这个线程实例,但是线程对象本身不会被销毁,所以TreadLocalMap中存放的数据也不会被回收,无效占用内存。
  • 2.key 变 null 后 value 无法回收。

原理:key变为null后,弱引用被GC回收,无法通过get/set定位到数组桶位上的Entry(无法通过寻址算法定位桶位),但是这个ThreadLocal中存放的value对象还被Entry强引用着无法回收,无效占用内存。

解决办法:

  • 等待下次set/get时TreadLocal自己的探测清理,但是不能保证一定可以清理
  • 等待线程被回收
  • 在线程方法中finally中提前调用remove。
  • TheadLocal对象被静态变量修饰,key永远不会被GC回收,例如在UserContext中使用。

如何避免内存泄露?

  • 1.TreadLocal存储信息用完后,在线程任务完结的时候主动remove销毁。例如登陆用户会话信息,在请求执行完结后再拦截器postProcessor中销毁。
  • 2.避免使用线程池中的线程对象去使用TreadLocal,防止线程执行完后被线程池回收但线程并未销毁。
  • 3.headLocal对象被静态变量修饰,key永远不会被GC回收,例如在UserContext中使用。

什么情况下key会变成null?

示例代码:

public class OrderService {
    
    public void processOrder(Order order) {
        
        // ⚠️ 局部变量!不是static!
        ThreadLocal<Order> orderTL = new ThreadLocal<>();
        orderTL.set(order);
        
        doSomething();
        
        // ❌ 忘记 remove()
        // 方法结束,orderTL 局部变量销毁
        // 强引用断开!
        
    } // ← 方法结束,orderTL 出栈,强引用消失
    
}

流程图解:

flowchart TB
    subgraph MethodRun["方法执行中"]
        subgraph Stack["线程栈"]
            LocalRef["局部变量\norderTL\n────强引用────→ ThreadLocal对象"]
        end
        subgraph Heap["堆内存"]
            TLObj["ThreadLocal对象"]
            EntryN["Entry\nkey ⇢ 弱引用 ⇢ ThreadLocal\nvalue → Order数据"]
        end
        LocalRef --> TLObj
        EntryN -.-> TLObj
    end

    subgraph MethodEnd["方法结束后"]
        subgraph Stack2["线程栈"]
            LocalRef2["orderTL 出栈\n强引用消失!"]
        end
        subgraph Heap2["堆内存"]
            TLObj2["ThreadLocal对象\n只剩弱引用指向它\nGC触发 → 被回收!"]
            EntryN2["Entry\nkey = null ⚠️\nvalue → Order数据 仍存活!"]
        end
        LocalRef2 -.->|"强引用断开"| TLObj2
        EntryN2 -.->|"弱引用\n已回收"| TLObj2
    end

    MethodRun -->|"方法结束"| MethodEnd

    style LocalRef2 fill:#FF6B6B,color:#fff
    style TLObj2 fill:#FF6B6B,color:#fff
    style EntryN2 fill:#FF8C00,color:#fff

6. 父子线程能共享 ThreadLocal 吗?

ThreadLocal 不能,因为Tread的threadLocalMap是当前线程使用,不考虑父线程。

如果需要子父线程使用,可以用 InheritableThreadLocal

7. ThreadLocalMap过期 key 的清理流程?

什么是探测清理

触发时机:发现某个脏Entry后,从这个位置出发向后线性扫描。

清理动作:遇到 key=null 的脏Entry 清理。遇到正常Entry 重新哈希。

停止时机:遇到 null 空槽停止。

完整流程图解

image.png

启发式清理

触发时机:新写入一个Entry后,开始log₂(n) 次抽样

清理动作:遇到 key=null 的脏Entry 清理。遇到正常Entry 重新哈希。

停止时机:抽样结束

8. 线程池

TODO


四、JVM部分

1.运行时数据区

1.7和1.8版本差异

image.png

组成部分:

JVM 运行时数据区分为线程私有线程共享两部分。

线程私有(生命周期与线程相同)包含三个区域:

  • 程序计数器:记录当前线程执行的字节码指令地址,是唯一不会发生 OOM 的区域
  • 虚拟机栈:每个方法调用对应一个栈帧,存放局部变量表、操作数栈、动态链接、方法返回地址
  • 本地方法栈:为 native 方法服务,HotSpot 虚拟机将其与虚拟机栈合并为一个

线程共享包含两个区域:

  • :存放几乎所有对象实例和数组,分为新生代(Eden + 两个 Survivor)和老年代
  • 方法区:存放类元信息、即时编译代码缓存、常量池、静态变量

JDK 1.7 vs 1.8 差异

两个版本最核心的差异在于方法区的实现方式不同:

  • JDK 1.7 用永久代实现方法区,永久代位于堆内存中,受 -XX:MaxPermSize 限制,默认大小较小,存放类元信息、运行时常量池、静态变量,容易触发 OutOfMemoryError: PermGen space
  • JDK 1.8 废除永久代,改用元空间实现方法区,元空间位于本地内存(堆外),默认没有大小上限,受本地内存限制,只存放类元信息和即时编译代码,触发的是 OutOfMemoryError: Metaspace

同时伴随这个变化,常量池和静态变量的存放位置也发生了迁移:字符串常量池在 JDK 1.7 就已经从永久代移入堆中,静态变量在 JDK 1.8 随 Class 对象一起移入堆中。

之所以做这个改变,主要原因是永久代大小难以评估、GC 回收效率低,使用本地内存的元空间可以动态扩展,大幅降低了 OOM 的风险。

每个区域存放什么内容

image.png

直接内存的作用是?

TODO

字符串常量池的作用?

字符串常量池 是 JVM 为了提升性能和减少内存消耗针对字符串(String 类)专门开辟的一块区域,主要目的是为了避免字符串的重复创建。

// 1.在字符串常量池中查询字符串对象 "ab",如果没有则创建"ab"并放入字符串常量池
// 2.将字符串对象 "ab" 的引用赋值给 aa
String aa = "ab";
// 直接返回字符串常量池中字符串对象 "ab",赋值给引用 bb
String bb = "ab";
System.out.println(aa==bb); // true

2.JMM内存模型

JMM(Java Memory Model)是 Java 的并发内存访问模型,定义了线程工作内存与主内存之间的交互规则,用来屏蔽不同硬件和编译器的内存访问差异

在 JMM 里,每个线程都有自己独立的 工作内存(也常叫本地内存),线程之间的工作内存彼此不可直接访问;而 主内存 中存放共享变量,是所有线程共享的。

如下图所示:

  • Java程序运行时,每开辟一个线程都会分配一个固定大小的线程栈区域
  • JVM命令参数控制线程栈内存大小:-Xss1m,默认是512k或1M
  • 新开线程执行时,会将当线程执行代码所需要用到相关变量,从主内存中拷贝到工作内存中,作为变量副本。
  • 线程执行过程中,更新或读取变量副本都是在工作内存执行的,并不会立刻将配置刷新回主内存。
graph TB
    subgraph 线程1工作区
        T1([线程1]) <-->|读写| LM1[本地内存1\n共享变量副本\nx=1]
    end

    subgraph 线程2工作区
        T2([线程2]) <-->|读写| LM2[本地内存2\n共享变量副本\nx=0]
    end

    subgraph 主内存 Main Memory
        X[共享变量x=1]
        Y[共享变量y]
        Z[共享变量z]
    end

    LM1 <-->|同步| X
    LM2 <-->|同步| X

    style T1 fill:#6366f1,color:#fff
    style T2 fill:#6366f1,color:#fff
    style LM1 fill:#fca5a5
    style LM2 fill:#fca5a5
    style X fill:#fde68a
    style Y fill:#fde68a
    style Z fill:#fde68a

JMM三大特性:可见性、原子性、有序性

3.java类加载过程?

4.对象创建过程?

5.垃圾回收机制

6.JVM核心参数


五、数据库部分

1. 分库分表相关

Q:为什么要分库分表?

分库分表,即把原来存放在一个数据库的一张大表里的数据,拆分到多个数据库、多个表中去,降低单表数据量过大查询效率降低。(数据量太大单库单表扛不住了)

常用分表拆分方式:

  • 按照用户ID取模分表
  • 按照时间范围分表
  • 按业务类型分表
例如按 user_id % 4 分表:
user_id % 4 = 0 放 user_order_0
user_id % 4 = 1 放 user_order_1
...
user_order_0
user_order_1
user_order_2
user_order_3

常用分库方式:

  • 按照用户ID取模分库
  • 按照单元区域分库
  • 按业务类型分库

分库分表带来的问题

  • 分页查询成本增加
  • 分布式事务问题
  • 扩容和数据迁移困难

分库分表场景下,如何做分页查询

  • 场景1: 单分片分页,根据用户ID取模或者按天分表,命中单个分片表,等同于单库单表分页
  • 场景2: 跨分片分页,查询数据量小,每个分片取少量数据(例如按照查询条件取每个分片前10条),应用层业务逻辑聚合。
  • 场景3: 跨分片分页,查询数据量大,使用游标分页

排序字段唯一:

order by create_time desc, id desc

每次查询,基于上一页最后一条数据的排序字段值(如 create_time 或 id)作为游标进行查询

-- 假设上一页最后一条数据的 create_time 是 '2023-10-01 12:00:00',id 是 1005
SELECT * FROM order_table 
WHERE create_time > '2023-10-01 12:00:00' 
   OR (create_time = '2023-10-01 12:00:00' AND id > 1005)
ORDER BY create_time, id 
LIMIT 10;

在每个分片,从游标位置开始往后查询,不需要每个分片下全表查询。

应用场景:Feed 流、消息列表、无限滚动加载。

  • 场景4: 跨分片分页,任意跳转某一页,使用二次查询法

第一次查询:查各分片 count

shard_0:80 条
shard_1:120 条
shard_2:50 条

第二步:根据页码推算目标区间

假设:

第 3 页 每页 20 条

那么目标区间就是:

全局第 41 ~ 60 条

第三步:根据各分片数量,算目标区间落在哪些分片

如果全局顺序等于分片顺序拼接,例如:

shard_0 -> shard_1 -> shard_2

那么全局位置区间就是:

shard_0:第 1 ~ 80 条
shard_1:第 81 ~ 200 条
shard_2:第 201 ~ 250 条

第 41~60 条显然就在 shard_0。

如果命中多个分片,需要在每个分片上过滤到业务数据,应用层聚合

2. 索引实效场景

  • 条件中有 or,且 or 两边不能同时命中索引:使用 OR 连接条件时,只要其中一个条件没有索引,整个查询就会失效,导致全表扫描。
  • 对索引列做函数、运算、类型转换
  • 联合索引不满足最左前缀原则:比如索引是 (a,b,c),但查询条件直接写 b=1 and c=2,由于跳过了最左列 a,通常无法有效使用这个联合索引。
  • 使用 !=、<>、not in、not like 等负向条件
  • like 以 % 开头
案例:
索引:name
失效 SQLWHERE name LIKE '%张%'WHERE name LIKE '%张'
原因:B+ 树是从左向右构建的,前缀未知无法定位起点,只能全表扫描。

六、中间件部分

1. Redis相关

2. RPC相关

TODO

3. MQ相关

TODO

4. Netty与网络IO

TODO

七、Spring部分

Q:Spring AOP中的动态代理如何实现?


八、AI部分

TODO

九、场景题

TODO