Go 面试题:string 是线程安全的吗?

简介: Go 面试题:string 是线程安全的吗?

面试题如下:



如标题所示,原题是:Go 中的 string 赋值是线程安全的吗?


我们可以一起先想想答案,看看中不中。


线程安全是什么


线程安全是指在多线程环境下,程序的执行能够正确地处理多个线程并发访问共享数据的情况,保证程序的正确性和可靠性。


a38e574980823f4b8136fb30ce1fea91.png



能被称之为:线程安全,需要在多个线程同时访问共享数据时,满足如下几个条件:


  • 不会出现数据竞争(data race):多个线程同时对同一数据进行读写操作,导致数据不一致或未定义的行为。
  • 不会出现死锁(deadlock):多个线程互相等待对方释放资源而无法继续执行的情况。
  • 不会出现饥饿(starvation):某个线程因为资源分配不公而无法得到执行的情况。


string 线程安全


需要有一个基础了解,对于 string 类型,运行时表现对照是 StringHeader 结构体。


如下:


type StringHeader struct {
   Data uintptr
   Len  int
}


  • Data:存放指针,其指向具体的存储数据的内存区域。
  • Len:字符串的长度。


在了解前置知识后,接下来进入到实践环境。看看在 Go 里 string 类型的变量,做并发赋值到底是否线程安全。


案例一:并发访问


我们先看第一个案例,多个 goroutine 中并发访问同一个 string 变量的场景。如下代码:


package main
import (
 "fmt"
 "sync"
)
func main() {
 var wg sync.WaitGroup
 str := "脑子进煎鱼了"
 for i := 0; i < 5; i++ {
  wg.Add(1)
  go func() {
   defer wg.Done()
   fmt.Println(str)
  }()
 }
 wg.Wait()
}


输出结果:


脑子进煎鱼了
脑子进煎鱼了
脑子进煎鱼了
脑子进煎鱼了
脑子进煎鱼了


在上面的例子中,我们定义了一个 string 变量 str,然后启动了 5 个 goroutine,每个 goroutine 都会输出 str 的值。由于 str 是不可变类型,因此在多个 goroutine 中并发访问它是安全的。


可能有同学疑惑不可变类型是什么?


不可变类型,指的是一种不能被修改的数据类型,也称为值类型(value type)。不可变类型在创建后其值不能被改变,任何对它的修改操作都会返回一个新的值,而不会改变原有的值。


案例二:并发写入


第一个案例看起来没什么问题。我们再看第二个案例,针对多个 goroutine 并发写入的场景来进行验证。

如下代码:


func main() {
 var wg sync.WaitGroup
 str := "脑子进煎鱼了"
 for i := 0; i < 5; i++ {
  wg.Add(1)
  go func() {
   defer wg.Done()
   str += "!" // 修改 str 变量
   fmt.Println(str)
  }()
 }
 wg.Wait()
}


输出结果:


脑子进煎鱼了!
脑子进煎鱼了!!
脑子进煎鱼了!!!
脑子进煎鱼了!!!!
脑子进煎鱼了!!!!!


看起来没什么问题,还是正常的拼接结果,输出的顺序也完全没有问题的样子。(大雾)


我们再多运行几次。再看看输出结果:


// demo1
脑子进煎鱼了!
脑子进煎鱼了!!
脑子进煎鱼了!!!
脑子进煎鱼了!!!
脑子进煎鱼了!!!
// demo2
脑子进煎鱼了!
脑子进煎鱼了!!!
脑子进煎鱼了!!
脑子进煎鱼了!!!!!
脑子进煎鱼了!!!!


在上面的例子中,我们在每个 goroutine 中向 str 变量中添加了一个感叹号。由于多个 goroutine 同时修改了 str 变量,因此可能会出现数据竞争的情况。


我们会发现程序输出结果会出现乱序或不一致的情况,可以确认 string 类型变量在多个 goroutine 中是不安全的。


要警惕这种场景,在实际业务代码中,常有人前人留 BUG,后人因此翻车。主打一个熬夜查和修 BUG,分分钟还得洗脏数据。


string 实现线程安全


使用互斥锁


要实现 string 类型变量的线程安全,第一种方式:使用互斥锁(Mutex)来保护共享变量,确保同一时间只有一个 goroutine 可以访问它。下面是一个改造后的例子。


如下代码:


func main() {
 var wg sync.WaitGroup
 var mu sync.Mutex // 定义一个互斥锁
 str := "煎鱼"
 for i := 0; i < 5; i++ {
  wg.Add(1)
  go func() {
   defer wg.Done()
   mu.Lock() // 加锁
   str += "!"
   fmt.Println(str)
   mu.Unlock() // 解锁
  }()
 }
 wg.Wait()
}


输出结果:


煎鱼!
煎鱼!!
煎鱼!!!
煎鱼!!!!
煎鱼!!!!!


在上面的例子中,我们使用了 sync 包中的 Mutex 类型来定义一个互斥锁 mu。在每个 goroutine 中,我们先使用 mu.Lock() 方法来加锁,确保同一时间只有一个 goroutine 可以访问 str 变量。


再修改 str 变量的值并输出,最后使用 mu.Unlock() 方法来解锁,让其他 goroutine 可以继续访问 str 变量。


需要注意,互斥锁会带来一些性能上的开销,两全难齐美。

使用 atomic 包


第二种方案是使用 atomic 包来实现原子操作,如下代码:


func main() {
 var wg sync.WaitGroup
 var str atomic.Value // 定义一个原子变量
 str.Store("hello, world")
 for i := 0; i < 5; i++ {
  wg.Add(1)
  go func() {
   defer wg.Done()
   oldStr := str.Load().(string) // 读取原子变量的值
   newStr := oldStr + "!"
   str.Store(newStr) // 写入原子变量的值
   fmt.Println(newStr)
  }()
 }
 wg.Wait()
}


这样子也可以保证 string 类型变量的原子操作。但在现实场景下,仍然无法解决多 goroutine 导致的竞态条件(race condition)。


也就是存在多个 goroutine 并发取到的变量值都是一样的,得到的结果还是不固定的,最终还是要用 Mutex 或者 RWMutex 锁来做共享变量保护。


这两者没有绝对的好坏,但需要分清楚你的使用场景,决定用锁还是 atomic,又或是其他逻辑上的调整。


总结


在前面我们有把 StringHeader 结构体让大家看看,其实很明显是不支持线程安全的。平白无故每个类型都去支持线程安全的话,会增加很多开销。


绝大多数的情况下,你可以默认任何数据类型的变量赋值都不是线程安全的,除非他加了锁(Mutex)或 atomic(原子操作)。而在 string、slice、map 的并发写导致出错的场景,更是每隔一段时间就能在线上看到一两次。

每次做并发操作时,都建议想清楚,这个场景的到底需不需要保护共享变量,做好原子操作等。

目录
打赏
0
1
1
0
75
分享
相关文章
阿里面试:5000qps访问一个500ms的接口,如何设计线程池的核心线程数、最大线程数? 需要多少台机器?
本文由40岁老架构师尼恩撰写,针对一线互联网企业的高频面试题“如何确定系统的最佳线程数”进行系统化梳理。文章详细介绍了线程池设计的三个核心步骤:理论预估、压测验证和监控调整,并结合实际案例(5000qps、500ms响应时间、4核8G机器)给出具体参数设置建议。此外,还提供了《尼恩Java面试宝典PDF》等资源,帮助读者提升技术能力,顺利通过大厂面试。关注【技术自由圈】公众号,回复“领电子书”获取更多学习资料。
面试必看:如何设计一个可以优雅停止的线程?
嘿,大家好!我是小米。今天分享一篇关于“如何停止一个正在运行的线程”的面试干货。通过一次Java面试经历,我明白了停止线程不仅仅是技术问题,更是设计问题。Thread.stop()已被弃用,推荐使用Thread.interrupt()、标志位或ExecutorService来优雅地停止线程,避免资源泄漏和数据不一致。希望这篇文章能帮助你更好地理解Java多线程机制,面试顺利! 我是小米,喜欢分享技术的29岁程序员。欢迎关注我的微信公众号“软件求生”,获取更多技术干货!
88 53
面试大神教你:如何巧妙回答线程优先级这个经典考题?
大家好,我是小米。本文通过故事讲解Java面试中常见的线程优先级问题。小明和小华的故事帮助理解线程优先级:高优先级线程更可能被调度执行,但并非越高越好。实际开发需权衡业务需求,合理设置优先级。掌握线程优先级不仅能写出高效代码,还能在面试中脱颖而出。最后,小张因深入分析成功拿下Offer。希望这篇文章能助你在面试中游刃有余!
31 4
面试大神教你:如何巧妙回答线程优先级这个经典考题?
Java社招面试题:一个线程运行时发生异常会怎样?
大家好,我是小米。今天分享一个经典的 Java 面试题:线程运行时发生异常,程序会怎样处理?此问题考察 Java 线程和异常处理机制的理解。线程发生异常,默认会导致线程终止,但可以通过 try-catch 捕获并处理,避免影响其他线程。未捕获的异常可通过 Thread.UncaughtExceptionHandler 处理。线程池中的异常会被自动处理,不影响任务执行。希望这篇文章能帮助你深入理解 Java 线程异常处理机制,为面试做好准备。如果你觉得有帮助,欢迎收藏、转发!
49 14
Java 面试必问!线程构造方法和静态块的执行线程到底是谁?
大家好,我是小米。今天聊聊Java多线程面试题:线程类的构造方法和静态块是由哪个线程调用的?构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节有助于掌握Java多线程机制。下期再见! 简介: 本文通过一个常见的Java多线程面试题,详细讲解了线程类的构造方法和静态块是由哪个线程调用的。构造方法由创建线程实例的主线程调用,静态块在类加载时由主线程调用。理解这些细节对掌握Java多线程编程至关重要。
38 13
面试中的难题:线程异步执行后如何共享数据?
本文通过一个面试故事,详细讲解了Java中线程内部开启异步操作后如何安全地共享数据。介绍了异步操作的基本概念及常见实现方式(如CompletableFuture、ExecutorService),并重点探讨了volatile关键字、CountDownLatch和CompletableFuture等工具在线程间数据共享中的应用,帮助读者理解线程安全和内存可见性问题。通过这些方法,可以有效解决多线程环境下的数据共享挑战,提升编程效率和代码健壮性。
43 6
Java线程调度揭秘:从算法到策略,让你面试稳赢!
在社招面试中,关于线程调度和同步的相关问题常常让人感到棘手。今天,我们将深入解析Java中的线程调度算法、调度策略,探讨线程调度器、时间分片的工作原理,并带你了解常见的线程同步方法。让我们一起破解这些面试难题,提升你的Java并发编程技能!
68 16
面试直击:并发编程三要素+线程安全全攻略!
并发编程三要素为原子性、可见性和有序性,确保多线程操作的一致性和安全性。Java 中通过 `synchronized`、`Lock`、`volatile`、原子类和线程安全集合等机制保障线程安全。掌握这些概念和工具,能有效解决并发问题,编写高效稳定的多线程程序。
68 11
硬核揭秘:线程与进程的底层原理,面试高分必备!
嘿,大家好!我是小米,29岁的技术爱好者。今天来聊聊线程和进程的区别。进程是操作系统中运行的程序实例,有独立内存空间;线程是进程内的最小执行单元,共享内存。创建进程开销大但更安全,线程轻量高效但易引发数据竞争。面试时可强调:进程是资源分配单位,线程是CPU调度单位。根据不同场景选择合适的并发模型,如高并发用线程池。希望这篇文章能帮你更好地理解并回答面试中的相关问题,祝你早日拿下心仪的offer!
39 6
Java 多线程 面试题
Java 多线程 相关基础面试题

热门文章

最新文章

AI助理

你好,我是AI助理

可以解答问题、推荐解决方案等