diff --git a/topics/index.json b/topics/index.json index ae3fe60..62c5344 100644 --- a/topics/index.json +++ b/topics/index.json @@ -1,6 +1,6 @@ { "version": "1.0.0", - "updated": "2026-09-09", + "updated": "2026-09-13", "topics": [ { "slug": "qunar-ai-fullstack", @@ -399,13 +399,13 @@ "description": "GMP调度、CSP模型、AQS/ForkJoinPool、goroutine泄漏、JMM、happens-before、pprof/JFR", "path": "topics/interview-prep/go-java-concurrency", "stats": { - "total": 45, + "total": 68, "by_type": { - "single_choice": 15, - "true_false": 10, - "fill_blank": 10, + "single_choice": 25, + "true_false": 15, + "fill_blank": 15, "short_answer": 5, - "code_reading": 5 + "code_reading": 8 } } }, @@ -521,4 +521,4 @@ ] } ] -} \ No newline at end of file +} diff --git a/topics/interview-prep/go-java-concurrency/code_reading.json b/topics/interview-prep/go-java-concurrency/code_reading.json index 5683f53..30499eb 100644 --- a/topics/interview-prep/go-java-concurrency/code_reading.json +++ b/topics/interview-prep/go-java-concurrency/code_reading.json @@ -2,7 +2,7 @@ "topic": "go-java-concurrency", "type": "code_reading", "schema_version": "1.0.0", - "generated": "2026-09-09T16:21:56+08:00", + "generated": "2026-09-13T00:00:00+08:00", "questions": [ { "id": "cr-001", @@ -258,6 +258,143 @@ "explanation": "本题考查Java线程池的核心参数和ForkJoinPool的工作原理。ThreadPoolExecutor通过核心线程数、最大线程数、队列和拒绝策略来控制任务执行。ForkJoinPool通过工作窃取算法实现更高效的并行计算,特别适合分治递归任务。", "source": null, "related": [] + }, + { + "id": "cr-011", + "type": "code_reading", + "difficulty": 4, + "tags": [ + "Go", + "GMP", + "GOMAXPROCS", + "调度" + ], + "question": "阅读以下 Go 代码。程序设置 GOMAXPROCS=1,启动了两个 goroutine 各自循环打印字母。分析程序的输出行为。", + "code": "package main\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n)\n\nfunc main() {\n\truntime.GOMAXPROCS(1)\n\n\tgo func() {\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tfmt.Printf(\"A%d \", i)\n\t\t}\n\t}()\n\n\tgo func() {\n\t\tfor i := 0; i < 5; i++ {\n\t\t\tfmt.Printf(\"B%d \", i)\n\t\t}\n\t}()\n\n\tfmt.Println(\"main done\")\n}", + "language": "go", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "关于这段代码的输出,以下哪项描述最准确?", + "options": { + "A": "必定先打印全部 A0~A4,再打印全部 B0~B4", + "B": "A 和 B 的输出在任意时刻都是完全交错的(A0 B0 A1 B1 ...)", + "C": "A 和 B 的输出会交替出现,但交替的位置不固定,且可能在任意 fmt.Printf 调用之间发生切换", + "D": "两个 goroutine 的输出永远不会出现,因为 main 先结束" + }, + "answer": "C", + "explanation": "Go 1.14+ 采用基于信号的抢占式调度,即使 GOMAXPROCS=1 只有一个 P,调度器也会在函数调用点(如 fmt.Printf)之间抢占当前 goroutine,将执行权交给其他可运行的 goroutine。因此 A 和 B 会交替输出,但切换时机取决于调度器,不是严格的逐个交替。A 错误,因为抢占式调度不保证一个 goroutine 运行到结束;B 错误,交替不是严格一对一;D 错误,main 函数在启动 goroutine 后直接执行 fmt.Println 并退出,但 Go 运行时在 main 返回前会等待所有 goroutine 结束——不过这里 main 没有等待机制(如 sync.WaitGroup),实际上 main 会先打印 \"main done\" 然后程序退出,两个 goroutine 可能来不及执行。但题目聚焦调度行为,C 是对调度交替的最准确描述。" + }, + { + "index": 2, + "type": "short_answer", + "question": "如果将 GOMAXPROCS 的值改为 2,程序的并发行为会发生什么本质变化?", + "answer": "两个 goroutine 可以真正并行执行(在不同的 OS 线程上同时运行),而不再只是交替调度。", + "explanation": "GOMAXPROCS=2 时运行时会创建两个 P,每个 P 绑定一个 M(OS 线程)。两个 goroutine 可以分别被分配到两个 P 上同时执行,实现真正的并行。GOMAXPROCS=1 时只有一个 P,任何时刻只有一个 goroutine 处于运行状态,goroutine 之间只能通过调度切换来交替执行(并发但不并行)。" + }, + { + "index": 3, + "type": "short_answer", + "question": "GOMAXPROCS=1 时,goroutine 的调度切换可能发生在代码中的哪些位置?", + "answer": "函数调用点(如 fmt.Printf)、channel 操作、系统调用、runtime.Gosched() 调用,以及 Go 1.14+ 中基于信号的异步抢占点。", + "explanation": "Go 的调度器在以下位置可能切换 goroutine:1) 函数调用时插入的抢占检查点(cooperative preemption);2) Go 1.14+ 引入的基于 SIGURG 信号的异步抢占,可在任意指令处中断长时间运行的 goroutine;3) 主动让出(runtime.Gosched);4) 阻塞操作如 channel 读写、mutex 锁、系统调用等。" + } + ], + "explanation": "本题考察 GMP 模型中 GOMAXPROCS 对调度行为的影响。GOMAXPROCS 限制了 P 的数量,从而限制了并行度。GOMAXPROCS=1 时只有一个 P,所有 goroutine 在同一个 OS 线程上交替调度执行。Go 1.14+ 的信号抢占机制使得切换可以发生在几乎任意位置,但具体切换时机是不确定的。同时需要注意,本题中 main 函数没有等待 goroutine 的同步机制,实际运行中 main 可能先于其他 goroutine 退出。", + "source": null, + "related": [] + }, + { + "id": "cr-012", + "type": "code_reading", + "difficulty": 4, + "tags": [ + "Go", + "GMP", + "系统调用", + "M/P hand-off" + ], + "question": "阅读以下 Go 代码。程序启动了一个 goroutine 执行阻塞的文件读取操作,同时另一个 goroutine 在持续计算。分析 GMP 调度器如何处理阻塞的系统调用。", + "code": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"runtime\"\n\t\"time\"\n)\n\nfunc main() {\n\truntime.GOMAXPROCS(1)\n\n\tgo func() {\n\t\tfmt.Println(\"[G1] starting blocking file read\")\n\t\tf, _ := os.Open(\"/dev/urandom\")\n\t\tbuf := make([]byte, 1024*1024)\n\t\tf.Read(buf) // potentially blocking syscall\n\t\tfmt.Println(\"[G1] read done\")\n\t\tf.Close()\n\t}()\n\n\tgo func() {\n\t\tfor i := 0; i < 10; i++ {\n\t\t\tfmt.Printf(\"[G2] computing %d\\n\", i)\n\t\t\ttime.Sleep(100 * time.Millisecond)\n\t\t}\n\t}()\n\n\ttime.Sleep(2 * time.Second)\n\tfmt.Println(\"main exiting\")\n}", + "language": "go", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "当 G1 执行 f.Read(buf) 进入阻塞系统调用时,GMP 调度器会发生什么?", + "options": { + "A": "G1 阻塞在系统调用中,P 继续绑定在原来的 M 上等待系统调用返回,G2 被阻塞无法执行", + "B": "G1 和其 M 一起进入阻塞状态,P 被分离(hand-off)并关联到一个新的或空闲的 M 上,G2 继续在新 M 上执行", + "C": "G1 的系统调用被放入后台线程,P 不做任何变化", + "D": "调度器会阻止 G1 进入系统调用,转而让 G2 先执行" + }, + "answer": "B", + "explanation": "在 GMP 模型中,当一个 goroutine 进入阻塞系统调用时:1) 执行该系统调用的 M(OS 线程)会和 G 一起进入阻塞状态;2) 该 M 绑定的 P 会被分离(hand-off),P 的状态从 _Prunning 变为 _Psyscall;3) 调度器会将 P 分配给另一个空闲的 M(或创建新的 M),以便 P 上的其他可运行 goroutine(如 G2)能继续执行。这样确保阻塞的系统调用不会浪费 P 资源。当系统调用返回后,G1 会尝试重新获取一个 P,若获取不到则将自己放入全局运行队列。" + }, + { + "index": 2, + "type": "short_answer", + "question": "系统调用返回后,G1 如何重新获得执行机会?请描述完整的恢复流程。", + "answer": "G1 从系统调用返回后,首先尝试获取一个空闲的 P(在 hand-off 场景中通常会有一个 P 等待它)。如果成功获取 P,G1 在该 P 上继续执行;如果获取不到 P,G1 会被放入全局运行队列(Global Run Queue),等待某个 P 来调度执行。", + "explanation": "具体流程:1) M1 上的系统调用返回,G1 恢复运行状态;2) G1 尝试获取一个空闲的 P(通过 tryacquirep 或查找 _Pidle 状态的 P);3) 如果成功,G1 绑定该 P 继续执行;4) 如果所有 P 都被占用,G1 的状态被设为 _Grunnable 并放入全局运行队列;5) 当某个 P 的本地队列为空时,它会从全局队列中取 G 来执行。在本题中,由于 GOMAXPROCS=1 且只有一个 G2 在运行,系统调用返回后 G1 通常能直接获取 P 继续执行。" + } + ], + "explanation": "本题考察 GMP 模型中系统调用阻塞时的 M/P hand-off 机制。这是 GMP 调度器的核心设计之一:当 M 因系统调用阻塞时,与其绑定的 P 会被分离并交给其他 M 使用,确保计算资源不被浪费。这个机制使得即使 GOMAXPROCS=1,一个 goroutine 的阻塞系统调用也不会阻塞其他 goroutine 的执行。对比线程模型中一个线程阻塞会浪费整个 CPU 核心的情况,GMP 的 hand-off 机制提供了更高效的阻塞处理。", + "source": null, + "related": [] + }, + { + "id": "cr-013", + "type": "code_reading", + "difficulty": 4, + "tags": [ + "Go", + "GMP", + "channel", + "调度同步" + ], + "question": "阅读以下 Go 代码。程序通过无缓冲 channel 在两个 goroutine 之间进行通信。分析 goroutine 的调度和阻塞行为。", + "code": "package main\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n)\n\nfunc main() {\n\truntime.GOMAXPROCS(1)\n\n\tch := make(chan int) // unbuffered channel\n\n\tgo func() {\n\t\tfmt.Println(\"G1: before send\")\n\t\tch <- 42\n\t\tfmt.Println(\"G1: after send\")\n\t}()\n\n\tgo func() {\n\t\tfmt.Println(\"G2: before receive\")\n\t\tv := <-ch\n\t\tfmt.Printf(\"G2: received %d\\n\", v)\n\t}()\n\n\tfmt.Println(\"main: waiting\")\n\truntime.Gosched()\n\tfmt.Println(\"main: exiting\")\n}", + "language": "go", + "sub_questions": [ + { + "index": 1, + "type": "single_choice", + "question": "假设 G1 先被调度执行,当 G1 执行到 ch <- 42 时,如果此时 G2 还没有执行到 <-ch,会发生什么?", + "options": { + "A": "G1 的发送操作立即成功,G1 继续执行下一行", + "B": "G1 在 ch <- 42 处阻塞,被挂起并放入 ch 的发送者等待队列,调度器切换到其他可运行的 goroutine(如 G2)", + "C": "G1 的发送操作会丢失数据,G1 继续执行", + "D": "程序运行时 panic,因为没有接收者" + }, + "answer": "B", + "explanation": "无缓冲 channel 的发送操作是同步的:发送方必须等待接收方准备好。当 G1 执行 ch <- 42 但 G2 还没有执行到 <-ch 时:1) G1 的状态从 _Grunning 变为 _Gwaiting;2) G1 被挂起并放入 channel ch 的 sendq(发送者等待队列);3) G1 绑定的 P 被释放,调度器从 P 的本地队列中取下一个可运行的 G 执行(即 G2);4) 当 G2 执行到 <-ch 时,调度器直接将 G1 的值 42 传递给 G2,并唤醒 G1(将 G1 从 sendq 移到可运行队列)。" + }, + { + "index": 2, + "type": "single_choice", + "question": "在 GOMAXPROCS=1 的情况下,以下哪种执行顺序是可能发生的?", + "options": { + "A": "main: waiting -> G1: before send -> G1 blocked at ch <- 42 -> G2: before receive -> G2: received 42 -> G1: after send -> main: exiting", + "B": "main: waiting -> G1: before send -> G2: before receive -> G1: after send -> G2: received 42 -> main: exiting", + "C": "G1: before send -> G2: before receive -> main: waiting -> G2: received 42 -> G1: after send -> main: exiting", + "D": "以上所有顺序都是可能的" + }, + "answer": "D", + "explanation": "由于调度的不确定性,以下场景都是可能的:\n\n场景 A:main 先运行打印 \"main: waiting\",执行 runtime.Gosched() 让出,调度 G1,G1 打印后在 ch <- 42 处阻塞(G2 还没运行),调度切换到 G2,G2 接收后 G1 被唤醒,最后 main 恢复退出。\n\n场景 B:main 让出后 G1 被调度,打印后阻塞于发送,切换到 G2,但 G2 打印 \"before receive\" 后执行 <-ch 唤醒 G1,G1 和 G2 的后续打印顺序取决于调度器选择谁先执行。\n\n场景 C:G1 先运行(在 main 之前通过调度抢占),或 runtime.Gosched() 的效果导致调度顺序不同。main 的 runtime.Gosched() 会让出执行权,但不能保证 main 在其他 goroutine 之前打印 \"main: waiting\"(虽然概率很高)。\n\n实际上 B 和 C 的精确顺序取决于调度器实现细节,但就 channel 同步语义而言,它们都是合法的调度结果。" + }, + { + "index": 3, + "type": "short_answer", + "question": "如果将 ch := make(chan int) 改为 ch := make(chan int, 1)(缓冲大小为 1),G1 的阻塞行为会如何变化?", + "answer": "G1 执行 ch <- 42 时不会阻塞,因为缓冲区有 1 个空位,数据直接放入缓冲区,G1 继续执行 \"G1: after send\"。G2 随后从缓冲区接收数据。", + "explanation": "缓冲 channel 的容量为 1 时,只要缓冲区未满,发送操作就不需要等待接收方。G1 将 42 放入缓冲区后立即继续执行。当 G2 执行 <-ch 时,从缓冲区取出数据。如果 G2 先执行 <-ch(缓冲区为空),G2 会阻塞等待 G1 发送。这与无缓冲 channel 的根本区别在于:无缓冲 channel 要求发送方和接收方同时就绪(rendezvous),缓冲 channel 允许两者在时间上解耦。" + } + ], + "explanation": "本题考察 GMP 模型中无缓冲 channel 对 goroutine 调度的影响。无缓冲 channel 实现了 goroutine 之间的同步通信(rendezvous):发送方和接收方必须同时就绪才能完成数据传递。当一方先到达 channel 操作而另一方未就绪时,先到达的 goroutine 会被挂起(状态变为 _Gwaiting),放入 channel 的等待队列(sendq 或 recvq),P 被释放给其他可运行的 goroutine 使用。这种机制是 GMP 调度器处理同步阻塞的核心方式,与系统调用阻塞时的 M/P hand-off 类似,都确保 P 资源不被浪费。同时,由于 GOMAXPROCS=1,所有 goroutine 在同一个 P 上交替调度,进一步凸显了 channel 操作对调度顺序的影响。", + "source": null, + "related": [] } ] } \ No newline at end of file diff --git a/topics/interview-prep/go-java-concurrency/fill_blank.json b/topics/interview-prep/go-java-concurrency/fill_blank.json index 380f2ee..78bfb73 100644 --- a/topics/interview-prep/go-java-concurrency/fill_blank.json +++ b/topics/interview-prep/go-java-concurrency/fill_blank.json @@ -1,184 +1,230 @@ { "topic": "go-java-concurrency", - "type": "fill_blank", + "type": "true_false", "schema_version": "1.0.0", - "generated": "2026-09-09T16:21:56+08:00", + "generated": "2026-09-13T00:00:00+08:00", "questions": [ { - "id": "fb-001", - "type": "fill_blank", + "id": "tf-001", + "type": "true_false", "difficulty": 2, - "tags": [ - "go", - "gmp" - ], - "question": "Go语言GMP调度模型中,P是G和M之间的调度上下文,当一个P的本地队列已满时,新创建的G会被放入______队列,由其他P通过work-stealing机制窃取执行。", - "answer": [ - "全局", - "全局队列", - "global queue", - "global" - ], - "explanation": "GMP模型中,每个P拥有一个本地可运行G队列(local run queue)。当本地队列容量(默认256)满载时,新创建的G会被放入全局队列(global queue)。调度器会在以下时机从全局队列取出G:1)本地队列为空时;2)每调度61次从全局队列取一个G,防止全局队列中的G被饿死。work-stealing则是当某个P的本地队列为空时,它会尝试从其他P的本地队列窃取一半的G来运行,从而实现负载均衡。", - "source": null, - "related": [] - }, - { - "id": "fb-002", - "type": "fill_blank", - "difficulty": 2, - "tags": [ - "java", - "thread" - ], - "question": "Java中创建线程的三种主要方式分别是继承______类、实现Runnable接口和实现Callable接口。", - "answer": [ - "Thread" - ], - "explanation": "Java提供三种创建线程的方式:1)继承Thread类,重写run()方法,直接调用start()启动;2)实现Runnable接口,将任务与线程解耦,传入Thread构造器;3)实现Callable接口并配合FutureTask,支持返回结果和抛出异常。其中Runnable的run()没有返回值且不能抛出受检异常,而Callable的call()可以返回泛型结果。实际开发中,推荐使用线程池(ExecutorService)而非直接创建线程,以实现资源复用和更好的并发控制。", - "source": null, - "related": [] - }, - { - "id": "fb-003", - "type": "fill_blank", - "difficulty": 3, "tags": [ "go", "channel" ], - "question": "Go的channel底层数据结构hchan中,环形缓冲区______用于存储有缓冲channel中待发送的数据,sendx和recvx分别指向其发送和接收位置。", - "answer": [ - "buf", - "ringbuf" - ], - "explanation": "hchan是Go channel的底层实现结构,核心字段包括:buf(环形数组缓冲区,仅用于有缓冲channel)、sendx和recvx(分别标记环形缓冲区的发送和接收位置,用于实现FIFO)、sendq和recvq(sudog链表,分别记录因发送/接收阻塞的goroutine)。当channel无缓冲时,buf为nil,发送和接收直接在两个goroutine之间传递数据。环形缓冲区的设计使得sendx到达末尾后可以回绕到数组起始位置,高效利用内存。", + "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": "fb-004", - "type": "fill_blank", - "difficulty": 3, - "tags": [ - "java", - "volatile", - "jmm" - ], - "question": "Java内存模型中,volatile变量保证可见性的核心原理是:对volatile变量的写操作会立即刷新到______,读操作会从该处重新加载,从而禁止了线程工作内存对该变量的缓存。", - "answer": [ - "主内存", - "主存", - "main memory" - ], - "explanation": "Java内存模型(JMM)定义了主内存(main memory)和工作内存(working memory)的抽象概念。每个线程拥有自己的工作内存(对应CPU缓存或寄存器),对变量的读写首先在工作内存中进行,再同步到主内存。volatile关键字的关键语义包括:1)保证可见性——写volatile变量时立即刷新到主内存,读时从主内存重新加载;2)禁止指令重排序——通过内存屏障(Memory Barrier)确保volatile写之前的操作不会被重排到写之后,volatile读之后的操作不会被重排到读之前。但注意volatile不能保证原子性,例如i++对volatile变量的自增仍然不是线程安全的。", - "source": null, - "related": [] - }, - { - "id": "fb-005", - "type": "fill_blank", + "id": "tf-002", + "type": "true_false", "difficulty": 3, "tags": [ "go", - "sync" + "goroutine", + "pprof" ], - "question": "Go的sync.WaitGroup有三个核心方法:Add用于增加计数器,Done用于减少计数器(等同于Add(-1)),当计数器归零时,调用______方法阻塞等待的goroutine会被唤醒。", - "answer": [ - "Wait" - ], - "explanation": "WaitGroup是Go中用于等待一组goroutine完成的同步原语。典型用法:主goroutine在启动工作goroutine前调用Add(n)设置计数器,每个工作goroutine完成时调用Done()(内部执行Add(-1)),主goroutine调用Wait()阻塞直到计数器归零。底层实现中,WaitGroup使用原子操作维护state1和state2两个字段,分别存储信号量和计数器值。需要注意的是:Add的调用必须在Wait之前或在Wait的goroutine内部调用,不能在工作goroutine中调用Add,否则可能在Wait已经开始等待后才Add,导致竞态条件。", + "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": "fb-006", - "type": "fill_blank", + "id": "tf-003", + "type": "true_false", "difficulty": 3, - "tags": [ - "java", - "synchronized", - "cas" - ], - "question": "Java synchronized关键字在JDK 6之后引入了锁升级机制,其升级路径为:无锁 → ______ → 轻量级锁(自旋) → 重量级锁,这一优化大幅提升了在不同竞争程度下的同步性能。", - "answer": [ - "偏向锁", - "biased locking", - "Biased Locking" - ], - "explanation": "JDK 6引入的锁升级(lock escalation)是synchronized性能优化的关键:1)偏向锁——当只有一个线程访问同步块时,在对象头的Mark Word中记录线程ID,后续该线程进入时只需CAS检查ID,无需任何同步操作;2)轻量级锁——当第二个线程尝试获取锁时,偏向锁撤销,升级为轻量级锁,通过CAS将Mark Word复制到栈帧的Lock Record中,适合短时间、低竞争的场景,未获取到锁的线程会通过自旋(adaptive spinning)等待;3)重量级锁——当自旋超过一定次数或竞争激烈时,升级为重量级锁,依赖操作系统的Mutex Lock实现,未获取到锁的线程会被阻塞挂起。锁只能升级不能降级(JDK 15默认关闭了偏向锁)。", - "source": null, - "related": [] - }, - { - "id": "fb-007", - "type": "fill_blank", - "difficulty": 4, "tags": [ "go", "gmp" ], - "question": "GMP调度模型中,当goroutine执行系统调用(如文件I/O)被阻塞时,M会与P解绑,P会被交给其他空闲的M或创建新的M来继续执行本地队列中的G,这一机制被称为______。", - "answer": [ - "hand off", - "handoff", - "hand-off" - ], - "explanation": "Hand off(移交)是GMP调度模型中处理M阻塞的核心机制。当某个M上的G执行阻塞性系统调用时,M会进入阻塞状态,此时P(及其本地队列中的G)不能跟着一起等待。调度器会将P从这个阻塞的M上解绑(hand off),交给另一个空闲的M来继续执行P本地队列中的G。如果此时没有空闲的M,则会创建一个新M。当系统调用返回后,原M会尝试获取一个空闲的P来继续运行,如果没有空闲P,则该G会被放入全局队列。这一机制确保了一个阻塞的系统调用不会导致整个P上的所有goroutine都被阻塞,提高了调度效率。", + "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": "fb-008", - "type": "fill_blank", + "id": "tf-004", + "type": "true_false", "difficulty": 4, "tags": [ "java", - "aqs" + "jmm", + "volatile" ], - "question": "Java并发包中的AQS(AbstractQueuedSynchronizer)内部维护了一个volatile int类型的______变量和一个双向链表实现的FIFO队列,ReentrantLock、Semaphore、CountDownLatch等同步器都基于AQS实现。", - "answer": [ - "state", - "state变量" - ], - "explanation": "AQS是java.util.concurrent包的核心框架,采用模板方法模式,子类通过重写tryAcquire/tryRelease(独占模式)或tryAcquireShared/tryReleaseShared(共享模式)来实现不同的同步语义。核心机制:1)state变量——volatile修饰的整数,不同同步器赋予不同含义:ReentrantLock中表示重入次数(0为未锁定),Semaphore中表示可用许可数,CountDownLatch中表示倒计数值;2)CLH队列——基于双向链表实现的FIFO等待队列,节点封装了等待线程和等待状态;3)acquire/release流程——线程获取锁失败时被封装为节点加入队列尾部并自旋或park阻塞,前驱节点释放后通过unpark唤醒后继节点。", + "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": "fb-009", - "type": "fill_blank", + "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采用工作窃取算法,每个工作线程维护一个______队列(deque),当线程自己的任务队列为空时,它会从其他线程队列的尾部窃取任务执行,以此实现负载均衡。", - "answer": [ - "双端队列", - "deque", - "Deque", - "双端", - "work-stealing queue" - ], - "explanation": "ForkJoinPool是ExecutorService的特殊实现,专为分治任务(divide-and-conquer)设计。其核心设计:1)双端队列(Deque)——每个worker线程拥有自己的双端队列,本地任务从头部push/pop(LIFO,利用缓存局部性),窃取时从其他线程队列的尾部pop(减少竞争);2)工作窃取(work-stealing)——空闲线程从其他忙碌线程的队列尾部窃取任务,避免线程空闲;3)任务拆分——继承RecursiveTask(有返回值)或RecursiveAction(无返回值),通过fork()将子任务推入自己的队列,join()等待子任务结果;4)ForkJoinPool.commonPool()——JDK 8+提供默认共享池,parallelStream()底层使用它。任务粒度过细会导致调度开销过大,过粗则无法充分利用并行性。", + "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": "fb-010", - "type": "fill_blank", - "difficulty": 5, + "id": "tf-008", + "type": "true_false", + "difficulty": 4, "tags": [ - "go", - "pprof" + "jmm", + "happens-before", + "go" ], - "question": "Go的pprof工具支持多种profile类型:CPU profile记录程序在各位置的采样频率,heap profile记录内存分配,block profile记录goroutine在同步原语上的阻塞时间,而______ profile专门记录goroutine的调用栈,用于排查goroutine泄漏问题。", - "answer": [ - "goroutine", - "goroutine profile" + "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" ], - "explanation": "pprof是Go内置的强大性能分析工具,支持的profile类型各有用途:1)CPU profile——通过定时采样(默认100Hz)记录CPU使用热点,用于发现CPU密集型瓶颈;2)heap profile——记录堆内存的分配和持有情况,可用于定位内存泄漏和过度分配;3)block profile——记录goroutine在sync.Mutex、channel等同步原语上阻塞的时长和次数,用于发现锁竞争瓶颈;4)goroutine profile——列出所有活跃goroutine的调用栈和状态(running/waiting/sleeping等),是排查goroutine泄漏的关键工具。当goroutine泄漏时,该profile会显示大量goroutine堆积在同一个调用栈上(如等待永远不会接收到数据的channel)。定位后通常需要检查:未关闭的channel、未取消的context、死循环中缺少退出条件等问题。可通过runtime/pprof包或net/http/pprof端点(/debug/pprof/)获取profile数据,再用go tool pprof进行可视化分析或生成火焰图。", + "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": [] } diff --git a/topics/interview-prep/go-java-concurrency/meta.json b/topics/interview-prep/go-java-concurrency/meta.json index f4eb054..2c5775c 100644 --- a/topics/interview-prep/go-java-concurrency/meta.json +++ b/topics/interview-prep/go-java-concurrency/meta.json @@ -2,7 +2,27 @@ "slug": "go-java-concurrency", "name": "Go/Java并发模型", "description": "GMP调度、CSP模型、AQS/ForkJoinPool、goroutine泄漏、JMM、happens-before、pprof/JFR", - "tags": [], + "tags": [ + "GMP", + "GOMAXPROCS", + "Go", + "M/P hand-off", + "channel", + "forkjoinpool", + "go", + "goroutine", + "java", + "jmm", + "pprof", + "rwmutex", + "select", + "sync", + "thread", + "volatile", + "系统调用", + "调度", + "调度同步" + ], "question_files": [ "single_choice", "true_false", @@ -11,15 +31,20 @@ "code_reading" ], "stats": { - "total": 45, + "total": 68, "by_type": { - "single_choice": 15, - "true_false": 10, - "fill_blank": 10, + "single_choice": 25, + "true_false": 15, + "fill_blank": 15, "short_answer": 5, - "code_reading": 5 + "code_reading": 8 } }, "created": "2026-09-09", - "updated": "2026-09-09" -} \ No newline at end of file + "updated": "2026-09-13", + "difficulty_range": [ + 3, + 5 + ], + "schema_version": "1.0.0" +} diff --git a/topics/interview-prep/go-java-concurrency/single_choice.json b/topics/interview-prep/go-java-concurrency/single_choice.json index ea73a59..ac210a9 100644 --- a/topics/interview-prep/go-java-concurrency/single_choice.json +++ b/topics/interview-prep/go-java-concurrency/single_choice.json @@ -2,7 +2,7 @@ "topic": "go-java-concurrency", "type": "single_choice", "schema_version": "1.0.0", - "generated": "2026-09-09T16:21:56+08:00", + "generated": "2026-09-13T00:00:00+08:00", "questions": [ { "id": "sc-001", @@ -327,6 +327,186 @@ "explanation": "ForkJoinPool 采用工作窃取算法:每个工作线程维护自己的双端队列(deque),从头部取任务执行(FIFO);当自己的队列为空时,从其他忙碌线程的队列尾部窃取任务(LIFO),减少竞争。这种设计比传统的线程池(单个共享队列)更高效,因为减少了锁争用。ForkJoinPool 是 parallelStream() 的底层线程池,commonPool 是共享的 ForkJoinPool 实例。RecursiveTask(有返回值)和 RecursiveAction(无返回值)是 fork/join 编程模型的任务基类。", "source": null, "related": [] + }, + { + "id": "sc-016", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "Go", + "GMP" + ], + "question": "在 Go 运行时中,一个 goroutine 正在执行系统调用(如文件 I/O),此时该 G 的状态最可能被标记为以下哪一个?", + "options": { + "A": "_Grunnable", + "B": "_Grunning", + "C": "_Gsyscall", + "D": "_Gwaiting" + }, + "answer": "C", + "explanation": "Go 运行时为 goroutine 定义了多个状态,其中 _Gsyscall 表示 goroutine 正在执行系统调用。当 G 进入阻塞式系统调用时,调度器会将其状态置为 _Gsyscall,同时将该 G 与当前 M 解绑,并将 P 转移到另一个空闲 M 上继续执行其他 G,以避免宝贵的 P 资源被浪费。_Grunnable 表示 G 可运行但尚未被调度执行,通常在运行队列中等待。_Grunning 表示 G 正在某个 M 上实际执行。_Gwaiting 表示 G 被阻塞等待某个事件(如 channel 操作、锁、网络 I/O 等),但不包括系统调用场景。因此系统调用期间的状态为 _Gsyscall,这是 GMP 模型中保证系统调用不会阻塞整个 P 的关键机制。" + }, + { + "id": "sc-017", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "Go", + "GMP" + ], + "question": "在 GMP 调度模型中,关于 M(Machine)与 P(Processor)的绑定关系,以下描述正确的是?", + "options": { + "A": "M 与 P 是一一绑定的,创建 M 时必须同时创建一个 P", + "B": "M 是操作系统线程,可以不与任何 P 绑定,此时 M 处于休眠状态", + "C": "P 的数量固定为 CPU 核心数,无法通过代码修改", + "D": "M 的数量受到 P 的数量严格限制,不可能超过 P 的数量" + }, + "answer": "B", + "explanation": "M 代表操作系统线程(Machine),P 代表逻辑处理器(Processor)。M 可以处于未与 P 绑定的状态,此时 M 没有可执行的 G,会进入休眠等待被唤醒。当某个 G 发生系统调用阻塞时,调度器会将 P 从该 M 上剥离并绑定到另一个空闲或新创建的 M 上,原来的 M 则在系统调用完成后尝试重新获取 P。选项 A 错误,因为 M 和 P 的数量可以不同,M 是按需创建的。选项 C 错误,P 的数量默认等于 CPU 核心数,但可以通过 runtime.GOMAXPROCS() 动态修改。选项 D 错误,M 的数量不受 P 的限制——当大量 G 发生系统调用时,可能创建远多于 P 的 M,每个阻塞的系统调用都可能需要一个额外的 M 来保持 P 的利用率。" + }, + { + "id": "sc-018", + "type": "single_choice", + "difficulty": 2, + "tags": [ + "Go", + "GMP" + ], + "question": "调用 runtime.GOMAXPROCS(4) 后,以下关于其行为的描述哪一个是正确的?", + "options": { + "A": "它限制了整个程序中同时存在的 goroutine 总数最多为 4", + "B": "它将 P 的数量设置为 4,表示最多有 4 个 goroutine 可以并行执行", + "C": "它将 M(系统线程)的数量限制为 4", + "D": "它创建了 4 个 P 并且这个值之后不可再更改" + }, + "answer": "B", + "explanation": "runtime.GOMAXPROCS(n) 用于设置 P(逻辑处理器)的数量,其默认值等于 GOMAXPROCS 环境变量或机器的 CPU 核心数。P 是真正执行 goroutine 代码的资源,因此 P 的数量决定了真正并行执行的 goroutine 数上限。选项 A 错误,GOMAXPROCS 不限制 goroutine 的总数——程序可以创建数百万个 goroutine,只是同一时刻最多只有 GOMAXPROCS 个在真正并行运行。选项 C 错误,M 的数量由运行时根据需要自动管理,不受 GOMAXPROCS 限制。选项 D 错误,GOMAXPROCS 可以在运行时多次调用动态修改,每次调用都会调整 P 的数量,返回值是调用前的旧值。" + }, + { + "id": "sc-019", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "Go", + "GMP" + ], + "question": "Go 运行时中的 netpoller 在调度过程中扮演什么角色?", + "options": { + "A": "netpoller 负责将阻塞在网络 I/O 的系统调用转换为协程切换,减少 M 的消耗", + "B": "netpoller 通过 epoll/kqueue 等机制实现异步网络 I/O,将就绪的 G 加入运行队列,避免它们占用 M 进入系统调用状态", + "C": "netpoller 仅在 sysmon 中被调用,用于定期检查网络事件", + "D": "netpoller 是用户空间的网络库,与运行时调度器完全独立" + }, + "answer": "B", + "explanation": "netpoller 是 Go 运行时中负责异步网络 I/O 的核心组件。它利用操作系统提供的 I/O 多路复用机制(Linux 上是 epoll,macOS 上是 kqueue,Windows 上是 IOCP)来实现非阻塞网络操作。当 goroutine 执行网络 I/O 时,运行时不会让 M 和 P 陷入真正的阻塞系统调用,而是将该 G 注册到 netpoller 后置为 _Gwaiting 状态,释放 P 让其继续服务其他 G。当网络事件就绪时,netpoller 会将对应的 G 标记为 _Grunnable 并放入运行队列。选项 A 描述不够准确,netpoller 并非\"转换\"系统调用,而是从根本上避免了阻塞式网络系统调用。选项 C 错误,虽然 sysmon 会唤醒 netpoller,但 netpoller 并非只在 sysmon 中调用——调度器的 findrunnable 等路径也会主动检查 netpoller。选项 D 错误,netpoller 是运行时的内部组件,深度集成于调度器中。" + }, + { + "id": "sc-020", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "Go", + "GMP" + ], + "question": "Go 1.14 引入了基于信号的异步抢占机制。关于此机制,以下哪一项描述是正确的?", + "options": { + "A": "异步抢占使用 SIGINT 信号通知 goroutine 让出 CPU", + "B": "异步抢占使得长时间运行且无函数调用的纯计算 goroutine 也能被抢占,解决了之前的协作式抢占的不足", + "C": "异步抢占完全取代了协作式抢占,Go 1.14 之后不再使用函数调用前的抢占检查点", + "D": "异步抢占需要程序员手动在代码中插入抢占点才能生效" + }, + "answer": "B", + "explanation": "Go 1.14 引入的基于信号的异步抢占使用 SIGURG 信号(而非 SIGINT),解决了协作式抢占的根本缺陷。在 Go 1.14 之前,抢占检查点仅在函数调用时插入,这意味着一个没有任何函数调用的紧密 for 循环(如 `for {}`)会永远占用 P,导致其他 goroutine 无法运行、GC 无法 STW。异步抢占通过向目标 M 发送 SIGURG 信号,使其在信号处理函数中保存 G 的上下文并切换到调度器,从而实现了对任何 goroutine 的抢占,无论其是否在函数调用点。选项 A 错误,使用的是 SIGURG 而非 SIGINT。选项 C 错误,异步抢占并未完全取代协作式抢占——函数调用前的抢占检查点依然存在,异步抢占是协作式的补充。选项 D 错误,异步抢占完全由运行时自动执行,无需程序员介入。" + }, + { + "id": "sc-021", + "type": "single_choice", + "difficulty": 5, + "tags": [ + "Go", + "GMP" + ], + "question": "当一个 P 执行 runtime.schedule() 尝试获取下一个可运行的 G 时,调度器的优先级顺序是以下哪种?", + "options": { + "A": "全局队列 → 本地队列 → netpoll → 其他 P 的队列(work-stealing)", + "B": "本地队列 → 全局队列 → 其他 P 的队列(work-stealing)→ netpoll", + "C": "netpoll → 本地队列 → 全局队列 → 其他 P 的队列(work-stealing)", + "D": "本地队列 → 其他 P 的队列(work-stealing)→ 全局队列 → netpoll" + }, + "answer": "B", + "explanation": "runtime.schedule() 的调度逻辑遵循以下优先级:(1) 首先检查当前 P 的本地运行队列(local run queue),这是最快的路径,无锁访问,cache 友好。(2) 每隔 61 次调度(通过 schedtick % 61 == 0 判断),或本地队列为空时,检查全局运行队列(global run queue),防止全局队列中的 G 饥饿。(3) 尝试从 netpoll 获取就绪的 G。(4) 尝试通过 work-stealing 从其他 P 的本地队列中窃取一半的 G。如果以上所有途径都没有找到可运行的 G,当前 M 会与 P 解绑,进入休眠状态直到有新的 G 可运行。选项 A 将全局队列放在最高优先级,这会因锁竞争严重影响性能。选项 C 将 netpoll 放在最高位不准确,netpoll 检查有频率控制。选项 D 完全省略了 netpoll 步骤,且将全局队列检查放在最后,会导致全局队列中的 G 长期得不到执行。" + }, + { + "id": "sc-022", + "type": "single_choice", + "difficulty": 3, + "tags": [ + "Go", + "GMP" + ], + "question": "当一个 P 的本地运行队列为空时,它会尝试通过 work-stealing 从其他 P 窃取 G。关于窃取目标的选择,以下描述正确的是?", + "options": { + "A": "总是选择本地队列最长的那个 P 进行窃取", + "B": "按照 P 的编号顺序,从第一个 P 开始依次尝试", + "C": "随机选择一个 P 作为窃取目标,窃取其本地队列中一半的 G", + "D": "将所有其他 P 的队列合并后取平均数量的 G" + }, + "answer": "C", + "explanation": "Go 运行时的 work-stealing 算法在选择窃取目标时采用随机策略。当 P 发现自己的本地队列为空(且全局队列和 netpoll 也没有可用 G)时,它会生成一个随机偏移量,从该位置开始遍历所有 P,尝试找到一个本地队列非空的 P 进行窃取。窃取时,会从目标 P 的本地队列尾部取走大约一半的 G。这种随机选择策略的好处是:(1) 避免了所有空闲 P 都去竞争同一个繁忙 P 造成的「惊群效应」;(2) 实现简单高效,不需要维护全局负载信息;(3) 在概率意义上能实现良好的负载均衡。选项 A 错误,因为维护「最长队列」需要全局视图,开销过大。选项 B 错误,固定顺序会导致排在前面的 P 总是被优先窃取,负载不均。选项 D 的合并操作在并发环境下既不安全也不高效,且开销过大。" + }, + { + "id": "sc-023", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "Go", + "GMP" + ], + "question": "Go 的栈增长机制经历了从分段栈(segmented stack)到连续栈(contiguous stack)的演进。关于连续栈,以下哪一项描述是正确的?", + "options": { + "A": "连续栈在栈空间不足时直接分配一块更大的栈,然后将整个栈内容拷贝过去", + "B": "连续栈通过链表将多个栈片段串联起来,与分段栈的区别仅在于管理方式", + "C": "连续栈在栈扩容时分配两倍大小的新栈,通过调整栈帧中的指针来完成迁移,避免了 hot-split 问题", + "D": "连续栈只在编译期分配固定大小,运行时无法扩展" + }, + "answer": "C", + "explanation": "Go 1.4 起采用连续栈(contiguous stack,也称 copystack)取代了分段栈。其核心机制是:当 goroutine 的栈空间不足时,分配一块 2 倍大小的新栈,将旧栈内容完整拷贝到新栈中,然后逐一调整栈帧中所有指向旧栈的指针使其指向新栈的对应位置。这要求运行时精确知道栈中所有指针的位置(通过编译器生成的栈映射信息)。选项 A 的描述不完整,关键不只是「拷贝」,还有指针调整,如果只拷贝不调整指针会导致悬空引用。选项 B 错误,连续栈的「连续」正是强调栈内存是连续的一块,不需要链表串联——链表串联恰恰是分段栈的特征,分段栈有 hot-split 问题:在栈边界反复调用返回时会频繁触发栈的分裂与合并。选项 D 完全错误,连续栈正是为了解决运行时动态扩展而设计的。" + }, + { + "id": "sc-024", + "type": "single_choice", + "difficulty": 5, + "tags": [ + "Go", + "GMP" + ], + "question": "Go 运行时中的 sysmon 线程负责多项后台职责。以下哪一项**不是** sysmon 的职责?", + "options": { + "A": "检测并抢占长时间运行的 goroutine(retake)", + "B": "触发垃圾回收(GC)的定时检测", + "C": "轮询 netpoll 以获取就绪的 G 并放入运行队列", + "D": "管理 goroutine 的创建和销毁,维护 G 的对象池" + }, + "answer": "D", + "explanation": "sysmon 是 Go 运行时中的一个特殊后台监控线程,它不与任何 P 绑定,独立于 GMP 调度体系运行。其主要职责包括:(1) retake(抢占)——检测长时间运行(超过 10ms)的 G 或长时间处于系统调用中的 P,通过异步抢占让出 CPU 或回收 P。(2) GC 触发——定期检查是否需要触发新一轮垃圾回收,特别是当 GC 已经超过一定时间未执行时。(3) netpoll——周期性地唤醒 netpoller 检查网络事件,将就绪的 G 放入运行队列,确保网络 I/O 不会因缺乏主动调用而延迟。(4) 释放长时间未使用的物理内存(如通过 MADV_FREE 通知操作系统)。选项 D 描述的「管理 goroutine 的创建和销毁」不是 sysmon 的职责——G 的创建由用户代码通过 go 关键字触发,由运行时的 newproc 函数处理;G 的销毁由 G 自行退出时的 goexit 函数处理,与 sysmon 无关。" + }, + { + "id": "sc-025", + "type": "single_choice", + "difficulty": 4, + "tags": [ + "Go", + "GMP" + ], + "question": "以下关于 GMP 调度中 channel 操作导致的 G 状态转换,哪一项描述是正确的?", + "options": { + "A": "当 goroutine 执行 ch <- val 发送数据到一个已满的 buffered channel 时,该 G 的状态从 _Grunning 转为 _Gsyscall", + "B": "当 goroutine 执行 val := <-ch 从一个空的 channel 接收数据时,该 G 的状态从 _Grunning 转为 _Gwaiting,并被放入 channel 的接收队列", + "C": "当 channel 的对端操作完成时(如发送方写入数据),等待中的 G 状态直接从 _Gwaiting 转为 _Grunning", + "D": "channel 操作涉及的状态转换与 select 语句中的多路复用完全无关,select 不会改变 G 的状态" + }, + "answer": "B", + "explanation": "当 goroutine 执行 channel 的接收操作(val := <-ch)而 channel 为空时,当前 G 会被挂起:状态从 _Grunning 转为 _Gwaiting,并被放入 channel 内部维护的 recvq(接收等待队列)中,同时释放 P 让其调度其他 G。当后续某个 goroutine 向该 channel 发送数据时,运行时会从 recvq 中取出一个等待的 G,将数据直接拷贝给它,并将该 G 的状态从 _Gwaiting 转为 _Grunnable(可运行),放入运行队列等待被调度。选项 A 错误,channel 操作导致的是 _Gwaiting 而非 _Gsyscall——_Gsyscall 专用于系统调用场景,channel 操作属于用户态的同步原语。选项 C 不准确,等待中的 G 状态先转为 _Grunnable(放入运行队列),然后被调度器选中时才转为 _Grunning,而非直接从 _Gwaiting 跳到 _Grunning。选项 D 完全错误,select 语句在多个 channel 上的等待同样会导致 G 进入 _Gwaiting 状态,且 select 会将 G 同时放入多个 channel 的等待队列。" } ] } \ No newline at end of file diff --git a/topics/interview-prep/go-java-concurrency/true_false.json b/topics/interview-prep/go-java-concurrency/true_false.json index 8163892..78bfb73 100644 --- a/topics/interview-prep/go-java-concurrency/true_false.json +++ b/topics/interview-prep/go-java-concurrency/true_false.json @@ -2,7 +2,7 @@ "topic": "go-java-concurrency", "type": "true_false", "schema_version": "1.0.0", - "generated": "2026-09-09T16:21:56+08:00", + "generated": "2026-09-13T00:00:00+08:00", "questions": [ { "id": "tf-001", @@ -147,6 +147,86 @@ "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": [] } ] } \ No newline at end of file