6.24面试日记
6.24面试日记
由于明后两天有连着三场面试,并且这次面试过后就是期末考试和暑假了,还是很希望在暑假找个实习然后不回家的,所以得好好准备一把了。
简历上的部分




这是我简历上的内容,我的简历缺点是缺少中间件了,但是我怕写了答不出来就没写了,我还是太菜了,八股文背得少,诶诶诶。
想到的可能在简历上问的八股
GMP调度模型
G:goroutine是一种比线程更轻量级的并发编程方式,它允许在单个线程内执行多个任务,并且可以在任务之间进行切换,而不需要线程上下文切换的开销。协程通过协作式多任务处理来实现并发,这意味着任务之间的切换是由程序显示控制的,而不是由操作系统调度的。
以下是协程的一些关键特征:
- 轻量级:协程的创建和切换开销非常小,因为它们不需要操作系统级别的线程管理。
- 非抢占式:协程的切换是显式的,由程序员在代码中指定,而不是由操作系统抢占式地调度。
- 状态保存:协程可以在暂停执行时保存其状态,并在恢复执行时从暂停的地方开始。
- 异步编程:协程非常适合用于异步编程,特别是在I/O密集型任务中,可以在等待I/O操作完成时切换到其他任务,从而提高程序的并发性和效率。
Goroutine实现原理
Goroutine的实现原理包括Goroutine的创建、调度、上下文切换和栈管理等多个方面。通过GMP模型和高效的调度机制,GO运行时能够高效地管理和调度大量的Goroutine,实现高并发编程。
Goroutine的创建
当使用go关键字启动一个新的Goroutine时,go运行时会执行以下步骤:
- 分配G结构体:Go运行时会为新的Goroutine分配一个G结构体,其中包含Goroutine的状态信息,栈指针,程序计数器等。
- 分配栈空间:Go运行时会为新的Goroutine分配初始的栈空间,通常是几kb。
- 初始化G结构体,将Goroutine的入口函数、参数、栈指针等信息填入G结构体中。
- 将Goroutine加入调度队列:Go运行时会将新的Goroutine加入到某个Processor的本地运行队列中,等待调度执行。
Goroutine的调度
go运行时使用GMP模型来管理和调度Goroutine
- P(Processor):P是Go运行时的一个抽象概念,表示一个逻辑处理器。每个P持有一个本地运行队列,用于存储执行的Goroutine。P的数量通常等于机器的CPU核心数,可以通过runtime.GOMAXPROCS函数设置。
- M(Machine):M表示一个操作系统线程。M负责实际执行P中的Goroutine。M和P是一对一绑定的关系,一个M只能绑定一个P,但应该P可以被多个M绑定(通过抢占机制)。M的数量是由Go运行时系统动态管理和确定的。M的数量并不是固定的,而是根据程序的运行情况和系统资源的使用情况动态调整的。通过runtime.NumGoroutine()和runtime.NumCPU()函数,我们可以查看当前的Goroutine数量和CPU核心数。Go运行时对M的数量有一个默认的最大限制,以防止创建过多的M导致系统资源耗尽。这个限制可以通过环境变量GOMAXPROCS进行调整,但通常不需要手动设置。
- G(Goroutine):代表一个Goroutine,它有自己的栈,instruction pointer和其他信息(正在等待的channel等等),用于调度。
- 调度循环:每个P会在一个循环中不断从本地运行队列中取出Goroutine,并将其分配给绑定的M执行。如果P的本地运行队列为空,P会尝试从其他的P的本地运行队列中窃取Goroutine(工作窃取机制)。
P的数量可以大于器的CPU核心数?
在Go语言中,P(Processor)的数量通常等于机器的CPU核心数,但也可以通过runtime.GOMAXPROCS函数进行调整。默认情况下,Go运行时会将P的数量设置为机器的逻辑CPU核心数。然而,P的数量可以被设置为大于或小于机器的CPU核心数,这取决于具体的应用需求和性能考虑。
P的数量与CPU核心数的影响
- P的数量大于CPU核心数的影响
- 上下文切换增加:当P的数量大于CPU核心数时,可能会导致更多的上下文切换。因为操作系统需要在有限的CPU核心上调度更多的线程M。这可能会增加调度开销。
- 资源竞争:更多的P意味着更多的Goroutine可以同时运行,但这也导致了更多的资源竞争,特别是在I/O密集型任务中。过多的P可能会导致资源争用,反而降低程序的整体性能,
- 并发性提高:在某些情况下,增加P的数量可以提高程序的并发性,特别是在存在大量阻塞操作(如I/O操作)的情况下。更多的P可以更好地利用CPU资源,减少阻塞时间。
- P的数量少于CPU核心数量的影响
- CPU利用率降低:当P的数量小于CPU核心数时,可能会导致CPU资源未被充分利用。因为P的数量限制了同时运行的Goroutine数量,可能会导致某些CPU核心处于空闲状态。
- 减少上下文切换:较少的P数量可以减少上下文切换的开销,因为操作系统需要调度的线程M数量减少。这可能会提高CPU密集型任务的性能。
选择合适的P数量选择合适的P数量需要根据具体的应用场景和性能需求进行调整,以下是一些建议:
- CPU密集型任务:对于CPU密集型任务,通常将P的数量设置为等于或接近机器的逻辑CPU核心数,以充分利用CPU资源。
- I/O密集型任务:对于I/O密集型任务,可以考虑将P的数量设置为大于CPU核心数,以提高并发性和资源利用率。
- 性能测试和调优:通过性能测试和调优,找到最佳的P数量设置。可以尝试不同的P数量,观察出现的性能表现,选择最优的配置。
Goroutine的栈管理
Goroutine的栈空间是动态分配的,可以根据需要自动扩展。Go运行时使用分段栈活连续栈来管理Goroutine的栈空间:
- 分段栈:在早期版本的Go中,GOroutine使用分段栈。每个GOroutine的栈由多个小段组成,当栈空间不足时,Go运行时会分配新的栈段并链接到现有的栈段上。
- 连续栈:在GO1.3及以后的版本中,GOroutine使用连续栈。每个GOroutine的栈是一个连续的内存块,当栈空间不足时,Go运行时会分配一个更大的栈,并将现有的栈内容复制到新的栈中。
GC机制
Golang的垃圾回收机制采用并发三色标记清除法+混合写屏障来实现
并发三色标记清除法
具体来说,根据对象的应用关系:
- 有黑白灰三个集合,初始所有对象都是白色
- 从root对象开始标记,将所有可达对象标记为灰色
- 从灰色对象集合取出对象,将其应用的对象标记为灰色,放入灰色集合,并将自己标记为黑色
- 重复上一步,知道灰色集合为空,即所有可达对象都被标记了灰或黑
- 将白色对象进行清除回收
- 重置GC状态
并发GC导致的问题
因为整个流程与代码并发执行,所以会出现一些edge case导致对象被漏标,多标的情况
漏标问题指的是在用户协程与GC协程并发执行的场景下,部分存货对象未被标记从而误删
多标指的是在用户协程与GC协程并发执行的场景下,部分垃圾对象被误标记导致GC未按时将其回收
屏障机制
屏障机制类似于一个回调保护机制,指的是在完成某个特定动作前,会先完成屏障设置的内容
其次一套用于解决漏标多标问题的方法论被提出来,称之为强弱三色不变式:
- 强三色不变式:三色标记需要维护:白色对象不能被黑色对象直接引用
- 弱三色不变式:三色标记需要维护:白色对象可以被黑色对象引用,但要从某个灰对象出发仍然可达该白色对象
插入写屏障的目标是实现强三色不变式,保证当一个黑色对象指向一个白色对象前,会先触发屏障将白色对象设置为灰色,再简历引用
删除写屏障的目标是实现弱三色不变式,保证当一个白色对象即将被上游删除引用前,会触发屏障将其置灰,之后再删除上游指向其的引用
简单理解就是,要指向一个白色对象时,先触发屏障把颜色改为灰,保证GC扫描不出问题,再走后面的程序逻辑;同理要删一个白色对象时,先触发屏障把颜色改成灰色,保证GC扫描不出问题,再走后面的程序逻辑
了解过go的内存模型吗?大致讲一讲
go内存模型指定了一系列条件,在这些条件下,可以保证在一个Goroutine钟读取变量可以观察到其他Goroutine钟对同一变量所写的值。即安全在不同的协程中读写变量。
为什么需要这些条件?
编译器或处理器不保证指令执行顺序和程序书写顺序一致,同时读写变量由于变量体积不同,也不一定是原子性的。
channel是什么,有什么作用?
- channel是Goroutine之间的通信的管道,遵循"不要通过共享内存来通信,而是通过通信来共享内存"
- 类型化:每个channel只能传递特定类型的数据
- 主要用途:数据传递、Goroutine同步、信号通知
- 分类:无缓冲channel(同步,发送和接收必须同时准备好)、有缓冲channel(异步,有一定容量)。
sync.WaitGroup的用途和使用场景?
解读:考察并发同步原语的使用。
思路:
- sync.WaitGroup用于等待一组Goroutine完成执行。
- 主要方法:Add(delta int)增加计数器,Done()减少计数器(通常在Goroutine结束时用defer调用),wait()阻塞直到计数器归零。
- 场景:主Goroutine需要等待多个子Goroutine完成某些任务后再继续执行。
select语句的作用是什么?
- 解读:考察多路复用channel操作的能力
- 思路:
- select语句勇者多个channel操作中进行选择。
- 行为类似switch,但其case是channel的发送或接收操作。
- 如果多个case同时就绪,select会随机选择一个执行。
- 可以有default子句,当所有case都不就绪执行default(实现非阻塞操作)。
如何实现一个Goroutine安全的计数器?
- 解读:考察并发访问共享资源时的同步问题。
- 思路:
- 直接并发修改共享变量会导致竞态条件。
- 方法1:使用sync.Mutex(互斥锁)保护临界区
- 方法2:;使用sync/atomic包提供的原子操作。
- 方式3:通过channel将所有修改串行到单个Goroutine处理。
context包的作用
-
解读:考察对Goroutine生命周期管理、取消、和超时的理解
-
主要功能:
- 取消:父context被取消时,其派生的所有子context也会被取消。Goroutine可以监听context.Done()channel来优雅退出
- 超时:可以设置context在特定时间点或一段时间后自动取消。
- 值传递:可以在context中携带请求范围的数据(主要传递特定信息,如用户ID、Trace ID)。
-
常见函数:
-
context.Background(), context.TODO(), context.WithCancel(), context.WithTimeout(), context.WithDeadline(), context.WithValue()
如何优雅地关闭一个channel并通知所有接受者?
- 解读:考察channel的关闭机制和广播信号。
- 思路:
- 通过close(ch)关闭channel。
- 关闭后,不能再向channel发送数据(会panic)
- 接收者可以从已经关闭的channel接受数据,会接收到已缓冲的值,如何对应类型的零值。
- 使用value,ok := <- ch语法判断channel是否已关闭
- 对于多个接受者,通常是发送者负责关闭。
什么是竞态条件?如何检测和避免?
- 解读:并发编程的核心问题之一
- 思路:
- 定义:多个Goroutine并发访问和修改共享资源,且最终结果依赖于这些操作的执行顺序
- 检测:Go提供了内置的竞态检测器。编译时使用-race标志(go run -race main.go或go test -race)。
- 避免:
- 互斥锁:保护对共享资源的访问。
- 原子操作(sync/atomic):对于基本数据类型进行原子读写
- channel:通过通信共享数据,将对共享资源的访问限制在单个Goroutine中
- 不可变数据:如果共享数据是不可变的,则不会发生竞态
sync.Once的使用场景是什么?
- 解读:考察如何确保某个操作只执行一次。
- 思路:
- sync.Once是一个对象,它包含一个Do(f func())方法
- Do方法会确保传入的函数f在所有的Goroutine中只执行一次,即使Do被多次调用
- 场景:单例模式的初始化、全局资源的惰性初始化等。
实现一个生产者-消费者模型
- 解读:综合考察channel、Goroutine和同步
- 思路:
- 生产者:生成数据并将其发送到channel
- 消费者:从channel接受数据并处理
- channel:作为生产者和消费者之间的缓冲区
- 同步:
- 生产者生产完毕后,需要通知消费者(例如关闭channel)
- 主Goroutine可能需要等待生产者和消费者都完成
package main
import (
"fmt"
"time"
)
func producer(ch chan<- int) {
for i := 1; i <= 5; i++ {
ch <- i
fmt.Println("生产者生产:", i)
time.Sleep(time.Second)
}
close(ch)
}
func consumer(ch <-chan int, done chan<- bool) {
for num := range ch {
fmt.Println("消费者消费:", num)
time.Sleep(2 * time.Second)
}
done <- true
}
func main() {
ch := make(chan int, 3)
done := make(chan bool)
go producer(ch)
go consumer(ch, done)
<-done
fmt.Println("done")
}
go map
map是一种key-value组成的哈希表,但是哈希表有一大问题就是哈希冲突,这在cpp中的解读就是在codeforces上敢用unorder_map绝对会被hack,因为哈希冲突会导致查询非常缓慢
那么go是如何解决这一难题:go map使用链地址法解决哈希冲突。当哈希冲突发生时,新的键值对会被添加到冲突位置对应的桶中,形成链表。以下是链地址法解决哈希冲突的步骤:
- 计算哈希值
- 确定桶位置
- 遍历链表
- 插入键值对:如果链表中不存在相同的键,则将键值对插入到链表的尾部;如果存在相同的键,则更新键值的对。
go slice
slice 实际上是一个结构体,包含三个字段:长度、容量、底层数组。
- array是底层数组的指针
- len是切片的长度
- cap是底层数组的长度,也就是切片的最大容量,cap的值永远大于等于len的值
切片是对数组的封装,提供了对数组中部分连续片段的引用,在运行期间可以修改它的长度和范围。
- [:] 表示截取全部的数组或切片作为新切片 [0:max)
- [:high] 截取 [0,high) 的数组或切片作为新切片
- [low:] 截取 [low,max) 的数组或切片作为新切片
在切片达到容量上限时,继续 append 操作会导致切片扩容。
微服务相关
什么是微服务
微服务架构是一种架构模式或一种架构风格,它提倡将单一应用程序划分成一组小的服务,每个服务运行在其独立的自己的进程中,服务之间互相协调、互相配合,为用户提供最终价值。
微服务之间是如何通讯的?
第一种是远程过程调用
也就是rpc,比如grpc,thrift,等等
有点是简单,常见,因为没有中间件代理,系统更简单
缺点是只支持请求/响应的模式,不支持别的,比如通知、请求/异步响应等
降低了可用性,因为客户端和服务端在请求过程中必须是可用的
第二种是消息
使用异步消息来做服务间的通信。服务间通过消息管道来交换信息,从而通信
比如rocketmq,kafka等
优点是把服务端和客户端解耦,更松耦合,提高可用性,因为消息中间件缓存了消息,直到消费者可以消费
支持很多通信机制比如通知、请求、异步响应、发步、订阅、发步、异步响应
缺点是消息中间件有额外的复杂度开销
微服务的优缺点
优点:
- 优点是内聚足够小,代码容易理解
- 开发简单,效率高
- 微服务能被小团队独立开发
- 微服务松耦合,是有可能意义的服务,无论是在开发阶段或部署阶段都是独立的
- 微服务能使用不同的语言开发
- 易于和第三方集成,微服务运行容易且灵活的方式集成自动部署,通过持续集成工具
- 微服务容易被理解,便于修改和维护
- 每个微服务都有自己的存储能力,可以有自己的数据库。也可以有统一数据库
缺点:
- 开发人员要处理分布式系统的复杂性
- 多服务运维难度,随着服务的增加,运维压力也在增大
- 系统部署依赖
- 服务间通信成本
- 数据一致性
- 系统集成测试
- 性能监控
消息队列相关
什么是消息队列
消息队列是一个使用队列来通信的组件,它的本质就是一个转发器包含发消息、存消息、消费消息的过程。
我们通常说的消息队列,简称mq,它其实就是指消息中间件,当前业务比较流行的开源信息中间件包括rabbitmq,rocketmq,kafka。
消息队列有什么使用场景(为什么使用消息队列)
- 应用解耦
- 流量削峰
- 异步处理
- 消息通讯
- 远程调用
生产者保证不丢失消息
生产端如何保证不丢失消息?确保生产的消息能达到存储端
如果是rocketmq消息中间件,producer生产者提供了三种发送消息的方式,分别是
- 同步发送
- 异步发送
- 单向发送
生产者要想发消息时保证消息不丢失,可以:
- 采用同步方式发送,send消息发送方法返回成功状态,就表示消息正常到达了存储端broker
- 如果send消息异常或者返回非成功状态,可以重试
- 可以使用事物消息,Rocketmq的事物消息机制就是为了保证零丢失来设计的
存储端不丢失消息
确保消息持久化到磁盘。大家很容易想到就是刷盘机制。
刷盘机制分同步刷盘和异步刷盘:
- 生产者消息发过来时,只有持久化到磁盘,rocketmq的存储端broker才返回一个成功的ACK相应,这就是同步刷盘。它保证消息不丢失,但是影响了性能。
- 异步刷盘的话。只要消息写入pagecache缓存,就返回一个成功的ACK相应
Broker一般是集群部署的,有master主节点和slave从节点。消息到Broker存储端,只有主节点和从节点都写入成功,才反馈成功的ack给生产者。这就是同步复制,它保证了消息不丢失,但是降低了系统的吞吐量。与之对应的就是异步复制,只要消息写入主节点成功,就返回成功的ack,它速度快,但是会有性能问题。
消息队列如何保证消息的顺序性
消息的有序性,就是指可以按照消息的发送顺序来消费。这些业务对消息的顺序是有要求的比如先下单再付款,最后再完成订单,这样等。假设生产者先后生产了两条消息,分别是下单消息,付款消息,M1比M2先产生,如何保证M1比M2先被消费呢
为了保证消息的顺序性,可以将将M1、M2发送到同一个Server上,当M1发送完收到ack后,M2再发送,这样还是可能会有问题,因为从MQ服务器到服务端,可能存在网络延迟,虽然M1先发送,但是它比M2晚到。那还能怎么办才能保证消息的顺序性呢?将M1和M2发往同一个消费者,且发送M1后,等到消费端ACK成功后,才发送M2就得了。
git:merge和rebase的区别
在git中,merge的原理就是将要合并分支的最新提交组合成一个新的提交,并且插入到目标分支中
merge会保留所有历史提交,不会破坏历史提交的结构,这一点在多人协作的时候很重要.
与merge不同的是,rebase并不会保留原有的提交,而是会创建当前分支比目标分支更新的所有提交的副本
但rebase会改变提交历史
merge和rebase最大的区别在于是否会保留原有的提交
merge会对提交历史进行保留,适合多人协作开发的场景,rebase简洁易读,更适合个人开发和整理分支情况

浙公网安备 33010602011771号