Files
examination/topics/interview-prep/go-java-concurrency/code_reading.json
T
wonder 0f68a64829
Deploy Examination / deploy (push) Successful in 10s
feat: add interview-prep topic group with 250 questions (6 subtopics, 5 question types)
Subtopics:
- distributed-microservice: 45 questions (分布式微服务架构)
- message-queue: 45 questions (消息队列)
- k8s-observability: 45 questions (K8s与可观测性)
- go-java-concurrency: 45 questions (Go/Java并发模型)
- database-advanced: 35 questions (数据库进阶)
- ai-engineering: 35 questions (AI工程实践)

Question types: single_choice, true_false, fill_blank, short_answer, code_reading
2026-09-09 16:36:27 +08:00

263 lines
17 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"topic": "go-java-concurrency",
"type": "code_reading",
"schema_version": "1.0.0",
"generated": "2026-09-09T16:21:56+08:00",
"questions": [
{
"id": "cr-001",
"type": "code_reading",
"difficulty": 3,
"tags": [
"go",
"channel",
"select"
],
"question": "分析以下Go代码片段,理解select多路复用与channel交互行为:",
"code": "func main() {\n ch := make(chan int, 1)\n quit := make(chan struct{})\n go func() {\n time.Sleep(100 * time.Millisecond)\n ch <- 42\n close(quit)\n }()\n select {\n case v := <-ch:\n fmt.Println(\"received:\", v)\n case <-quit:\n fmt.Println(\"quit\")\n }\n // 第二个select\n select {\n case v := <-ch:\n fmt.Println(\"second:\", v)\n case <-quit:\n fmt.Println(\"quit again\")\n }\n}",
"language": "go",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "这段程序的输出最可能是什么?",
"options": {
"A": "received: 42,然后 quit again",
"B": "quit,然后 quit again",
"C": "received: 42,然后 second: 42",
"D": "编译错误"
},
"answer": "A",
"explanation": "第一个select等待100ms后goroutine向ch发送42并close(quit)。由于ch有缓冲1,send先于close执行,第一个select匹配case v:=<-ch,输出'received: 42'。之后ch已被消费为空,quit已关闭。第二个select时ch为空无法接收,quit已关闭可立即接收,输出'quit again'。"
},
{
"index": 2,
"type": "short_answer",
"question": "如果将ch的缓冲大小从1改为0(无缓冲channel),程序的行为会有什么变化?",
"answer": "第一个select可能匹配quit分支而非ch分支",
"keywords": [
"无缓冲",
"同步阻塞",
"send和receive必须同时就绪",
"竞争"
],
"scoring_rubric": "正确指出无缓冲channel需要发送方和接收方同时就绪(2分),说明goroutine中send在close之前但第一个select可能先匹配quit(1分),说明行为不确定/依赖调度(1分)",
"explanation": "无缓冲channel的send操作必须有对应的receive就绪才能完成。goroutine执行ch <- 42时会阻塞,直到主goroutine的select准备接收。但select也会检查quit channel(此时quit未close),所以send和select存在竞争。如果select先选中quit分支则退出,否则选中ch分支完成send。行为不确定。"
}
],
"explanation": "本题考查Go channel的select多路复用机制。select会同时监听所有case,当多个case就绪时随机选择一个。有缓冲channel的send不阻塞(缓冲区未满),而无缓冲channel的send必须有对应的receive。close已关闭的channel会立即返回零值。",
"source": null,
"related": []
},
{
"id": "cr-002",
"type": "code_reading",
"difficulty": 4,
"tags": [
"go",
"sync",
"rwmutex"
],
"question": "分析以下Go代码片段,找出sync.RWMutex使用中的潜在问题:",
"code": "type Cache struct {\n mu sync.RWMutex\n data map[string]string\n}\n\nfunc (c *Cache) Get(key string) string {\n c.mu.RLock()\n defer c.mu.RUnlock()\n if v, ok := c.data[key]; ok {\n return v\n }\n c.mu.RUnlock()\n c.mu.Lock()\n defer c.mu.Unlock()\n // double check\n if v, ok := c.data[key]; ok {\n return v\n }\n c.data[key] = \"default\"\n return \"default\"\n}",
"language": "go",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "这段代码在key不存在时会出现什么问题?",
"options": {
"A": "死锁,因为重复调用了RUnlock",
"B": "panic: sync: unlock of unlocked RWMutex",
"C": "正常运行,无任何问题",
"D": "数据竞争,但不会崩溃"
},
"answer": "B",
"explanation": "当key不存在时,第一个RLock/RUnlock的defer已经unlock了读锁,接着又显式调用c.mu.RUnlock()试图再次释放读锁,导致panic: sync: unlock of unlocked RWMutex。即使没有defer,RUnlock后再次RUnlock也会panic。"
},
{
"index": 2,
"type": "short_answer",
"question": "请写出修正后的Get方法,正确实现读锁升级为写锁的逻辑。",
"answer": "func (c *Cache) Get(key string) string {\n c.mu.RLock()\n if v, ok := c.data[key]; ok {\n c.mu.RUnlock()\n return v\n }\n c.mu.RUnlock()\n c.mu.Lock()\n defer c.mu.Unlock()\n if v, ok := c.data[key]; ok {\n return v\n }\n c.data[key] = \"default\"\n return \"default\"\n}",
"keywords": [
"先释放读锁",
"再获取写锁",
"double check",
"defer释放"
],
"scoring_rubric": "正确释放读锁后再获取写锁(2分),保留double check避免重复写(1分),写操作在写锁保护下(1分),使用defer释放锁(1分)",
"explanation": "Go的RWMutex不支持锁升级(直接从读锁升级为写锁)。正确做法是先释放读锁,再获取写锁,然后进行double check确认数据是否已被其他goroutine写入。"
}
],
"explanation": "本题考查RWMutex的正确使用模式。Go的sync.RWMutex不支持锁升级,读锁和写锁互斥,必须先释放读锁再获取写锁。常见的错误是重复释放锁导致panic。",
"source": null,
"related": []
},
{
"id": "cr-003",
"type": "code_reading",
"difficulty": 4,
"tags": [
"go",
"goroutine",
"pprof"
],
"question": "分析以下Go代码片段,识别goroutine泄漏的场景:",
"code": "func queryAll(urls []string) []Result {\n results := make(chan Result, len(urls))\n for _, url := range urls {\n go func(u string) {\n resp, err := http.Get(u)\n if err != nil {\n results <- Result{URL: u, Err: err}\n return\n }\n defer resp.Body.Close()\n body, _ := io.ReadAll(resp.Body)\n results <- Result{URL: u, Body: body}\n }(url)\n }\n var collected []Result\n for i := 0; i < len(urls); i++ {\n collected = append(collected, <-results)\n }\n return collected\n}",
"language": "go",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "如果调用queryAll时传入的urls切片包含100个URL,但http.Get对某些URL长时间超时未响应,会发生什么?",
"options": {
"A": "queryAll会立即返回空结果",
"B": "queryAll会阻塞直到所有goroutine完成(包括超时的)",
"C": "超时的goroutine会被自动回收",
"D": "程序会panic"
},
"answer": "B",
"explanation": "主goroutine的for循环会依次从results channel接收len(urls)个结果。如果某些goroutine因网络超时长时间阻塞在http.Get上,主goroutine会在对应的<-results处阻塞等待,直到所有goroutine都完成。没有设置context超时控制。"
},
{
"index": 2,
"type": "short_answer",
"question": "如何改进这段代码以避免goroutine泄漏?至少给出两种方案。",
"answer": "方案一:使用context.WithTimeout控制整体超时。方案二:使用select+time.After或context控制单个请求超时。方案三:使用errgroup管理goroutine。",
"keywords": [
"context.WithTimeout",
"http.NewRequestWithContext",
"errgroup",
"select",
"超时控制"
],
"scoring_rubric": "提出context超时方案(2分),说明具体实现方式(2分),解释为什么能防止泄漏(1分)",
"explanation": "方案1:使用context.WithTimeout创建带超时的context传给http.NewRequestWithContext,超时后HTTP请求取消。方案2:使用errgroup.Group的WithContext方法自动管理goroutine生命周期。方案3:增加done channel或select+timer实现超时退出。"
},
{
"index": 3,
"type": "single_choice",
"question": "使用pprof定位此类goroutine泄漏时,最应该查看哪个profile?",
"options": {
"A": "cpu profile",
"B": "heap profile",
"C": "goroutine profile",
"D": "block profile"
},
"answer": "C",
"explanation": "goroutine profile可以显示当前所有goroutine的调用栈,能直接看到泄漏的goroutine数量和它们阻塞在哪个函数调用上,是排查goroutine泄漏的首选工具。block profile显示锁竞争和channel阻塞,但不如goroutine profile直观。"
}
],
"explanation": "本题考查goroutine泄漏的典型场景——缺乏超时控制导致goroutine无法退出。正确做法是通过context控制请求超时,并使用pprof的goroutine profile定位泄漏。",
"source": null,
"related": []
},
{
"id": "cr-004",
"type": "code_reading",
"difficulty": 4,
"tags": [
"java",
"volatile",
"jmm"
],
"question": "分析以下Java代码片段,理解volatile语义与happens-before规则:",
"code": "public class VisibilityExample {\n private volatile boolean running = true;\n private int counter = 0;\n\n public void startLoop() {\n new Thread(() -> {\n while (running) {\n counter++;\n }\n System.out.println(\"Stopped. counter=\" + counter);\n }).start();\n }\n\n public void stop() {\n running = false;\n }\n}",
"language": "java",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "关于这段代码,以下哪个说法是正确的?",
"options": {
"A": "running是volatile的,所以counter++也是线程安全的",
"B": "running的修改能被子线程看到,但counter可能存在可见性问题",
"C": "由于while循环的存在,子线程永远无法看到running=false",
"D": "这段代码完全没有并发问题"
},
"answer": "B",
"explanation": "volatile保证running的修改对子线程可见(happens-before语义),所以子线程最终会退出循环。但counter不是volatile的,counter++(读-改-写)不是原子操作,子线程读取counter的值可能存在可见性问题——子线程可能一直看到旧值。不过由于只有一个线程修改counter,实际不会有数据竞争。选项B正确指出counter可能存在可见性问题(虽然单线程写不会出错)。"
},
{
"index": 2,
"type": "short_answer",
"question": "如果在main线程中频繁调用stop()方法,而同时子线程在执行counter++,counter的最终值是否准确?请从JMM的角度解释原因。",
"answer": "counter的值不保证准确。counter++不是原子操作,包含read、increment、write三步。虽然本例中只有子线程一个线程修改counter,但如果counter被多个线程修改,就会出现数据竞争。从JMM角度看,非volatile变量的修改不保证对其他线程可见。本例中只有一个线程写counter,所以值是准确的,但如果设计意图是多个线程共享counter,则需要AtomicInteger或synchronized。",
"keywords": [
"原子性",
"read-modify-write",
"volatile不保证原子性",
"AtomicInteger",
"数据竞争"
],
"scoring_rubric": "指出counter++非原子操作(2分),说明volatile不保证原子性(1分),提出AtomicInteger/synchronized解决方案(1分),区分单线程写和多线程写场景(1分)",
"explanation": "volatile只保证可见性和有序性,不保证原子性。counter++是复合操作(读-改-写),在多线程环境下会出现竞态条件。应使用AtomicInteger的getAndIncrement()或synchronized保护counter。"
}
],
"explanation": "本题考查Java内存模型中volatile的核心语义。volatile保证变量的可见性(一个线程的修改对其他线程可见)和有序性(禁止指令重排序),但不保证原子性。counter++这样的复合操作仍需要额外同步机制。",
"source": null,
"related": []
},
{
"id": "cr-005",
"type": "code_reading",
"difficulty": 5,
"tags": [
"java",
"thread",
"forkjoinpool"
],
"question": "分析以下Java代码片段,理解ThreadPoolExecutor与ForkJoinPool的区别:",
"code": "// 方案A: ThreadPoolExecutor\nExecutorService poolA = new ThreadPoolExecutor(\n 4, 8, 60L, TimeUnit.SECONDS,\n new LinkedBlockingQueue<>(1000)\n);\n\n// 方案B: ForkJoinPool\nForkJoinPool poolB = new ForkJoinPool(\n Runtime.getRuntime().availableProcessors()\n);\n\n// 提交任务\npoolA.submit(() -> {\n // CPU密集型计算\n return heavyComputation();\n});\n\npoolB.submit(() -> {\n // 可分治的CPU密集型计算\n return recursiveComputation();\n});",
"language": "java",
"sub_questions": [
{
"index": 1,
"type": "single_choice",
"question": "当poolA的线程都在执行任务且队列已满时,新提交的任务会怎样?",
"options": {
"A": "创建新的线程来执行",
"B": "任务在调用者的线程中执行(CallerRunsPolicy)",
"C": "抛出RejectedExecutionException(使用默认拒绝策略时)",
"D": "任务被丢弃但不抛异常"
},
"answer": "C",
"explanation": "ThreadPoolExecutor的默认拒绝策略是AbortPolicy,当线程数达到maximumPoolSize且工作队列已满时,会抛出RejectedExecutionException。注意:本例中核心线程数4 < 最大线程数8,队列是无界的(capacity=1000),但实际上LinkedBlockingQueue(1000)有界。当8个线程都在忙且队列1000个位置都满了,才会触发拒绝策略。"
},
{
"index": 2,
"type": "single_choice",
"question": "ForkJoinPool相比ThreadPoolExecutor的核心优势是什么?",
"options": {
"A": "支持更多的线程数",
"B": "工作窃取算法允许空闲线程从其他线程的队列中取任务,提高CPU利用率",
"C": "不需要传入Runnable或Callable任务",
"D": "自动管理线程的生命周期"
},
"answer": "B",
"explanation": "ForkJoinPool的核心优势是工作窃取(work-stealing)算法。每个工作线程有自己的双端队列(Deque),当一个线程的队列为空时,它可以'窃取'其他线程队列尾部的任务。这比ThreadPoolExecutor的单一共享队列减少了锁竞争,特别适合递归分治任务(如ForkJoinTask的fork/join模式)。"
},
{
"index": 3,
"type": "short_answer",
"question": "在什么场景下应该选择ForkJoinPool而不是ThreadPoolExecutor?请举例说明。",
"answer": "适合ForkJoinPool的场景:1.任务可以分解为子任务的分治问题(如排序、搜索);2.大量小任务且执行时间差异大(工作窃取能平衡负载);3.递归任务。适合ThreadPoolExecutor的场景:1.独立的异步任务(如HTTP请求);2.任务之间没有依赖关系;3.需要精确控制队列和拒绝策略。",
"keywords": [
"分治",
"递归",
"工作窃取",
"负载均衡",
"独立任务",
"队列策略"
],
"scoring_rubric": "正确描述ForkJoinPool适用场景(2分),正确描述ThreadPoolExecutor适用场景(1分),给出具体例子(1分),提到工作窃取的负载均衡优势(1分)",
"explanation": "ForkJoinPool设计用于可递归分解的任务,其工作窃取算法在任务执行时间不均匀时表现优异。ThreadPoolExecutor更通用,适合独立的异步任务。Java 8+的parallelStream底层就使用ForkJoinPool.commonPool()。"
}
],
"explanation": "本题考查Java线程池的核心参数和ForkJoinPool的工作原理。ThreadPoolExecutor通过核心线程数、最大线程数、队列和拒绝策略来控制任务执行。ForkJoinPool通过工作窃取算法实现更高效的并行计算,特别适合分治递归任务。",
"source": null,
"related": []
}
]
}