1. 项目背景
业务场景:某视频平台的"直播带货"模块在高并发抢购场景下,库存扣减逻辑出现了严重的超卖问题。开发组使用的扣减代码看起来非常简单——先查库存、判断是否充足、然后扣减——但压测时 1000 并发请求中有 30+ 次超卖。开发认为加了 synchronized 就"线程安全"了,但测试发现加锁后的接口 P99 延迟从 30ms 飙升到 800ms。
痛点:
- synchronized 的迷思:大多数开发知道
synchronized是"一把锁",但不清楚锁的真实数据结构、锁升级过程(偏向→轻量→重量)、以及与ObjectMonitor的关系。有人以为"加锁就安全",有人以为"加锁就慢"——两个极端都是错误的。 - 线程状态黑盒:当线上出现"线程阻塞"告警时,运维拿着
jstackdump 看到BLOCKED状态一头雾水——不知道这个线程在等哪把锁、锁当前被谁持有、为什么会阻塞这么久。 - wait/notify 的虚假唤醒:业务代码中频繁使用
wait()/notify()做线程协调,但大多数开发不知道wait()必须放在while循环中而非if——这是 Java 语言规范明确要求的防"虚假唤醒"机制。
本章从 Thread 的生命周期出发,深入 synchronized 的底层机制——对象头中的 Mark Word、锁升级三阶段(偏向锁→轻量级锁→重量级锁)、ObjectMonitor 源码入口——最后实现一个线程安全的简易阻塞队列,并用 jstack 观察锁竞争全貌。
2. 项目设计
(小胖看着 jstack 输出中满屏的 BLOCKED 状态,抓耳挠腮。)
小胖:大师,为什么加了 synchronized,线程还是被阻塞得死死的?synchronized 不就是一把互斥锁吗,为什么分什么"偏向锁""轻量级锁""重量级锁"——锁不就锁吗,搞那么复杂干嘛?
大师(笑了笑):你这个"锁不就锁吗"的问题,正是很多人对并发模型的最大误解。synchronized 不是一把简单的互斥锁——它是一个自适应的、三级升级的锁系统。理解这个系统,就能解释为什么你同一个方法加锁后,单线程快如闪电,多线程却慢如蜗牛。
让我先从线程本身说起。Java 中的线程有 6 种状态——你一定要背下来,因为 jstack 里状态列展示的就是这些:
NEW ──start()──→ RUNNABLE ──wait()──→ WAITING
│ ↑ │
│ └──notify()──────┘
│
synchronized 竞争失败 → BLOCKED
│ ↑
│ └── 获取到锁 ────┘
│
sleep()/join(timeout) → TIMED_WAITING
↓
TERMINATED
技术映射:线程状态 ↔ 餐厅顾客的状态:NEW ↔ 刚进门还没入座,RUNNABLE ↔ 正在点菜/等菜,BLOCKED ↔ 排队等空位,WAITING ↔ 菜已点完在等上菜(没人叫他永远不会走),TIMED_WAITING ↔ 限时等位(超时就走了),TERMINATED ↔ 吃完买单走人。
小白:那synchronized 的"三级锁升级"到底是怎么回事?为什么 HotSpot 不直接用一把重量级锁,而要搞偏向锁和轻量级锁这种中间态?
大师:因为真实场景中,"绝大部分锁竞争并不存在"。
想象一个场景——食堂有一个窗口卖牛肉面,但 90% 的时间只有一个人来买。如果你在窗口配一个全职保安(重量级锁),他会站一整天,但 90% 的时间无事可做——浪费人力资源。HotSpot 的设计者用数据证明:多数锁在整个生命周期中只被一个线程持有,且竞争非常短暂。
基于这个统计规律,HotSpot 设计了三级锁升级:
无锁状态
└→ 偏向锁 (Biased Locking) ← 只有一个线程反复进出,不释放也不争用
└→ 轻量级锁 (Lightweight) ← 有另一个线程来争,但持有时间很短 → CAS 自旋
└→ 重量级锁 (Heavyweight) ← 自旋太久或竞争线程太多 → 交给 OS 管
偏向锁:当锁第一次被一个线程获取时,JVM 在 Mark Word 中记下这个线程的 ID。之后这个线程再次进入同步块时,只需比对一下"还是我!"就放行——没有任何同步开销。这就像食堂窗口的阿姨认识了你,你走过去她就开始做你的面,不用刷卡验证。
轻量级锁:当另一个线程也来争这个锁时,偏向锁被撤销。两个线程通过 CAS(Compare-And-Swap)操作在对象头中竞争一个"锁记录指针"——赢了就拿着锁,输了就自旋重试。这就像两个顾客同时走到窗口前,阿姨说"你们石头剪刀布吧,谁赢谁先点"。
重量级锁:如果自旋了 N 次还没拿到锁(JDK 6 后自适应自旋次数),或等待线程超过一定数量,JVM 认为"竞争激烈,别再自旋浪费 CPU 了",将锁升级为重量级锁——通过操作系统的 pthread_mutex_lock 挂起未获锁的线程。这就像窗口人太多,保安介入:"排队!一个一个来!排后面的先坐旁边等叫号!"
技术映射:偏向锁 ↔ 熟客刷脸进店(无阻力),轻量级锁 ↔ 两三个人的自觉排队(自旋一下就好),重量级锁 ↔ 保安拿号叫号的大排队(成本高但有序)。
小胖:等一下!那 synchronized 作用于"对象"和"类"有什么区别?
大师:经典面试题。synchronized 锁住的是对象还是类,取决于你修饰的是什么:
public synchronized void instanceMethod() { } // 锁对象: this 实例
public static synchronized void staticMethod() { } // 锁对象: Foo.class (Class 对象)
public void block() {
synchronized (this) { } // 锁对象: this
synchronized (Foo.class) { } // 锁对象: Foo.class
}
每一个 Java 对象在 HotSpot 中都有一个对应的 ObjectMonitor(重量级锁时使用),这个 monitor 可以用在任何对象上——包括 Object o = new Object()。这是 synchronized(obj) 能锁任意对象的原因。
技术映射:对象上的 monitor ↔ 每个餐桌旁站一个服务员(每桌锁独立);类上的 monitor ↔ 整个餐厅只有一个大堂经理(全局唯一)。
小白:最后一个问题——wait()/notify() 为什么要放在 while 而不是 if 中?我看很多老代码都用的 if。
大师:这是 Java 并发编程的铁律之一:wait() 必须放在循环中。
原因有两点:第一,虚假唤醒(Spurious Wakeup)——操作系统层面可能因为信号等原因,在没有收到 notify() 的情况下唤醒等待线程。Java 语言规范明确允许这种情况发生。第二,多消费者竞争——如果有两个消费者同时被 notifyAll() 唤醒,第一个消费者拿了资源后,第二个消费者醒来时资源已经没了。while 循环确保醒来后再次检查条件是否满足。
// 正确写法
synchronized (lock) {
while (queue.isEmpty()) { // ← while, 不是 if
lock.wait();
}
return queue.poll();
}
// 错误写法
synchronized (lock) {
if (queue.isEmpty()) { // ← 如果虚假唤醒,这里已经过去了
lock.wait();
}
return queue.poll(); // ← 队列可能还是空的!
}
3. 项目实战
3.1 环境准备
| 组件 | 版本 | 用途 |
|---|---|---|
| JDK | OpenJDK 21 | 运行示例 |
| jstack | 内置 | 线程 dump 分析 |
| jconsole | 内置 | 可视化线程状态 |
3.2 分步实现
步骤一:观察 Java 线程的 6 种状态
目标:创建处于不同状态的线程,用 jstack 验证每种状态的可观察性。
// ThreadStateDemo.java —— 展示六种线程状态
public class ThreadStateDemo {
static Object lock = new Object();
public static void main(String[] args) throws Exception {
System.out.println("PID: " + ProcessHandle.current().pid());
// 1. NEW 状态
Thread newThread = new Thread(() -> {}, "NEW-Thread");
System.out.println("NEW-Thread 状态: " + newThread.getState());
// 2. RUNNABLE 状态(正在执行或等待 CPU)
Thread runnableThread = new Thread(() -> {
long sum = 0;
for (int i = 0; i < Integer.MAX_VALUE; i++) {
sum += i; // 忙等,保持 RUNNABLE
}
System.out.println(sum);
}, "RUNNABLE-Thread");
runnableThread.start();
// 3. BLOCKED 状态(等待获取锁)
synchronized (lock) {
Thread blockedThread = new Thread(() -> {
synchronized (lock) {
System.out.println("我拿到锁了");
}
}, "BLOCKED-Thread");
blockedThread.start();
Thread.sleep(500);
System.out.println("BLOCKED-Thread 状态: " + blockedThread.getState());
}
// 4. WAITING 状态
Thread waitingThread = new Thread(() -> {
synchronized (lock) {
try {
lock.wait(); // 无限等待
} catch (InterruptedException e) {}
}
}, "WAITING-Thread");
waitingThread.start();
Thread.sleep(500);
System.out.println("WAITING-Thread 状态: " + waitingThread.getState());
// 5. TIMED_WAITING 状态
Thread timedThread = new Thread(() -> {
try {
Thread.sleep(60000); // 限时等待
} catch (InterruptedException e) {}
}, "TIMED_WAITING-Thread");
timedThread.start();
Thread.sleep(500);
System.out.println("TIMED_WAITING-Thread 状态: " + timedThread.getState());
// 6. TERMINATED 状态
Thread terminatedThread = new Thread(() -> {}, "TERMINATED-Thread");
terminatedThread.start();
terminatedThread.join(); // 等待线程结束
System.out.println("TERMINATED-Thread 状态: " + terminatedThread.getState());
System.out.println("\n请执行 jstack " + ProcessHandle.current().pid() + " 查看线程 dump");
Thread.sleep(30000);
}
}
jstack 验证——开另一个终端执行:
jstack <pid> | grep -E "Thread|State"
输出中应能看到 BLOCKED、WAITING (on object monitor)、TIMED_WAITING 等状态。
步骤二:实现线程安全的简易阻塞队列
目标:用 synchronized + wait/notify 实现一个生产者-消费者模式的阻塞队列。
// SimpleBlockingQueue.java —— 简易阻塞队列
import java.util.LinkedList;
import java.util.Queue;
public class SimpleBlockingQueue<T> {
private final Queue<T> queue = new LinkedList<>();
private final int capacity;
public SimpleBlockingQueue(int capacity) {
this.capacity = capacity;
}
// 生产者:队列满时阻塞
public synchronized void put(T item) throws InterruptedException {
while (queue.size() >= capacity) { // ← 用 while 而非 if
System.out.println(Thread.currentThread().getName()
+ " 队列已满,等待消费...");
wait();
}
queue.offer(item);
System.out.println(Thread.currentThread().getName()
+ " 生产: " + item + " [队列大小: " + queue.size() + "]");
notifyAll(); // 唤醒可能在等待的消费者
}
// 消费者:队列空时阻塞
public synchronized T take() throws InterruptedException {
while (queue.isEmpty()) { // ← 用 while 而非 if
System.out.println(Thread.currentThread().getName()
+ " 队列为空,等待生产...");
wait();
}
T item = queue.poll();
System.out.println(Thread.currentThread().getName()
+ " 消费: " + item + " [队列大小: " + queue.size() + "]");
notifyAll(); // 唤醒可能在等待的生产者
return item;
}
// 测试用例
public static void main(String[] args) {
SimpleBlockingQueue<String> queue = new SimpleBlockingQueue<>(3);
// 生产者线程
Thread producer = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
try {
queue.put("消息-" + i);
Thread.sleep(100); // 模拟生产耗时
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "Producer");
// 消费者线程
Thread consumer = new Thread(() -> {
for (int i = 1; i <= 5; i++) {
try {
queue.take();
Thread.sleep(500); // 模拟消费耗时(比生产慢)
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}, "Consumer");
producer.start();
consumer.start();
}
}
运行结果:
Producer 生产: 消息-1 [队列大小: 1]
Consumer 消费: 消息-1 [队列大小: 0]
Producer 生产: 消息-2 [队列大小: 1]
Producer 生产: 消息-3 [队列大小: 2]
Producer 生产: 消息-4 [队列大小: 3]
Producer 队列已满,等待消费... ← 生产者被阻塞
Consumer 消费: 消息-2 [队列大小: 2]
Producer 生产: 消息-5 [队列大小: 3]
Consumer 消费: 消息-3 [队列大小: 2]
Consumer 消费: 消息-4 [队列大小: 1]
Consumer 消费: 消息-5 [队列大小: 0]
步骤三:用 jstack 观察阻塞队列中的锁竞争
目标:运行阻塞队列测试时,用 jstack 抓取线程 dump,观察 BLOCKED 和 WAITING 状态。
// QueueWithJStack.java —— 制造可观察的锁竞争场景
public class QueueWithJStack {
static Object lockA = new Object();
static Object lockB = new Object();
public static void main(String[] args) {
System.out.println("PID: " + ProcessHandle.current().pid());
// 线程1:先拿 lockA,再拿 lockB
new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread-1 持有 lockA");
try { Thread.sleep(2000); } catch (InterruptedException e) {}
synchronized (lockB) {
System.out.println("Thread-1 拿到 lockB");
}
}
}, "Thread-1").start();
// 线程2:先拿 lockB,再拿 lockA → 制造潜在死锁
new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread-2 持有 lockB");
try { Thread.sleep(2000); } catch (InterruptedException e) {}
synchronized (lockA) {
System.out.println("Thread-2 拿到 lockA");
}
}
}, "Thread-2").start();
System.out.println("等待死锁形成,请执行: jstack " + ProcessHandle.current().pid());
}
}
javac QueueWithJStack.java
java QueueWithJStack
# 另开终端
jstack <pid> > thread_dump.txt
# 在 thread_dump.txt 中搜索 "deadlock" → jstack 会自动检测死锁
jstack 输出末尾会包含死锁检测信息:
Found one Java-level deadlock:
=============================
"Thread-2":
waiting to lock monitor 0x... (object 0x..., a java.lang.Object),
which is held by "Thread-1"
"Thread-1":
waiting to lock monitor 0x... (object 0x..., a java.lang.Object),
which is held by "Thread-2"
步骤四:查看 Mark Word 在锁升级中的变化
目标:用 JOL 观察同一个对象在无锁→偏向锁→轻量级锁后的 Mark Word 变化。
// MarkWordDemo.java
import org.openjdk.jol.info.ClassLayout;
public class MarkWordDemo {
static final Object lock = new Object();
public static void main(String[] args) throws Exception {
// 1. 无锁状态(偏向锁延迟4秒开启,或手动 -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0)
System.out.println("=== 无锁状态 ===");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
// 2. 偏向锁(延迟4秒后 JVM 自动启用偏向锁,或者等一会儿)
Thread.sleep(5000);
synchronized (lock) {
System.out.println("=== 偏向锁状态 ===");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
// 3. 轻量级锁(另一个线程争用后升级)
Thread competitor = new Thread(() -> {
synchronized (lock) {
System.out.println("竞争线程持有锁");
}
});
synchronized (lock) {
competitor.start();
Thread.sleep(1000); // 等待竞争发生
System.out.println("=== 轻量级/重量级锁状态 ===");
System.out.println(ClassLayout.parseInstance(lock).toPrintable());
}
competitor.join();
}
}
# JDK 21 默认偏向锁延迟 4 秒
java -cp ".:jol-core-0.17.jar" MarkWordDemo
# 注意:JDK 15+ 偏向锁默认禁用 (-XX:-UseBiasedLocking)
# 如要观察偏向锁,需显式开启:
java -XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0 \
-cp ".:jol-core-0.17.jar" MarkWordDemo
在 JOL 输出中观察:Mark Word 的最低 2 bits 标识锁状态——01 = 无锁/偏向,00 = 轻量级锁,10 = 重量级锁。
可能遇到的坑:
- 偏向锁在 JDK 15+ 默认禁用:由于偏向锁在高并发场景下带来的延迟撤销成本,JDK 15 开始默认
-XX:-UseBiasedLocking。如果你在 JDK 21 上看不到偏向锁——这是正常行为,不是 bug。 notify()vsnotifyAll():notify()只随机唤醒一个等待线程——如果有多个消费者在等同一个条件,就一个会被唤醒。一旦条件被"抢走",另一个永远醒不来。所以生产代码中总是用notifyAll(),除非你能 100% 保证只有一个等待线程。wait()必须持有锁:在wait()/notify()调用前必须持有对象锁(synchronized块内),否则抛出IllegalMonitorStateException。这是最常见的新手错误。
3.3 测试验证
验证矩阵:
| 测试场景 | 预期行为 | 验证方法 |
|---|---|---|
| 线程状态观察 | NEW/RUNNABLE/BLOCKED/WAITING/TIMED_WAITING/TERMINATED 六种状态对号入座 | 运行 ThreadStateDemo + jstack 验证 |
| 阻塞队列 | 生产者满阻塞、消费者空阻塞,不超卖不超取 | 运行 SimpleBlockingQueue 查看日志 |
| 死锁检测 | jstack 自动检测并报告死锁环 | 运行 QueueWithJStack + jstack |
| 锁升级可视化 | Mark Word 最低 2 bits 随锁状态变化 | JOL 在 MarkWordDemo 中观察 |
| wait/notify 的 while | 虚假唤醒后条件不满足也不会错误执行 | 用 while 版本的队列即使反复 notify 也安全 |
# 一键验证脚本
echo "=== 1. 线程状态验证 ==="
java ThreadStateDemo &
sleep 3
jstack $(jps -l | grep ThreadStateDemo | awk '{print $1}') | grep "State:"
echo ""
echo "=== 2. 阻塞队列验证 ==="
timeout 10 java SimpleBlockingQueue 2>&1 || true
echo ""
echo "=== 3. 死锁检测验证 ==="
java QueueWithJStack &
sleep 4
jstack $(jps -l | grep QueueWithJStack | awk '{print $1}') | grep -A 10 "deadlock"
4. 项目总结
4.1 优点与缺点
| 维度 | 优点 | 缺点 |
|---|---|---|
| 三级锁升级 | 自适应场景:无竞争零开销,短暂竞争 CAS 自旋,激烈竞争 OS 挂起 | 偏向锁的撤销有额外成本(需要等待 Safepoint),高并发下反而拖累性能 |
| synchronized | 语法简洁、自动解锁、JVM 深度优化(锁消除、锁粗化) | 不可中断、不可超时、只支持非公平竞争——复杂场景需 ReentrantLock 补充 |
| wait/notify | 标准的生产者-消费者通信机制 | API 过于底层——容易写出假唤醒 bug、容易遗漏 notifyAll() |
| jstack 诊断 | 一键导出所有线程状态,自动检测死锁 | 只反映瞬间快照,瞬态竞争会被漏掉 |
| 线程状态精确 | 6 种状态覆盖所有线程行为的可观测性 | 与 OS 原生线程状态不完全对齐——BLOCKED 在 OS 层可能是 RUNNING |
| 对比技术 | synchronized | ReentrantLock | volatile |
|---|---|---|---|
| 互斥性 | 支持 | 支持 | 不支持互斥 |
| 可中断 | 不支持 | lockInterruptibly() |
N/A |
| 公平锁 | 不支持 | 支持(new ReentrantLock(true)) |
N/A |
| 条件变量 | wait/notify |
Condition(可多个) |
N/A |
| 性能 | JVM 深度优化,无竞争时接近零开销 | 竞争激烈时比 synchronized 快 2-3x | 读操作几乎无开销 |
4.2 适用场景
- 简单互斥:保护共享数据结构的读写,如计数器、状态标志位修改——用
synchronized最简单。 - 生产者-消费者:用
wait/notifyAll实现阻塞队列、线程间任务分发。 - 死锁诊断:遇到线程卡死问题时,
jstack+ 死锁检测是第一反应工具。 - 锁竞争评估:通过 JOL 观察 Mark Word,判断哪些锁频繁升级到重量级——针对性地优化锁粒度。
- 面试准备:线程状态、锁升级、wait/notify 机制是 Java 并发面试的必考题。
不适用场景:
- 需要"公平排队"的场景——用
ReentrantLock(true)替代。 - 读多写少的场景——
synchronized会阻塞读操作,此时ReentrantReadWriteLock或StampedLock更合适。 - 高并发计数——
AtomicLong/LongAdder利用 CAS 无锁化,远快于synchronized保护下的long++。
4.3 注意事项
| 类型 | 详细说明 |
|---|---|
| 偏向锁延迟 | JDK 6-14 偏向锁启动延迟 4 秒(-XX:BiasedLockingStartupDelay=4000),意味着 JVM 启动后前 4 秒的 synchronized 块会直接走轻量级锁 |
| 锁消除 | JIT 编译时,如果逃逸分析证明某锁对象不会被其他线程访问,C2 编译器会直接消除 synchronized 块——这是"没加锁比加了锁更快"的极端反直觉案例 |
| 锁粗化 | 如果 JIT 检测到连续的加锁-解锁操作,会把多个 synchronized 块合并成一个——减少锁获取/释放次数 |
| interrupt 与 lock | synchronized 块中的线程即使收到 interrupt(),也不会退出阻塞状态(不像 ReentrantLock.lockInterruptibly() 会响应中断) |
4.4 常见踩坑经验
案例 1:wait/notify 的 if 陷阱导致生产事故
某订单系统的异步通知模块用 if (queue.isEmpty()) wait() 等消息。某天运维重启了消息队列,导致一瞬间多个 notifyAll() 连续触发——其中一个消费者被"虚假唤醒"后直接 poll() 空队列,抛出 NoSuchElementException。根因:if 唤醒后不检查条件,直接取数据。修复:改为 while (queue.isEmpty()) wait()——这是 Java 官方规范强制要求的。
案例 2:synchronized 锁 String 对象导致全局阻塞
一个配置管理模块用 synchronized (configKey) 保护配置更新,其中 configKey 是一个 String。结果两个"看起来不同"的配置键 "database" 和 "database" 由于字符串常量池共享了同一个 String 实例——两个完全无关的模块相互锁住了对方。根因:String 的 intern() 机制导致常量池内的字符串是全局单例。修复:绝不锁 String 对象或任何共享的全局对象,用 new Object() 或专用锁对象。
案例 3:线程 interrupt 后仍然卡在 synchronized 块
健康检查线程通过 thread.interrupt() 尝试终止一个可能死锁的工作线程。但工作线程卡在 synchronized 块里,interrupt() 信号被忽略,导致优雅关闭超时。根因:synchronized 块不可响应中断。修复:改用 ReentrantLock.lockInterruptibly(),或确保 synchronized 块的持有时间足够短。
4.5 思考题
-
进阶题:
Object类中的wait()、notify()、notifyAll()为什么被定义为final且是native方法?如果允许子类重写wait(),会引入什么问题?(提示:从ObjectMonitor源码src/hotspot/share/runtime/objectMonitor.cpp的角度思考。) -
实战题:你的服务中有一个
public synchronized Map<String, Object> getCache() { ... }方法,压测发现 P99 延迟高达 2 秒。请用 JOL 分析锁对象的 Mark Word 状态,判断锁是否已升级为重量级锁。如果确实升级了,请提供不少于两种优化方案。
答案提示:思考题 1 答案见第 17 章 AQS 与并发同步器家族;思考题 2 答案见第 18 章 ConcurrentHashMap 与并发容器实战。
下一章预告:第 8 章将深入 JMM(Java 内存模型)——可见性、有序性、原子性三座大山,手把手复现"无同步下的可见性失败"并用
volatile/锁/AtomicInteger逐个修复。
延伸阅读与资源
RabbitMQ从入门到进阶的实战之旅:从单机到大促高可用架构
Celery 入门到进阶之路:从异步任务到自研调度平台
LangGraph 生产级实战进阶:从零到生产级Agent工作流开发
Dify 从入门到源码:LLM 应用平台实战修炼
从零到生产级:FastAPI 异步高并发、源码与 SRE 实战
实战SQLAlchemy 2.0: 从 CRUD 到生产级架构
从零打造企业级 AI 助手:LangChain RAG、Agent 与生产实战
Java 工程师进阶:从 JVM 生产排障到OpenJDK原理
后端工程师 AI 转型课:Ollama 私有化大模型从入门到生产
MongoDB 实战进阶与内核修炼
NumPy 从入门到生产落地:全链路实战指南(科学计算/向量化/性能调优)
Milvus向量数据库实战修炼:从 0 到 1 精通向量检索与生产落地
Redis 8 实战精讲:从 CRUD 到源码,构建高可用缓存系统
Python 3实战精进:从脚本到高并发订单引擎
python入门:Rquests从菜鸟脚本到企业级SDK的网络实战圣经

微信公众号: 架构师日常笔记 欢迎关注!
浙公网安备 33010602011771号