feat: add interview-prep topic group with 250 questions (6 subtopics, 5 question types)
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
This commit is contained in:
2026-09-09 16:36:27 +08:00
parent 515acbcc7f
commit 0f68a64829
38 changed files with 6835 additions and 2 deletions
@@ -0,0 +1,263 @@
{
"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": []
}
]
}
@@ -0,0 +1,186 @@
{
"topic": "go-java-concurrency",
"type": "fill_blank",
"schema_version": "1.0.0",
"generated": "2026-09-09T16:21:56+08:00",
"questions": [
{
"id": "fb-001",
"type": "fill_blank",
"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到达末尾后可以回绕到数组起始位置,高效利用内存。",
"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",
"difficulty": 3,
"tags": [
"go",
"sync"
],
"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,导致竞态条件。",
"source": null,
"related": []
},
{
"id": "fb-006",
"type": "fill_blank",
"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都被阻塞,提高了调度效率。",
"source": null,
"related": []
},
{
"id": "fb-008",
"type": "fill_blank",
"difficulty": 4,
"tags": [
"java",
"aqs"
],
"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唤醒后继节点。",
"source": null,
"related": []
},
{
"id": "fb-009",
"type": "fill_blank",
"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()底层使用它。任务粒度过细会导致调度开销过大,过粗则无法充分利用并行性。",
"source": null,
"related": []
},
{
"id": "fb-010",
"type": "fill_blank",
"difficulty": 5,
"tags": [
"go",
"pprof"
],
"question": "Go的pprof工具支持多种profile类型:CPU profile记录程序在各位置的采样频率,heap profile记录内存分配,block profile记录goroutine在同步原语上的阻塞时间,而______ profile专门记录goroutine的调用栈,用于排查goroutine泄漏问题。",
"answer": [
"goroutine",
"goroutine profile"
],
"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进行可视化分析或生成火焰图。",
"source": null,
"related": []
}
]
}
@@ -0,0 +1,25 @@
{
"slug": "go-java-concurrency",
"name": "Go/Java并发模型",
"description": "GMP调度、CSP模型、AQS/ForkJoinPool、goroutine泄漏、JMM、happens-before、pprof/JFR",
"tags": [],
"question_files": {
"single_choice": "single_choice.json",
"true_false": "true_false.json",
"fill_blank": "fill_blank.json",
"short_answer": "short_answer.json",
"code_reading": "code_reading.json"
},
"stats": {
"total": 45,
"by_type": {
"single_choice": 15,
"true_false": 10,
"fill_blank": 10,
"short_answer": 5,
"code_reading": 5
}
},
"created": "2026-09-09",
"updated": "2026-09-09"
}
@@ -0,0 +1,138 @@
{
"topic": "go-java-concurrency",
"type": "short_answer",
"schema_version": "1.0.0",
"generated": "2026-09-09T16:21:56+08:00",
"questions": [
{
"id": "sa-001",
"type": "short_answer",
"difficulty": 3,
"tags": [
"go",
"gmp"
],
"question": "请简述Go语言GMP调度模型中work-stealing机制的工作原理,包括什么情况下会触发stealing,以及stealing的来源优先级。",
"answer": "在GMP模型中,每个P维护一个本地运行队列(local run queue),同时存在一个全局运行队列(global run queue)。当一个P的本地队列为空时,该P对应的M(线程)会按照以下顺序尝试获取新的G(goroutine):1)先检查全局队列,通过GOMAXPROCS/2的固定频率周期性检查全局队列防止饥饿;2)若本地队列为空且全局队列也无可用G,则通过netpoller检查网络轮询器中是否有就绪的G;3)若仍无,则从其他P的本地队列中窃取一半的G(work-stealing)。stealing的来源是随机选择另一个P,取其本地队列长度的一半G,放到自己的本地队列中运行。这一机制保证了各P之间的负载均衡,避免某个P空闲而其他P过载。",
"keywords": [
"work-stealing",
"本地队列",
"全局队列",
"空闲P",
"负载均衡",
"netpoller"
],
"scoring_rubric": "满分需涵盖:(1) 本地队列为空时触发stealing(2分);(2) stolen来源是其他P的本地队列且取一半G(2分);(3) 提及全局队列周期性检查防止饥饿(1分);(4) 提及netpoller作为中间检查步骤(1分)。缺少work-stealing核心描述扣2分,未提及steal数量(一半)扣1分。",
"explanation": "work-stealing是GMP模型保持负载均衡的核心机制。当一个P处理完本地所有G后,它不会立即阻塞,而是主动去其他忙碌的P那里'偷'任务。Go调度器的完整G获取顺序为:本地队列→全局队列(每61次调度检查一次)→netpoller→随机stealing。这种设计在保持调度效率的同时,通过周期性全局队列检查避免了全局队列中的G被永久饥饿。",
"source": null,
"related": []
},
{
"id": "sa-002",
"type": "short_answer",
"difficulty": 4,
"tags": [
"go",
"channel",
"hchan"
],
"question": "从底层实现角度,解释Go channel中hchan结构体的核心字段及其作用。当一个goroutine向无缓冲channel发送数据时,发生了哪些步骤?",
"answer": "hchan是channel的底层结构体,核心字段包括:1)qcount uint:队列中的元素个数;2)dataqsiz uint:环形队列的容量(缓冲区大小,无缓冲channel为0);3)buf unsafe.Pointer:指向环形队列的指针;4)sendx uint:环形队列中发送位置的索引;5)recvx uint:环形队列中接收位置的索引;6)sendq waitq:发送等待队列,存放因发送阻塞的goroutine(sudog链表);7)recvq waitq:接收等待队列,存放因接收阻塞的goroutine;8)lock mutex:保护hchan所有字段的互斥锁。\n\n无缓冲channel发送数据的步骤:1)加锁(lock(ch.lock));2)尝试直接将数据拷贝给正在等待接收的goroutine(recvq不为空时),此时不需要经过环形队列;3)若没有接收者且环形队列未满,则将数据拷贝到buf中(但无缓冲channel这步不会执行);4)若无接收者且无法入队,则当前goroutine被封装为sudog放入sendq等待队列,并挂起(gopark);5)当有goroutine接收时,被挂起的发送者被唤醒,数据直接从发送者的栈拷贝到接收者的栈。",
"keywords": [
"hchan",
"qcount",
"buf",
"sendq",
"recvq",
"lock",
"sudog",
"gopark",
"直接拷贝"
],
"scoring_rubric": "满分需涵盖:(1) 正确列出hchan核心字段(qcount/buf/sendx/recvx/sendq/recvq/lock)至少5个(3分);(2) 无缓冲channel发送步骤中提及直接拷贝给等待接收者(2分);(3) 无接收者时goroutine阻塞挂起(gopark)(2分);(4) 提及lock互斥锁保护(1分)。字段缺失2个以上扣1分,未说明无缓冲channel特殊性扣1分。",
"explanation": "hchan是Go channel的核心数据结构,所有channel操作都在其上加锁进行。Go对channel做了大量优化:当有goroutine在等待时,发送和接收操作会绕过环形队列,直接在goroutine栈之间拷贝数据(hand-off机制),避免了额外的内存拷贝。这解释了为什么在高并发场景下Go channel的性能优于共享内存+锁的方案。无缓冲channel的dataqsiz为0,buf为nil,所有数据交换都通过sudog等待队列和直接拷贝完成。",
"source": null,
"related": []
},
{
"id": "sa-003",
"type": "short_answer",
"difficulty": 4,
"tags": [
"java",
"aqs",
"synchronized"
],
"question": "请解释Java中AQS(AbstractQueuedSynchronizer)的核心设计思想,并说明ReentrantLock的公平锁和非公平锁在AQS层面的主要区别。",
"answer": "AQS是Java并发包(JUC)的基石,其核心设计思想是:通过一个volatile int state变量表示同步状态,配合一个FIFO的CLH(Craig, Landin, and Hagersten)队列来管理等待获取锁的线程。子类通过重写tryAcquire/tryRelease(独占模式)或tryAcquireShared/tryReleaseShared(共享模式)来实现不同的同步器。\n\nReentrantLock在AQS层面的公平锁与非公平锁区别:\n1)非公平锁(默认):线程调用lock()时,先尝试直接通过CAS修改state抢占锁,若成功直接获取锁进入临界区,不关心CLH队列中是否有等待线程。只有CAS失败时才会走正常的acquire流程(可能入队等待)。\n2)公平锁:线程调用lock()时,先检查CLH队列中是否有前驱节点在等待,若有则必须入队排队,遵循FIFO顺序获取锁。\n\n两者在tryAcquire中的关键差异:非公平锁的tryAcquire中没有hasQueuedPredecessors()检查;公平锁在发现state为0(锁空闲)时,会先调用hasQueuedPredecessors()判断是否有等待线程,若有则不抢锁,返回false让当前线程入队。",
"keywords": [
"AQS",
"state",
"CLH队列",
"CAS",
"tryAcquire",
"hasQueuedPredecessors",
"公平锁",
"非公平锁"
],
"scoring_rubric": "满分需涵盖:(1) AQS核心:state变量 + CLH队列(2分);(2) 子类重写tryAcquire/tryRelease(1分);(3) 非公平锁直接CAS抢占(2分);(4) 公平锁检查hasQueuedPredecessors排队(2分)。未提及CLH队列扣2分,公平/非公平区别描述不清扣2分。",
"explanation": "AQS采用模板方法模式:模板方法(acquire/acquireShared)定义了获取锁的通用流程(try成功则返回,失败则入队并park),具体逻辑由子类通过tryAcquire等钩子方法实现。CLH队列是其高效的关键:每个等待线程被封装为Node节点,通过自旋+CAS+LockSupport.park/unpark实现低延迟的线程唤醒。非公平锁吞吐量通常高于公平锁,因为减少了线程上下文切换,但可能导致线程饥饿。",
"source": null,
"related": []
},
{
"id": "sa-004",
"type": "short_answer",
"difficulty": 5,
"tags": [
"java",
"jmm",
"volatile"
],
"question": "请解释Java内存模型(JMM)中volatile关键字的两条语义规则,并举例说明在什么场景下仅靠volatile无法保证线程安全(即需要synchronized或Atomic类的原因)。",
"answer": "volatile的两条语义规则:\n1)可见性保证(写后读):对volatile变量的写操作 happens-before 后续对该变量的读操作。这意味着写入线程对volatile变量的新值以及该写操作之前(程序顺序上)对其他变量的修改,都会对读取线程可见。底层通过内存屏障(Store Barrier + Load Barrier)实现:写时插入StoreStore和StoreLoad屏障,读时插入LoadLoad和LoadStore屏障。\n2)禁止指令重排序:volatile通过内存屏障阻止编译器和CPU对volatile变量的读写操作与其前后的指令进行重排序,保证了一定的有序性。\n\n仅靠volatile无法保证线程安全的场景——复合操作(read-modify-write):\n例1:volatile int count = 0; count++不是原子操作,它包含读count、加1、写count三步。两个线程同时执行count++,可能都读到相同的旧值,导致最终结果只加了1而非2。\n例2:volatile引用的非原子更新:volatile Object ref; ref = new Object(); 虽然ref的赋值本身是安全的,但如果有检查后执行(check-then-act)模式如 if (ref == null) ref = new Object(); 仍存在竞态条件。\n\n解决方案:使用synchronized块保证复合操作的原子性,或使用AtomicInteger等基于CAS的原子类。",
"keywords": [
"JMM",
"volatile",
"happens-before",
"内存屏障",
"可见性",
"有序性",
"原子性",
"CAS",
"复合操作"
],
"scoring_rubric": "满分需涵盖:(1) 可见性语义:写happens-before读(2分);(2) 禁止指令重排序(2分);(3) 举例说明复合操作(如count++)非原子(2分);(4) 提及内存屏障实现原理(1分)。两条语义缺一扣2分,无反例说明扣2分,未提及解决方案扣1分。",
"explanation": "volatile解决的是可见性和有序性问题,但不解决原子性问题。JMM的happens-before八大规则中,volatile规则是:对volatile变量的写happens-before后续对同一变量的读。这意味着volatile写之前的所有操作(包括普通变量的写)都对volatile读之后的操作可见。但count++这样的操作涉及多次内存访问,volatile只能保证每次读到最新值,不能保证读-改-写这个整体是原子的。这就是为什么需要AtomicInteger(内部使用CAS+volatile)或synchronized来保护复合操作。",
"source": null,
"related": []
},
{
"id": "sa-005",
"type": "short_answer",
"difficulty": 5,
"tags": [
"go",
"pprof",
"java"
],
"question": "请分别描述如何使用Go pprof和Java JFR(Java Flight Recorder)定位goroutine泄漏问题,包括关键的分析步骤和关注的指标。",
"answer": "Go pprof定位goroutine泄漏:\n1)引入net/http/pprof包或使用runtime/pprof:在HTTP服务中导入_ \"net/http/pprof\"并启动debug/pprof端点;或在程序中通过pprof.StartCPUProfile等API采集。\n2)关键端点:访问/debug/pprof/goroutine?debug=1查看所有goroutine的完整堆栈;debug=2显示所有goroutine的原始堆栈。\n3)分析步骤:采集goroutine profile → 按堆栈分组查看数量 → 重点关注阻塞在channel收发(chansend/chanrecv)、select、time.Sleep、net.Conn.Read/Write的goroutine → 如果某个堆栈的goroutine数量持续增长,则定位到泄漏点。\n4)结合race detector(go run -race)检测数据竞争,以及使用go tool pprof分析CPU和内存profile辅助定位。\n5)关注指标:goroutine总数(runtime.NumGoroutine())、各阻塞点的goroutine分布。\n\nJava JFR定位线程泄漏:\n1)启动JFR:java -XX:StartFlightRecording=duration=60s,filename=recording.jfr 或通过jcmd pid JFR.start。\n2)关键事件:关注jdk.ThreadStart和jdk.ThreadEnd事件,可统计线程创建/销毁速率;jdk.ThreadLock事件可发现线程阻塞原因。\n3)分析步骤:用JDK Mission Control(JMC)打开.jfr文件 → 查看'线程'面板中的线程总数和线程状态分布 → 关注BLOCKED和WAITING状态的线程数是否持续增长 → 检查线程的堆栈,定位到wait()/park()/sleep()等阻塞点。\n4)关注指标:活跃线程数、BLOCKED/WAITING线程数趋势、线程池核心参数与实际使用量对比(如ThreadPoolExecutor的activeCount与corePoolSize)。",
"keywords": [
"pprof",
"goroutine泄漏",
"debug/pprof/goroutine",
"JFR",
"JMC",
"ThreadStart",
"ThreadEnd",
"阻塞分析",
"线程状态"
],
"scoring_rubric": "Go部分满分4分:(1) pprof接入方式(1分);(2) goroutine profile查看方法(1分);(3) 分析步骤与阻塞点定位(2分)。Java部分满分4分:(1) JFR启动方式(1分);(2) 关键事件(ThreadStart/End)(1分);(3) JMC分析步骤与线程状态关注(2分)。两个工具缺一扣4分,分析步骤不完整扣2分。",
"explanation": "goroutine泄漏是Go中常见但隐蔽的性能问题,因为goroutine创建成本低,容易被大量创建后遗忘。pprof的goroutine profile是最直接的诊断工具,debug=1模式下可看到每个goroutine的创建位置和当前阻塞原因,通过分组统计即可快速定位泄漏点。Java JFR是JDK内置的低开销(<1%性能影响)诊断工具,适合生产环境长期运行。JMC提供可视化界面,Thread面板可按线程状态分组展示,配合堆栈信息可精确定位线程泄漏或阻塞的根因。两者的核心思路一致:采集→分组统计→定位阻塞点→分析根因。",
"source": null,
"related": []
}
]
}
@@ -0,0 +1,332 @@
{
"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": []
}
]
}
@@ -0,0 +1,152 @@
{
"topic": "go-java-concurrency",
"type": "true_false",
"schema_version": "1.0.0",
"generated": "2026-09-09T16:21:56+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 <pid> 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": []
}
]
}