{ "topic": "go-java-concurrency", "type": "true_false", "schema_version": "1.0.0", "generated": "2026-09-13T00:00:00+08:00", "questions": [ { "id": "tf-001", "type": "true_false", "difficulty": 2, "tags": [ "go", "channel" ], "question": "在Go中,向已关闭的channel发送数据会导致panic,但从已关闭的channel接收数据会立即返回零值且ok为false。", "answer": true, "explanation": "Go中channel的close语义明确规定:向已关闭的channel发送数据会触发panic(send on closed channel),而从已关闭的channel接收数据不会阻塞,而是返回该元素类型的零值,第二个返回值ok为false。这是channel安全关闭的核心规则——关闭者负责发送端的停止,接收者只需检查ok值。", "source": null, "related": [] }, { "id": "tf-002", "type": "true_false", "difficulty": 3, "tags": [ "go", "goroutine", "pprof" ], "question": "Go中goroutine泄漏最常见的原因之一是向无缓冲channel发送数据后,没有对应的接收者,导致发送方goroutine永远阻塞。", "answer": true, "explanation": "goroutine泄漏的本质是goroutine无法正常退出。向无缓冲channel发送时,如果没有goroutine在接收,发送方会永久阻塞。类似地,从无人发送的channel接收也会阻塞。其他常见泄漏场景包括:context未正确取消导致的无限等待循环、select中没有default分支且所有case都不就绪等。可以通过pprof的goroutine profile(runtime.NumGoroutine()或net/http/pprof)来定位泄漏的goroutine及其阻塞堆栈。", "source": null, "related": [] }, { "id": "tf-003", "type": "true_false", "difficulty": 3, "tags": [ "go", "gmp" ], "question": "Go的GMP调度模型中,当一个M(系统线程)发现本地P的本地队列为空时,会先尝试从全局队列获取G,再尝试从其他P的本地队列偷取(work-stealing)一半的G。", "answer": true, "explanation": "GMP调度模型中,M绑定P后优先从P的本地队列获取G执行。当本地队列为空时,调度器的窃取逻辑为:1)先检查全局队列(global run queue);2)再尝试netpoller(网络轮询器);3)最后从其他P的本地队列偷取(随机选择一个P,偷取其本地队列一半的G)。sysmon监控线程会定期检测长时间运行的G(抢占式调度),并将空闲P绑定到空闲M。", "source": null, "related": [] }, { "id": "tf-004", "type": "true_false", "difficulty": 4, "tags": [ "java", "jmm", "volatile" ], "question": "在Java内存模型(JMM)中,volatile变量的写操作对后续所有线程的读操作都可见,因此volatile可以完全替代synchronized来保证复合操作的原子性。", "answer": false, "explanation": "volatile确实保证了可见性(写入后立即刷新到主内存,读取时从主内存加载)和有序性(禁止指令重排序),但它不保证复合操作的原子性。例如volatile int count; count++实质是读-改-写三步操作,多个线程并发执行时仍会出现竞态条件。对于复合操作的原子性,需要使用synchronized、ReentrantLock或java.util.concurrent.atomic包中的原子类(如AtomicInteger的CAS操作)。volatile的经典适用场景是状态标志位(boolean flag)和双重检查锁定(DCL)单例模式。", "source": null, "related": [] }, { "id": "tf-005", "type": "true_false", "difficulty": 3, "tags": [ "java", "aqs", "synchronized" ], "question": "Java中AQS(AbstractQueuedSynchronizer)是ReentrantLock和Semaphore等同步器的核心实现基础,它通过CLH队列变体和volatile状态变量来实现线程排队与唤醒。", "answer": true, "explanation": "AQS是java.util.concurrent包的基石,其核心机制为:1)内部维护一个volatile int state变量表示同步状态;2)使用CLH(Craig, Landin, and Hagersten)队列变体实现线程排队,每个等待线程被封装为Node节点;3)获取锁失败的线程被包装为Node加入CLH队列尾部,然后通过LockSupport.park()阻塞;4)释放锁时通过unparkSuccessor()唤醒队列中的下一个线程。ReentrantLock通过state表示重入次数,Semaphore通过state表示可用许可数,CountDownLatch通过state表示倒计数值。而synchronized从JVM层面实现,不依赖AQS。", "source": null, "related": [] }, { "id": "tf-006", "type": "true_false", "difficulty": 2, "tags": [ "go", "sync" ], "question": "Go的sync.Once保证传入的函数只被执行一次,即使多个goroutine同时调用Do方法,也只有一个goroutine会执行该函数,其余goroutine会阻塞等待其完成。", "answer": true, "explanation": "sync.Once内部通过atomic.LoadInt32/StoreInt32检查并设置标志位来保证函数只执行一次。其Do方法的实现逻辑为:先通过原子操作检查done标志,若已执行则直接返回;若未执行,则通过Mutex加锁后再次检查(double-check)并执行函数。其他并发调用Do的goroutine会在Mutex上阻塞,直到执行的goroutine完成函数调用并释放锁。这保证了初始化代码的线程安全执行,是Go中实现懒初始化单例的标准方式。", "source": null, "related": [] }, { "id": "tf-007", "type": "true_false", "difficulty": 4, "tags": [ "java", "forkjoinpool" ], "question": "Java的ForkJoinPool采用工作窃取(work-stealing)算法,空闲的工作线程会从其他繁忙线程的任务队列尾部偷取任务执行,commonPool使用ForkJoinPool.defaultParallelism()作为默认并行度。", "answer": true, "explanation": "ForkJoinPool的核心设计是工作窃取:每个工作线程维护一个双端队列(deque),新任务从队列头部推入(push),本线程也从头部取出执行(LIFO,有利于局部性)。当某个线程的队列为空时,它会随机选择其他线程的队列,从其尾部窃取任务(FIFO,减少竞争)。commonPool是静态共享池,通过ForkJoinPool.commonPool()获取,默认并行度Runtime.getRuntime().availableProcessors()-1(当大于1时),适用于ParallelStream和CompletableFuture等场景。RecursiveTask(有返回值)和RecursiveAction(无返回值)是ForkJoinTask的两个子类,用于定义可分解的递归任务。", "source": null, "related": [] }, { "id": "tf-008", "type": "true_false", "difficulty": 4, "tags": [ "jmm", "happens-before", "go" ], "question": "Go的内存模型中,goroutine的创建(go关键字)发生在该goroutine的执行之前,而channel的每次发送操作都happens-before对应的接收操作完成。", "answer": false, "explanation": "Go内存模型的happens-before规则中:1)goroutine的创建确实happens-before该goroutine的执行开始——这是正确的。2)但对于无缓冲channel:发送操作happens-before对应的接收操作完成;而对于有缓冲channel:第k次发送happens-before第k次接收的完成。关键区别在于'完成'——接收操作的完成是指接收到了数据,而不是仅仅开始接收。该题将两种channel混为一谈,表述不够准确,且channel的发送happens-before的是'对应的接收完成'而非'接收操作本身',存在语义偏差,因此判定为false。", "source": null, "related": [] }, { "id": "tf-009", "type": "true_false", "difficulty": 3, "tags": [ "java", "thread" ], "question": "Java中通过ThreadPoolExecutor创建线程池时,核心参数maximumPoolSize必须大于等于corePoolSize,当核心线程数未满时新任务会创建核心线程执行,核心线程满后任务会先进入工作队列,队列满后才创建非核心线程。", "answer": true, "explanation": "ThreadPoolExecutor的任务提交流程严格遵循以下顺序:1)当前线程数 < corePoolSize → 创建新核心线程执行任务;2)核心线程满 → 将任务放入workQueue(工作队列)等待;3)队列已满且线程数 < maximumPoolSize → 创建非核心线程执行任务;4)线程数已达maximumPoolSize且队列满 → 执行拒绝策略(AbortPolicy/CallerRunsPolicy/DiscardPolicy/DiscardOldestPolicy)。maximumPoolSize < corePoolSize在语义上不成立,构造时虽然不会抛异常,但实际线程数永远不会超过corePoolSize。注意:如果使用无界队列(如LinkedBlockingQueue),maximumPoolSize实际上不会生效。", "source": null, "related": [] }, { "id": "tf-010", "type": "true_false", "difficulty": 2, "tags": [ "java", "jfr" ], "question": "Java Flight Recorder(JFR)是JDK内置的低开销性能分析工具,通过事件采样的方式记录CPU、内存、I/O、线程等运行时信息,可通过jcmd或jfr命令行工具启动录制,也可在代码中通过Recording API编程控制。", "answer": true, "explanation": "JFR是Oracle JDK和OpenJDK(JEP 171/328/336)内置的生产级监控框架,设计目标是开销低于1%。其核心特点包括:1)基于事件驱动的采样机制,预定义了数百种事件类型(如jdk.CPULoad、jdk.GarbageCollection、jdk.ThreadStart等);2)支持命令行启动(jcmd JFR.start、jfr start/stop/dump)和编程方式(new Recording().start());3)录制数据以.jfr文件存储,可通过JDK Mission Control(JMC)可视化分析,生成火焰图等;4)从JDK 11起可免费用于生产环境(此前商业特性)。JFR与异步采样profiling配合,能有效定位CPU热点、锁竞争、GC停顿等问题。", "source": null, "related": [] }, { "id": "tf-011", "type": "true_false", "difficulty": 3, "tags": [ "Go", "GMP", "调度模型", "goroutine" ], "question": "在 Go 的 GMP 调度模型中,当一个 G 进入系统调用(_Gsyscall 状态)且该系统调用阻塞时,G 会从 _Gsyscall 直接转换为 _Gwaiting 状态。", "answer": false, "explanation": "这是对 G 状态机的常见误解。当 G 执行系统调用时,它处于 _Gsyscall 状态。如果系统调用阻塞,调度器会执行 hand-off 操作——将 P 从当前 M 上摘下并交给另一个空闲 M(或新建 M)以继续运行其他 G。阻塞的系统调用完成后,G 的状态转换路径是 _Gsyscall → _Grunnable(可运行),而非 _Gwaiting。_Gwaiting 状态用于显式的等待操作,例如 goroutine 阻塞在 channel 接收/发送、sync.Mutex.Lock()、select 无就绪 case 等场景。_Gsyscall 和 _Gwaiting 是两种不同的阻塞语义:前者是操作系统层面的系统调用阻塞,后者是 Go 运行时层面的同步原语阻塞。理解这一区别对于分析 goroutine 的生命周期和 pprof 输出至关重要。", "source": null, "related": [] }, { "id": "tf-012", "type": "true_false", "difficulty": 2, "tags": [ "Go", "GMP", "调度模型", "goroutine" ], "question": "从 Go 1.14 版本开始,Go 运行时引入了基于信号的异步抢占机制,使用的信号是 SIGURG。", "answer": true, "explanation": "Go 1.14 正式引入了基于信号的异步抢占机制(asynchronous preemption),解决了此前协作式抢占(cooperative preemption)的致命缺陷——当 goroutine 没有函数调用(例如执行紧密的纯计算循环)时,调度器无法插入抢占检查点,导致其他 goroutine 被饿死甚至 GC 无法 STW。异步抢占通过向目标 M 发送 SIGURG 信号实现:运行时的 sysmon 后台线程检测到长时间运行的 G 后,向其绑定的 M 发送 SIGURG,M 的信号处理器会在安全点(safe point)暂停当前 G 的执行,将其状态设为 _Grunnable 并放回队列。选择 SIGURG 是因为它在 Linux 上未被标准工具和应用广泛使用,避免与用户信号处理冲突。该机制标志着 Go 从纯协作式调度迈向了混合式调度模型。", "source": null, "related": [] }, { "id": "tf-013", "type": "true_false", "difficulty": 2, "tags": [ "Go", "GMP", "调度模型", "work-stealing" ], "question": "在 Go 的 GMP 模型中,每个 P(Processor)持有的本地运行队列(local run queue)的固定容量为 256 个 G。", "answer": true, "explanation": "Go 运行时中,每个 P 维护一个本地运行队列(local run queue),其实现是一个固定大小为 256 的环形队列(ring buffer)。这个容量在运行时源码中定义为 localRunQueueCap = 256。当一个 P 的本地队列已满时,新创建的 G 或从全局队列获取的 G 不会被放入本地队列,而是会将本地队列中约一半的 G(128个)连同新 G 一起移到全局运行队列(global run queue)中,这一操作称为 runqputslow。这种设计在保证本地队列访问无锁(lock-free)的同时,通过溢出机制避免 G 被丢弃。256 的容量是一个工程权衡——足够大以减少全局队列竞争,又足够小以保持缓存友好性。全局队列则是一个无容量限制的链表结构,使用互斥锁保护。", "source": null, "related": [] }, { "id": "tf-014", "type": "true_false", "difficulty": 3, "tags": [ "Go", "GMP", "调度模型", "goroutine" ], "question": "在 Go 的 GMP 模型中,当一个 M 进入系统调用时,调度器会立即终止该 M 并将其资源回收,以节省操作系统线程资源。", "answer": false, "explanation": "这是错误的理解。当 M 进入系统调用时,Go 调度器执行的是 hand-off(移交)操作,而非终止 M。具体流程如下:M 进入系统调用前,调度器将其绑定的 P 摘下(hand-off),将 P 转交给一个空闲的 M(如果有)或新建一个 M 来接管该 P,确保 P 能继续执行其本地队列中的其他 G。进入系统调用的 M 本身会阻塞在操作系统层面,等待系统调用返回。系统调用完成后,M 尝试获取一个空闲的 P:如果成功则继续运行;如果没有空闲 P,M 会将当前 G 放入全局队列,然后自身进入休眠状态(放入空闲 M 链表),而非被终止。这种设计保证了 P 资源不被浪费,同时 M 可以被复用。M 的数量可能超过 P 的数量(M:N 模型中 N 可大于 M),但受到信号处理等约束限制。", "source": null, "related": [] }, { "id": "tf-015", "type": "true_false", "difficulty": 4, "tags": [ "Go", "GMP", "调度模型", "goroutine" ], "question": "在 Go 运行时中,栈增长的触发机制是通过硬件内存保护页(guard page)触发 SIGSEGV 信号来实现的。", "answer": false, "explanation": "Go 的栈增长并不依赖硬件内存保护页和 SIGSEGV 信号。Go 采用的是编译器插桩(compiler instrumentation)的软件检查方式。编译器在每个函数入口处插入栈检查代码(称为 stack bound check 或 prologue),将当前栈指针 SP 与 g 结构体中的 stackguard0 字段进行比较。当 SP 低于 stackguard0 时,说明栈空间不足,运行时会触发 morestack 流程:分配一个更大的栈(通常为当前栈的 2 倍),将旧栈内容完整拷贝到新栈,并更新所有指向旧栈的指针(stack copying / stack growth)。stackguard0 的值在栈空间底部附近,预留了一定的安全余量(stackSmall,通常为 128 字节)。这种方式避免了系统调用开销和信号处理的复杂性,且能精确控制栈增长的时机。Go 1.4 之后统一使用连续栈(contiguous stack)方案,取代了早期的分段栈(segmented stack)。", "source": null, "related": [] } ] }