0f68a64829
Deploy Examination / deploy (push) Successful in 10s
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
263 lines
17 KiB
JSON
263 lines
17 KiB
JSON
{
|
||
"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": []
|
||
}
|
||
]
|
||
} |