Files
examination/topics/interview-prep/go-java-concurrency/short_answer.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

138 lines
13 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": "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": []
}
]
}