{ "topic": "go-java-concurrency", "type": "single_choice", "schema_version": "1.0.0", "generated": "2026-09-09T16:21:56+08:00", "questions": [ { "id": "sc-001", "type": "single_choice", "difficulty": 2, "tags": [ "Go", "GMP", "调度模型" ], "question": "在 Go 的 GMP 调度模型中,P(Processor)代表什么?", "options": { "A": "操作系统线程(OS Thread)", "B": "逻辑处理器,持有本地运行队列和资源上下文", "C": "用户级轻量级线程(goroutine)", "D": "内存管理单元(MMU)" }, "answer": "B", "explanation": "P(Processor)是逻辑处理器,不是物理 CPU 核心。P 持有一个本地 goroutine 运行队列(local run queue)和执行 goroutine 所需的资源上下文(如 mcache、span 等)。G 是 goroutine,M 是操作系统线程。默认情况下 P 的数量等于 GOMAXPROCS,默认值为 CPU 核心数。P 在 G、M 之间起桥梁作用,G 绑定到 P 的本地队列上运行,M 则需要绑定一个 P 才能执行 G。", "source": null, "related": [] }, { "id": "sc-002", "type": "single_choice", "difficulty": 3, "tags": [ "Go", "GMP", "work-stealing" ], "question": "关于 Go GMP 调度器的 work-stealing 机制,以下描述正确的是?", "options": { "A": "当某个 P 的本地队列为空时,它会从其他 P 的本地队列中窃取一半的 goroutine", "B": "当某个 M 空闲时,它会随机选择一个 P 并抢占其正在运行的 goroutine", "C": "work-stealing 仅在全局队列为空时才会触发", "D": "每次窃取操作都会从目标 P 的队列头部取走一个 goroutine" }, "answer": "A", "explanation": "当一个 P 的本地队列为空且全局队列也为空时,调度器会执行 work-stealing:随机选择另一个 P,将其本地队列中的 goroutine 偷走一半。这种设计保证了负载均衡——不会出现某个 P 忙碌而另一个 P 空闲的情况。选项 C 错误,work-stealing 的前提是本地队列为空,然后先尝试全局队列,再尝试 work-stealing。选项 D 错误,窃取时是取走一半(约 half),而非单个。", "source": null, "related": [] }, { "id": "sc-003", "type": "single_choice", "difficulty": 4, "tags": [ "Go", "GMP", "sysmon", "netpoller" ], "question": "Go 运行时中 sysmon 线程的主要职责不包括以下哪项?", "options": { "A": "检测并抢占长时间运行的 goroutine", "B": "回收超过 10ms 未使用的内存堆栈", "C": "将因系统调用阻塞的 M 上的 P 抢占并绑定到空闲 M", "D": "编译 Go 源码中的泛型函数" }, "answer": "D", "explanation": "sysmon 是一个特殊的后台 M(不绑定 P),其职责包括:1)抢占运行超过 10ms 的 goroutine(基于协作+信号的抢占式调度);2)将阻塞在系统调用上的 M 的 P 抢占出来绑定到空闲 M 上,保证 P 不被浪费;3)触发 GC 的后台标记;4)回收长时间未使用的堆栈。编译工作由编译器在编译期完成,与 sysmon 无关。netpoller 也与 sysmon 有协作关系,用于处理网络 I/O 的轮询。", "source": null, "related": [] }, { "id": "sc-004", "type": "single_choice", "difficulty": 2, "tags": [ "Go", "channel", "hchan" ], "question": "在 Go 中,一个无缓冲 channel(unbuffered channel)的发送操作何时完成?", "options": { "A": "将值放入 channel 缓冲区后立即返回", "B": "必须有另一个 goroutine 同时执行接收操作,两者同步后才完成", "C": "无论是否有接收方,发送操作都会立即返回", "D": "发送操作会阻塞直到 channel 被关闭" }, "answer": "B", "explanation": "无缓冲 channel(make(chan T))是同步通道,发送方在接收方准备好之前会阻塞。只有当另一个 goroutine 执行了接收操作,发送和接收才会同步完成。这就是 Go 的'通信通过共享内存'而非'共享内存通过通信'的核心体现。有缓冲 channel(make(chan T, n))在缓冲区未满时发送不会阻塞,这是两者的关键区别。", "source": null, "related": [] }, { "id": "sc-005", "type": "single_choice", "difficulty": 4, "tags": [ "Go", "channel", "hchan", "底层结构" ], "question": "Go channel 的底层数据结构 hchan 中,sendx 和 recvx 字段的作用是什么?", "options": { "A": "记录 channel 的发送和接收总次数,用于性能统计", "B": "环形缓冲区中下一个发送/接收位置的索引,用于实现 FIFO 语义", "C": "记录当前正在阻塞的发送者/接收者数量", "D": "指向 channel 类型元信息的指针偏移量" }, "answer": "B", "explanation": "hchan 是 Go channel 的底层结构体。其中 buf 是一个环形缓冲区(用于有缓冲 channel),sendx 和 recvx 分别记录下一次发送和接收在 buf 中的索引位置,实现 FIFO(先进先出)语义。当 sendx 或 recvx 到达缓冲区末尾时会回绕到 0。与之相关的字段还有 sendq(等待发送的 goroutine 队列,即 sudog 链表)和 recvq(等待接收的 goroutine 队列)。", "source": null, "related": [] }, { "id": "sc-006", "type": "single_choice", "difficulty": 2, "tags": [ "Go", "channel", "close" ], "question": "关于 Go channel 的 close 语义,以下哪项描述是正确的?", "options": { "A": "对一个已关闭的 channel 再次调用 close 会 panic", "B": "关闭一个 nil channel 会静默忽略,不会 panic", "C": "关闭 channel 后,仍然可以向其发送数据", "D": "关闭 channel 后,接收操作会立即阻塞" }, "answer": "A", "explanation": "Go 对 channel 的关闭有严格的规则:1)对已关闭的 channel 再次 close 会触发 panic(send on closed channel 的同类保护);2)对 nil channel 调用 close 同样会 panic;3)关闭 channel 后不能再发送数据(会 panic),但可以继续接收——接收操作会返回缓冲区中的剩余数据,缓冲区读完后接收操作返回零值且 ok 为 false;4)只有发送方或唯一所有者应该关闭 channel,不应由接收方关闭。", "source": null, "related": [] }, { "id": "sc-007", "type": "single_choice", "difficulty": 3, "tags": [ "Go", "sync", "Mutex", "RWMutex" ], "question": "Go 的 sync.RWMutex 相比 sync.Mutex 的主要优势是什么?", "options": { "A": "在写锁模式下性能更好", "B": "允许多个 goroutine 同时持有读锁,提高读多写少场景的并发性能", "C": "支持可重入锁,同一个 goroutine 可多次加锁", "D": "使用无锁算法实现,完全不涉及操作系统原语" }, "answer": "B", "explanation": "RWMutex 提供了读写分离的锁机制:多个 goroutine 可以同时持有 RLock(读锁),只有写锁是互斥的。这在读多写少的场景下显著优于 Mutex(所有操作互斥)。选项 A 错误,RWMutex 的写锁性能通常略低于纯 Mutex,因为需要维护读写计数。选项 C 错误,Go 的 Mutex 和 RWMutex 都不是可重入的,重复加锁会导致死锁。选项 D 错误,底层仍使用原子操作和信号量。", "source": null, "related": [] }, { "id": "sc-008", "type": "single_choice", "difficulty": 3, "tags": [ "Go", "sync", "Once", "Pool" ], "question": "关于 Go 的 sync.Once 和 sync.Pool,以下哪项描述是正确的?", "options": { "A": "sync.Once 的 Do 方法在并发调用时,只有第一个调用会执行传入的函数,其余阻塞等待", "B": "sync.Pool 中的对象永远不会被回收,适合存放全局唯一实例", "C": "sync.Once 的 Do 方法中如果传入的函数 panic,后续调用会重新执行该函数", "D": "sync.Pool 是线程安全的,但不支持跨 goroutine 共享对象" }, "answer": "A", "explanation": "sync.Once 的 Do(f) 保证 f 只被执行一次,即使多个 goroutine 并发调用。第一个调用者执行 f,其他调用者阻塞等待 f 完成后直接返回。选项 B 错误,Pool 中的对象在每次 GC 时可能被清除(Go 1.13 后有 victim cache 机制,延迟一个 GC 周期),不适合存放必须持久化的对象。选项 C 错误,如果 f panic,Once 仍然认为已执行过,后续调用不会再执行 f。选项 D 错误,Pool 设计上就是跨 goroutine 共享的,用于对象复用以减少 GC 压力。", "source": null, "related": [] }, { "id": "sc-009", "type": "single_choice", "difficulty": 4, "tags": [ "Go", "sync", "Map", "并发" ], "question": "Go 的 sync.Map 适合以下哪种使用场景?", "options": { "A": "频繁写入、少量读取的场景", "B": "key 集合稳定,大量并发读取、极少写入的场景", "C": "需要有序遍历所有 key-value 的场景", "D": "需要支持批量删除操作的场景" }, "answer": "B", "explanation": "sync.Map 针对两种场景优化:1)key 集合稳定后只读(read-only);2)多个 goroutine 读写不同的 key(disjoint key sets)。它内部使用 read(只读,无锁访问)和 dirty(写锁保护)两个 map 实现读写分离,在大量并发读的场景下性能远优于 Mutex+map。选项 A 错误,频繁写入会导致 read/dirty 频繁同步,性能反而不如加锁。选项 C 错误,sync.Map 不保证遍历顺序。选项 D 错误,sync.Map 不提供批量删除接口。", "source": null, "related": [] }, { "id": "sc-010", "type": "single_choice", "difficulty": 3, "tags": [ "Go", "内存模型", "happens-before", "race" ], "question": "Go 的 -race 检测器主要基于什么技术实现?", "options": { "A": "静态代码分析,在编译期检测所有潜在的数据竞争", "B": "AddressSanitizer(ASan),在运行时检测内存越界访问", "C": "ThreadSanitizer(TSan),在运行时动态追踪内存访问的 happens-before 关系", "D": "在每个内存访问处插入互斥锁,通过死锁检测间接发现竞争" }, "answer": "C", "explanation": "Go 的 race detector 基于 Google 的 ThreadSanitizer(TSan)技术,在编译期插桩(instrumentation)并在运行时动态追踪所有内存访问和同步操作,构建 happens-before 关系图。如果两个并发访问(至少一个是写)之间没有 happens-before 关系,就报告数据竞争。选项 A 错误,它不是纯静态分析,需要运行时执行才能检测。选项 B 错误,ASan 检测的是内存安全(越界、use-after-free),不是数据竞争。选项 D 完全不正确。", "source": null, "related": [] }, { "id": "sc-011", "type": "single_choice", "difficulty": 4, "tags": [ "Go", "goroutine泄漏", "pprof", "context" ], "question": "以下哪种情况最容易导致 goroutine 泄漏?", "options": { "A": "goroutine 中执行了一次短暂的 CPU 计算后正常返回", "B": "goroutine 向一个没有接收者的无缓冲 channel 发送数据", "C": "goroutine 调用了 time.Sleep 后正常退出", "D": "goroutine 中使用了 defer 语句" }, "answer": "B", "explanation": "goroutine 泄漏是指 goroutine 无法正常退出,持续占用内存和调度资源。向无缓冲 channel 发送数据时,如果没有接收者,发送操作会永久阻塞,goroutine 就泄漏了。常见的泄漏模式还包括:1)从无人发送的 channel 接收;2)未设置超时或取消的阻塞操作(如未用 context 控制的 HTTP 请求);3)无限循环且无退出条件。排查工具:pprof 的 goroutine profile 可以看到所有存活 goroutine 的堆栈,定位泄漏源。", "source": null, "related": [] }, { "id": "sc-012", "type": "single_choice", "difficulty": 3, "tags": [ "Java", "线程", "ThreadPoolExecutor" ], "question": "Java ThreadPoolExecutor 的核心参数 corePoolSize、maximumPoolSize、workQueue 之间的执行关系是?", "options": { "A": "任务提交后直接创建线程,不受 corePoolSize 限制", "B": "先创建到 corePoolSize 个线程 → 放入 workQueue → 满了才创建到 maximumPoolSize 个线程", "C": "先创建到 maximumPoolSize 个线程 → 放入 workQueue → 满了才缩减到 corePoolSize", "D": "workQueue 仅在 corePoolSize 和 maximumPoolSize 都满时才使用" }, "answer": "B", "explanation": "ThreadPoolExecutor 的任务处理流程:1)当前线程数 < corePoolSize → 直接创建新线程执行;2)线程数 ≥ corePoolSize → 放入 workQueue 等待;3)workQueue 已满且线程数 < maximumPoolSize → 创建新线程执行;4)线程数 ≥ maximumPoolSize 且队列满 → 执行拒绝策略(RejectedExecutionHandler)。keepAliveTime 控制非核心线程的空闲存活时间。", "source": null, "related": [] }, { "id": "sc-013", "type": "single_choice", "difficulty": 4, "tags": [ "Java", "锁机制", "synchronized", "锁升级" ], "question": "Java synchronized 的锁升级过程是怎样的?", "options": { "A": "无锁 → 轻量级锁 → 偏向锁 → 重量级锁", "B": "偏向锁 → 轻量级锁 → 重量级锁(不可逆)", "C": "无锁 → 偏向锁 → 轻量级锁 → 重量级锁", "D": "轻量级锁 → 重量级锁 → 偏向锁(竞争结束后降级)" }, "answer": "C", "explanation": "JVM 对 synchronized 的优化采用逐步升级策略:1)无锁状态 → 首次进入同步块时,如果对象头 Mark Word 未偏向任何线程,升级为偏向锁(仅记录线程 ID,无 CAS 开销);2)当第二个线程尝试竞争时,偏向锁撤销,升级为轻量级锁(通过 CAS 将 Mark Word 替换为指向栈帧中 Lock Record 的指针);3)轻量级锁竞争失败(自旋超过一定次数),膨胀为重量级锁(OS mutex,未获锁的线程阻塞挂起)。注意:偏向锁在 JDK 15 后默认关闭,因为其撤销成本较高。", "source": null, "related": [] }, { "id": "sc-014", "type": "single_choice", "difficulty": 4, "tags": [ "Java", "JMM", "volatile", "happens-before" ], "question": "在 Java 内存模型(JMM)中,volatile 变量的 happens-before 语义是?", "options": { "A": "对 volatile 变量的写操作 happens-before 后续对同一变量的读操作", "B": "volatile 仅保证变量的原子性,不保证可见性", "C": "volatile 读写操作不会插入内存屏障", "D": "volatile 变量的写 happens-before 所有后续的读写操作" }, "answer": "A", "explanation": "根据 JMM 的 happens-before 八大规则之一:volatile 变量规则——对 volatile 字段的写操作 happens-before 后续对同一字段的读操作。具体实现上,volatile 写后插入 StoreStore + StoreLoad 屏障,volatile 读前插入 LoadLoad + LoadStore 屏障。选项 B 错误,volatile 不保证原子性(如 i++ 不是原子操作),但保证可见性。选项 C 错误,volatile 正是通过内存屏障实现的。选项 D 错误,happens-before 是针对同一变量的特定读写对,不是全局的。", "source": null, "related": [] }, { "id": "sc-015", "type": "single_choice", "difficulty": 2, "tags": [ "Java", "ForkJoinPool", "工作窃取" ], "question": "Java ForkJoinPool 的工作窃取(work-stealing)机制是指?", "options": { "A": "空闲线程从全局任务队列中取任务执行", "B": "空闲线程从其他忙碌线程的本地任务队列尾部窃取任务执行", "C": "主线程将任务平均分配给所有工作线程", "D": "工作线程按固定顺序依次执行所有提交的任务" }, "answer": "B", "explanation": "ForkJoinPool 采用工作窃取算法:每个工作线程维护自己的双端队列(deque),从头部取任务执行(FIFO);当自己的队列为空时,从其他忙碌线程的队列尾部窃取任务(LIFO),减少竞争。这种设计比传统的线程池(单个共享队列)更高效,因为减少了锁争用。ForkJoinPool 是 parallelStream() 的底层线程池,commonPool 是共享的 ForkJoinPool 实例。RecursiveTask(有返回值)和 RecursiveAction(无返回值)是 fork/join 编程模型的任务基类。", "source": null, "related": [] } ] }