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