【大白话说Java面试题 第105题】【并发篇】第5题:说一下 synchronized 关键字的底层原理?

📌 人工智能开发基于Spring AI的智能对话系统设计:Java全栈实现RAG与工具调用

第5题:说一下 synchronized 关键字的底层原理?

📚 回答:

  • 核心考点
    synchronized 的底层原理是大厂面试中并发编程板块的"压轴题"。面试官不仅期望你掌握 Monitor 对象模型和 CAS 竞争机制,更要深入理解 对象头 Mark Word 的锁状态编码锁升级全过程的字节级细节cxq/EntryList/WaitSet 三条队列的协作关系park/unpark 与操作系统线程调度的交互 ,以及 可重入性的栈帧追踪机制。真正的大厂面试官会通过追问"锁膨胀时 _cxq 和 _EntryList 的转移策略""wait/notify 为什么必须在同步块内调用"来区分"背过八股"和"理解原理"的候选人。

1. 字节码层面的入口------monitorenter 与 monitorexit
  • 1.1 反编译证据

    通过 javap -verbose -c 反编译同步代码块,可以清晰看到 JVM 在同步边界插入的指令 citation:13

    java 复制代码
    public void syncBlock();
      Code:
         0: aload_0
         1: getfield      #2    // Field lock:Ljava/lang/Object;
         4: dup
         5: astore_1
         6: monitorenter        // ← 同步块入口:获取 Monitor
         7: aload_0
         8: invokevirtual #3    // Method doSomething:()V
        11: aload_1
        12: monitorexit         // ← 正常退出:释放 Monitor
        13: goto          21
        16: astore_2
        17: aload_1
        18: monitorexit         // ← 异常退出:释放 Monitor(确保不泄露)
        19: aload_2
        20: athrow
        21: return
      Exception table:
         from    to  target type
             7    13    16   any
            16    19    16   any

    关键发现 citation:13

    1. 编译器生成了 两个 monitorexit------一个用于正常路径(offset 12),一个用于异常路径(offset 18);
    2. Exception table 确保任何异常都会跳转到 offset 16,执行第二个 monitorexit
    3. 这是 JVM 的刚性保证:无论同步块如何退出(正常、return、break、异常),锁一定会被释放。
  • 1.2 同步方法的隐式实现

    对于 synchronized 修饰的方法,JVM 不生成 monitorenter/monitorexit,而是在方法的 ACC_SYNCHRONIZED 访问标志位上置位。调用方法时,JVM 自动检查该标志并获取/释放锁 citation:4

    同步方式 字节码实现 锁对象
    同步代码块 monitorenter + monitorexit 显式指定对象
    实例方法 ACC_SYNCHRONIZED 标志 this
    静态方法 ACC_SYNCHRONIZED 标志 Class 对象

2. Monitor 对象的内部结构------HotSpot 源码级解析

synchronized 重量级锁的核心是 HotSpot 虚拟机中的 ObjectMonitor 对象(C++ 实现),其关键字段如下 citation:6citation:4

cpp 复制代码
// hotspot/share/runtime/objectMonitor.hpp
class ObjectMonitor : public CHeapObj<mtInternal> {
    volatile markOop   _header;       // 对象头备份(Displaced Mark Word)
    volatile void*     _object;       // 指向锁对象本身
    volatile intptr_t  _recursions;   // 重入计数(0 表示首次进入)
    volatile Thread*   _owner;        // 持有锁的线程(NULL 表示无锁)
    volatile void*     _WaitSet;      // 调用 wait() 的线程队列(WaitNode 双向链表)
    volatile void*     _cxq;          // 竞争队列(Contention Queue,单向链表,LIFO)
    volatile void*     _EntryList;    // 入口队列(双向链表,FIFO)
    volatile int       _WaitSetLock;  // 保护 WaitSet 的锁
    volatile int       _Responsible;  // 负责唤醒的线程
    volatile intptr_t  _succ;         // 后继线程(启发式唤醒优化)
    volatile int       _Spinner;      // 自旋计数
    volatile intptr_t  _SpinFreq;     // 自旋频率统计
    volatile intptr_t  _SpinClock;    // 自旋时钟
    volatile int       _SpinDuration; // 自旋持续时间(自适应)
};

核心字段详解 citation:6citation:4

字段 类型 作用
_header markOop 锁膨胀时备份的对象头 Mark Word,解锁时恢复
_object void* 指向被锁定的 Java 对象,用于反向查找
_recursions intptr_t 重入计数。线程每次进入同步块 +1,退出 -1,为 0 时真正释放锁
_owner Thread* 锁持有者。CAS 原子操作修改,NULL 表示锁空闲
_WaitSet WaitNode* 等待队列 。调用 wait() 的线程被封装为 WaitNode 放入此处,需 notify() 唤醒
_cxq ObjectWaiter* 竞争队列 (Contention Queue)。获取锁失败的线程先进入此处,单向链表,LIFO(栈)
_EntryList ObjectWaiter* 入口队列 。从 _cxq 中筛选或直接从竞争失败的线程移入,双向链表,FIFO
_succ intptr_t 启发式优化。记录可能被唤醒的线程,减少不必要的唤醒操作
_SpinDuration int 自适应自旋。根据历史竞争情况动态调整自旋时间

3. 锁获取全过程------从 monitorenter 到 _owner

当线程执行 monitorenter 时,JVM 的 InterpreterRuntime::monitorenter 方法被调用,核心逻辑如下 citation:6citation:4

步骤 1:检查锁状态(快速路径)

cpp 复制代码
// 1. 检查对象头 Mark Word
markOop mark = obj->mark();
if (mark->is_neutral()) {  // 无锁状态(001)
    // 尝试 CAS 将 Mark Word 替换为指向 Lock Record 的指针(轻量级锁)
    // 或设置为偏向锁(101)
}

步骤 2:偏向锁/轻量级锁处理

  • 如果是偏向锁且线程 ID 匹配 → 直接返回(零开销);
  • 如果是轻量级锁 → CAS 替换 Mark Word 为 Lock Record 指针;
  • 如果 CAS 失败 → 进入锁膨胀流程。

步骤 3:锁膨胀为重量级锁

cpp 复制代码
// 创建或获取 ObjectMonitor
ObjectMonitor* monitor = inflate(thread, obj);
// 进入 monitor 的 enter 方法
monitor->enter(thread);

步骤 4:Monitor::enter 的竞争逻辑

cpp 复制代码
void ObjectMonitor::enter(TRAPS) {
    Thread * const Self = THREAD;

    // 4.1 尝试 CAS 获取锁(快速路径)
    if (CAS(&_owner, NULL, Self) == NULL) {
        _recursions = 1;  // 首次进入,重入计数设为 1
        return;
    }

    // 4.2 检查是否重入(已持有锁的线程再次进入)
    if (_owner == Self) {
        _recursions++;    // 重入计数 +1
        return;
    }

    // 4.3 自旋尝试(自适应自旋)
    if (TrySpin(Self) > 0) {
        _recursions = 1;
        return;
    }

    // 4.4 进入竞争队列 _cxq(慢路径)
    ObjectWaiter node(Self);
    node.TState = ObjectWaiter::TS_CXQ;

    // 将 node 插入 _cxq 头部(LIFO 栈)
    for (;;) {
        ObjectWaiter * front = _cxq;
        node._next = front;
        if (CAS(&_cxq, front, &node) == front) break;
    }

    // 4.5 挂起线程(park)
    Self->set_current_pending_monitor(this);
    park();  // ← 调用操作系统 pthread_cond_wait 或 WaitForSingleObject
}

关键设计_cxq 使用 LIFO(栈) 结构,新竞争者插入头部。这是基于"最近活跃的线程缓存更热"的假设,但可能导致老线程饥饿 citation:6


4. 锁释放全过程------从 monitorexit 到 unpark

当线程执行 monitorexit 时,JVM 调用 InterpreterRuntime::monitorexit,核心逻辑 citation:6

步骤 1:重入检查

cpp 复制代码
void ObjectMonitor::exit(bool not_suspended, TRAPS) {
    Thread * const Self = THREAD;

    // 1.1 检查是否是锁持有者
    if (_owner != Self) {
        // 异常情况:未持有锁却调用 monitorexit → IllegalMonitorStateException
    }

    // 1.2 重入计数递减
    if (_recursions != 0) {
        _recursions--;  // 重入退出,不真正释放锁
        return;
    }
}

步骤 2:真正释放锁

cpp 复制代码
    // 2.1 将 _owner 置为 NULL(CAS 操作)
    for (;;) {
        if (CAS(&_owner, Self, NULL) == Self) break;
    }

    // 2.2 检查是否有等待线程
    if (_cxq != NULL || _EntryList != NULL) {
        // 2.3 唤醒策略:cxq → EntryList 转移 + 唤醒头节点
        ObjectWaiter * w = _EntryList;
        if (w != NULL) {
            // 从 EntryList 头部取出一个线程唤醒
            ExitEpilog(Self, w);  // unpark(w->_thread)
        } else {
            // EntryList 为空,将 _cxq 整体转移到 _EntryList
            // 然后唤醒 EntryList 头部线程
            QMode == 0: _EntryList = _cxq; _cxq = NULL;
            w = _EntryList;
            ExitEpilog(Self, w);
        }
    }

唤醒策略详解 citation:6

QMode 参数 策略 说明
QMode = 0(默认) _cxq_EntryList 整体转移,FIFO 唤醒 最公平,但转移有开销
QMode = 1 直接唤醒 _cxq 头部(LIFO) 性能最好,但最不公平
QMode = 2 直接唤醒 _cxq 头部,且 _cxq 中线程直接竞争锁 跳过 EntryList,减少一次排队
QMode = 3 _cxq 逆序后插入 _EntryList 将 LIFO 转为 FIFO,平衡公平与性能
QMode = 4 _cxq 直接插入 _EntryList 头部 保持 LIFO,新竞争者优先

默认 QMode = 0 的流程

复制代码
释放锁时:
  _cxq: [T5] → [T4] → [T3] → NULL(LIFO,T5 最新)
  _EntryList: [T2] → [T1] → NULL(FIFO)

  → 转移 _cxq 到 _EntryList 尾部
  _EntryList: [T2] → [T1] → [T3] → [T4] → [T5] → NULL

  → 唤醒 _EntryList 头部 T2

5. 可重入性的栈帧追踪机制

synchronized 的可重入性不是通过调用栈比对实现的,而是通过 _recursions 计数器 citation:6

java 复制代码
public synchronized void outer() {
    inner();  // 同一线程,_recursions 从 1 → 2
}

public synchronized void inner() {
    // _recursions = 2,无需再次获取锁
}

底层实现

  1. 线程首次进入 outer() → CAS _owner 为当前线程,_recursions = 1
  2. 进入 inner() → 检查 _owner == Self_recursions++ → 变为 2;
  3. 退出 inner()_recursions-- → 变为 1,不释放锁;
  4. 退出 outer()_recursions-- → 变为 0,CAS _owner 为 NULL,真正释放。

为什么不会死锁?

  • 如果不可重入:线程 A 持有锁后调用另一个同步方法 → 发现锁被占用(自己)→ 阻塞等待自己释放 → 死锁;
  • 可重入设计:通过 _recursions 识别"是自己",直接放行,避免死锁 citation:4

6. wait/notify 的底层实现------_WaitSet 与 _cxq/_EntryList 的协作
  • 6.1 wait() 的实现

    cpp 复制代码
    void ObjectMonitor::wait(jlong millis, bool interruptable, TRAPS) {
        // 1. 检查是否持有锁
        if (_owner != Self) throw IllegalMonitorStateException();
    
        // 2. 创建 WaitNode 并加入 _WaitSet
        ObjectWaiter node(Self);
        node.TState = ObjectWaiter::TS_WAIT;
        AddWaiter(&node);  // 加入 _WaitSet 双向链表
    
        // 3. 释放锁(_recursions = 0, _owner = NULL)
        exit(true, Self);
    
        // 4. 挂起线程(park)
        park();  // 等待被 notify/unpark
    
        // 5. 被唤醒后,重新竞争锁(进入 _cxq 或 _EntryList)
        enter(Self);
    }

    关键设计 :调用 wait() 时必须持有锁,因为 wait() 内部需要先释放锁再挂起。如果不加同步块,释放锁时可能抛出 IllegalMonitorStateException citation:6

  • 6.2 notify() 的实现

    cpp 复制代码
    void ObjectMonitor::notify(TRAPS) {
        // 1. 检查是否持有锁
        if (_owner != Self) throw IllegalMonitorStateException();
    
        // 2. 从 _WaitSet 头部取出一个 WaitNode
        ObjectWaiter * iterator = DequeueWaiter();
        if (iterator == NULL) return;  // _WaitSet 为空,无操作
    
        // 3. 将 WaitNode 转移到 _cxq 或 _EntryList
        // 默认策略:转移到 _EntryList(保证被唤醒线程优先竞争)
        iterator->TState = ObjectWaiter::TS_ENTER;
        AddWaiterToEntryList(iterator);
    
        // 4. unpark 唤醒线程
        unpark(iterator->_thread);
    }

    notify() vs notifyAll() citation:6

    • notify():从 _WaitSet 移出一个线程到 _EntryList,唤醒一个;
    • notifyAll():将 _WaitSet所有 线程移到 _EntryList,全部唤醒。
  • 6.3 为什么 wait/notify 必须在同步块内?

    1. 语义要求wait() 需要先释放锁,如果不持有锁就调用 release,会抛出 IllegalMonitorStateException
    2. 竞态条件保护while (condition) { wait(); } 这种模式需要锁保护条件检查的原子性。如果不在同步块内,条件检查和 wait() 之间可能被其他线程修改条件,导致"虚假唤醒"问题 citation:6

7. 非公平锁的饥饿问题与缓解策略
  • 7.1 非公平性来源

    synchronized 是非公平锁,来源于两个设计 citation:6

    1. _cxq 的 LIFO 结构:新竞争者插入头部,老线程沉底;
    2. 唤醒后直接与外部线程竞争 :被唤醒的线程从 park 恢复后,需要重新竞争锁,此时外部新线程可能刚好 CAS 成功,"插队"获取锁。
  • 7.2 饥饿场景

    复制代码
    线程 A 持有锁 → 释放 → 唤醒 _EntryList 头部的线程 B
    同时线程 C 刚好到达 → CAS _owner → 成功获取锁
    线程 B 从 park 恢复 → CAS _owner → 失败 → 再次进入 _cxq
    循环往复,线程 B 始终无法获取锁(饥饿)
  • 7.3 缓解策略

    • 默认 QMode = 0 将 _cxq 整体转移到 _EntryList 尾部,一定程度上缓解了 LIFO 的不公平;
    • 如果业务要求严格公平,应使用 ReentrantLock(true)(公平锁),但会牺牲 10%~20% 的吞吐量 citation:3

8. 锁膨胀的触发条件与优化参数
触发条件 说明 优化参数
CAS 自旋失败 轻量级锁 CAS 替换 Mark Word 失败 -XX:PreBlockSpin(JDK6 前固定自旋次数)
多个线程竞争 通常超过 2 个线程同时竞争 自适应自旋(JDK6+)
调用 wait()/notify() 需要 ObjectMonitor 的等待队列
调用 hashCode() 占用 Mark Word 空间,禁用偏向锁
批量撤销达到阈值 偏向锁撤销 40 次后永久禁用 -XX:BiasedLockingDecayTime

关键 JVM 参数 citation:13

bash 复制代码
# 启用/禁用偏向锁(JDK 15+ 默认禁用)
-XX:+UseBiasedLocking
-XX:-UseBiasedLocking

# 偏向锁启动延迟(JDK 8 默认 4000ms,避免启动时竞争)
-XX:BiasedLockingStartupDelay=0

# 批量重偏向阈值(默认 20)
-XX:BiasedLockingBulkRebiasThreshold=20

# 批量撤销阈值(默认 40)
-XX:BiasedLockingBulkRevokeThreshold=40

# 打印锁升级日志
-XX:+PrintBiasedLockingStatistics

9. 面试官追问与高分回答模板
  • 追问 1:"synchronized 的底层原理是什么?"

    低分回答:"通过 monitor 实现,用 CAS 竞争锁,失败就 park。"(太浅)

    高分回答

    "synchronized 的底层原理可以从三个层面理解:

    1. 字节码层面 :JVM 在同步块入口插入 monitorenter,出口插入两个 monitorexit(正常和异常路径),通过 Exception table 确保锁在任何情况下都能释放。
    2. JVM 层面 :重量级锁依赖 HotSpot 的 ObjectMonitor 对象,核心字段包括 _owner(持有者)、_recursions(重入计数)、_cxq(竞争队列,LIFO)、_EntryList(入口队列,FIFO)、_WaitSet(等待队列)。获取锁时先 CAS _owner,失败则自旋,再失败则进入 _cxqpark;释放锁时 _recursions 减到 0 后 CAS _owner 为 NULL,然后从 _EntryList_cxq 唤醒线程。
    3. 操作系统层面park 调用 pthread_cond_wait(Linux)或 WaitForSingleObject(Windows),涉及用户态→内核态切换。这也是重量级锁开销大的根本原因。
      此外,JDK 6 后引入了偏向锁、轻量级锁、锁消除、锁粗化、自适应自旋等优化,使 synchronized 在大多数场景下性能已接近 ReentrantLock。" citation:4citation:6citation:13
  • 追问 2:"Monitor 的 _cxq 和 _EntryList 有什么区别?为什么需要两个队列?"

    高分回答

    "_cxq_EntryList 是 Monitor 中两条功能不同的竞争队列:

    • _cxq(Contention Queue) :获取锁失败的线程首先进入此处,单向链表,LIFO(栈)。新竞争者插入头部,基于'最近活跃线程缓存更热'的假设,但可能导致老线程饥饿。
    • _EntryList(Entry List) :从 _cxq 中筛选或转移过来的线程,双向链表,FIFO 。锁释放时优先从 _EntryList 头部唤醒线程,比直接从 _cxq 唤醒更公平。
      为什么需要两个队列?
    1. 性能分离_cxq 作为"快速缓冲区",新竞争者快速入队,无需维护复杂结构;_EntryList 作为"调度队列",按 FIFO 顺序唤醒,减少饥饿。
    2. 转移策略 :默认 QMode = 0 时,释放锁会将 _cxq 整体转移到 _EntryList 尾部,将 LIFO 转为 FIFO,平衡性能与公平。
    3. QMode 调优 :QMode = 1 直接唤醒 _cxq 头部(性能最好但不公平);QMode = 3 将 _cxq 逆序后插入 _EntryList(最公平但开销大)。" citation:6
  • 追问 3:"wait() 和 notify() 的底层实现是什么?为什么必须在同步块内调用?"

    高分回答

    "wait() 的底层实现:

    1. 检查当前线程是否持有锁(_owner == Self),否则抛出 IllegalMonitorStateException
    2. 将当前线程封装为 WaitNode 加入 _WaitSet 双向链表;
    3. 释放锁(_recursions = 0_owner = NULL);
    4. 调用 park() 挂起线程;
    5. notify() 唤醒后,从 _WaitSet 移到 _EntryList_cxq,重新竞争锁。
      notify() 的底层实现:
    6. 检查锁持有者;
    7. _WaitSet 头部取出一个 WaitNode
    8. 将其状态改为 TS_ENTER 并加入 _EntryList
    9. 调用 unpark() 唤醒线程。
      必须在同步块内调用的原因
    10. wait() 需要先释放锁,不持有锁就释放会抛异常;
    11. while (condition) { wait(); } 模式需要锁保护条件检查的原子性,防止虚假唤醒和竞态条件。" citation:6
  • 追问 4:"synchronized 是可重入锁,底层怎么实现的?"

    高分回答

    "synchronized 的可重入性通过 ObjectMonitor_recursions 字段实现:

    1. 线程首次获取锁 → CAS _owner 为当前线程,_recursions = 1
    2. 同一线程再次进入同步块 → 检查 _owner == Self_recursions++(无需再次 CAS);
    3. 每次退出同步块 → _recursions--
    4. _recursions 减到 0 → CAS _owner 为 NULL,真正释放锁。
      如果 synchronized 不可重入,线程 A 持有锁后调用另一个同步方法,会发现锁被自己占用而阻塞,永远无法释放,导致死锁。_recursions 的设计完美解决了这个问题。" citation:4citation:6
  • 追问 5:"synchronized 是非公平锁,怎么解决饥饿问题?"

    高分回答

    "synchronized 的非公平性来源于两个设计:

    1. _cxq 是 LIFO 栈,新竞争者总是插队到头部;
    2. 被唤醒的线程从 park 恢复后需要重新竞争锁,外部新线程可能刚好 CAS 成功。
      缓解策略
    • 默认 QMode = 0:释放锁时将 _cxq 整体转移到 _EntryList 尾部,将 LIFO 转为 FIFO,一定程度上缓解饥饿;
    • 如果业务要求严格公平,应使用 ReentrantLock(true),但会牺牲 10%~20% 吞吐量;
    • 避免长时间持有锁,减少其他线程等待时间。
      注意:在大多数业务场景中,非公平锁的吞吐量优势远大于饥饿风险,因此 synchronized 和 ReentrantLock 默认都是非公平的。" citation:3citation:6
  • 追问 6:"JDK 6 后 synchronized 做了哪些优化?锁升级的过程是怎样的?"

    高分回答

    "JDK 6 前 synchronized 只有重量级锁,性能差。JDK 6 引入了五大优化:

    1. 偏向锁:单线程重复获取锁时零开销,CAS 设置线程 ID 后后续直接进入;
    2. 轻量级锁:低竞争时 CAS 自旋,避免阻塞;
    3. 锁消除:逃逸分析发现对象不会逃逸出线程时,直接消除锁;
    4. 锁粗化:连续对同一对象加锁时合并为更大的同步块;
    5. 自适应自旋 :根据历史竞争情况动态调整自旋次数。
      锁升级路径
      无锁(001)→ 偏向锁(101):第一个线程获取时 CAS 设置线程 ID;
      偏向锁 → 轻量级锁(00):其他线程竞争时撤销偏向,CAS 替换 Mark Word 为 Lock Record 指针;
      轻量级锁 → 重量级锁(10):CAS 自旋失败或等待线程过多,膨胀为 ObjectMonitor。
      注意:JDK 15+ 默认禁用偏向锁,因为现代应用多线程竞争激烈,偏向锁的收益已不明显。" citation:2citation:4citation:13

10. 方案选型速查表
业务场景 推荐方案 核心理由
简单同步代码块 synchronized 语法简洁,JVM 自动优化
需要超时/中断响应 ReentrantLock tryLock/lockInterruptibly
需要公平锁 ReentrantLock(true) 可选公平模式
需要多条件等待/唤醒 ReentrantLock + Condition 多个 Condition 对象
高并发计数/累加 LongAdder 分段累加,性能碾压锁
单线程重复获取锁 synchronized(偏向锁) JDK 8 及以前零开销
低竞争短同步块 synchronized(轻量级锁) CAS 自旋,无阻塞
高竞争长同步块 synchronized(重量级锁) 阻塞等待,不消耗 CPU

💡 面试官想要的满分总结

synchronized 的底层原理是 JVM 并发编程中最复杂也最精密的机制之一,理解它必须建立从字节码到操作系统内核的完整认知链路:

字节码层面monitorenter 进入,monitorexit 释放(正常+异常双保险),ACC_SYNCHRONIZED 标志修饰方法。

JVM 层面 :重量级锁依赖 ObjectMonitor,核心字段 _owner(CAS 原子修改)、_recursions(重入计数)、_cxq(LIFO 竞争队列)、_EntryList(FIFO 入口队列)、_WaitSet(等待队列)。获取锁时先 CAS,再自旋,再 park;释放锁时 _recursions 归零后 CAS _owner 为 NULL,然后从队列唤醒线程。

操作系统层面park 调用 pthread_cond_waitunpark 调用 pthread_cond_signal,涉及用户态/内核态切换,这是重量级锁开销大的根本原因。

优化层面:JDK 6 后通过偏向锁(单线程零开销)、轻量级锁(CAS 自旋)、锁消除、锁粗化、自适应自旋五大优化,使 synchronized 性能大幅提升。但 JDK 15+ 已默认禁用偏向锁,现代应用应更多关注轻量级锁和锁消除的优化效果。

最后记住:synchronized 锁的是对象(对象头 Mark Word),而非代码。锁对象必须用 final 修饰,避免引用变化导致锁失效。


觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯

相关推荐
焦虑的说说1 天前
秒杀系统设计方案
java
huangdong_1 天前
淘宝商品SKU图自动分类技术深度解析:从DOM解析到智能归档
开发语言·javascript·ecmascript
阿正的梦工坊1 天前
【Rust】12-借用检查器与非词法生命周期
开发语言·后端·rust
许彰午1 天前
30_Java Stream流操作全解
java·windows·python
qq_2518364571 天前
基于java Web网络订餐系统设计与实现 源码文档
java·开发语言·前端
秋91 天前
3年经验Python后端转AI Engineer:3个月实战转型计划(2026版)
开发语言·人工智能·python
凡人叶枫1 天前
Effective C++ 条款17:以独立语句将 newed 对象置入智能指针
java·linux·开发语言·c++·算法
飞天狗1111 天前
零基础JavaWeb入门——第2课:让网页“活”起来 —— JSP是什么?
java·开发语言·前端·后端·web
梦@_@境1 天前
面向 Spring Boot 的可观测业务流程编排引擎
java·spring boot·后端
云烟成雨TD1 天前
Spring AI Alibaba 1.x 系列【77】执行取消
java·人工智能·spring