✍个人博客:Pandaconda-CSDN博客
📣专栏地址:http://t.csdnimg.cn/UWz06
📚专栏简介:在这个专栏中,我将会分享 Golang 面试中常见的面试题给大家~
❤️如果有收获的话,欢迎点赞👍收藏📁,您的支持就是我创作的最大动力💪
19. 原 子操作和锁的区别
原子操作是一种特殊的操作,它可以在单个 CPU 指令周期内完成对共享变量的读取和修改,从而保证了操作的原子性。在 Go 中,使用 sync/atomic
包提供的原子操作函数可以对共享变量进行原子操作,从而避免了多个 Goroutine 对同一变量进行并发修改时出现的竞争条件问题。原子操作不需要获取锁,因此效率比锁更高,但是只适用于一些简单的操作,比如读取和修改整数类型的变量。
锁是另一种保证并发安全的机制,它可以确保同一时间只有一个 Goroutine 可以访问共享资源。在 Go 中,使用 sync
包提供的锁可以实现互斥访问共享资源的目的。锁的机制需要获取锁才能对共享变量进行操作,因此效率比原子操作略低。但是锁可以用于任何类型的变量,而不仅仅是整数类型的变量,因此可以用于更复杂的操作。
Go 中的原子操作和锁(Mutex)都是用于实现并发控制的机制,它们的主要区别如下:
1、作用范围
原子操作用于对单个共享变量的读写进行原子性的保障,而锁则用于对一段代码(即临界区)进行互斥访问,保障多个 goroutine 之间对共享资源的访问顺序。
2、并发性
原子操作的实现是通过硬件级别上的指令保证原子性,因此在高并发情况下执行效率较高。而锁则需要在多个 goroutine 之间进行状态切换和内核态和用户态之间的切换,因此在高并发情况下执行效率相对较低。
3、使用场景
原子操作适合于对单个共享变量进行频繁的读写操作,例如计数器等场景。锁则适用于需要对一段临界区进行互斥访问的场景,例如多个 goroutine 对同一数据结构进行操作时。
4、错误处理
在使用原子操作时,出现错误可能会导致程序崩溃,因此需要仔细地处理错误。而在使用锁时,错误处理相对简单,可以使用 defer 关键字保证锁的正确释放。
综上所述,如果需要对简单的整数类型变量进行原子操作,可以使用原子操作;如果需要对任意类型的变量进行并发安全的操作,应该使用锁。需要根据具体的应用场景选择使用哪种机制,以获得最佳的性能和可靠性。
20. 什么是 CAS?
CAS,即 Compare-And-Swap,是一种常见的并发控制机制,也是原子操作的一种。它用于实现在多个线程并发修改同一数据时的同步和互斥访问,是实现锁、并发队列等数据结构的基础。
CAS 操作需要三个参数:内存地址 V,期望值 A 和新值 B。CAS 操作的执行过程如下:
-
比较内存地址 V 中存储的值与期望值 A 是否相等;
-
如果相等,则将内存地址 V 中存储的值更新为新值 B;
-
如果不相等,则说明其他线程已经修改了内存地址 V 中存储的值,此时 CAS 操作失败,需要重新尝试。
在 Go 中,使用 sync/atomic
包提供的 CompareAndSwapXXX()
函数可以执行 CAS 操作,其中 XXX
表示不同的数据类型。例如,CompareAndSwapInt32()
函数用于对一个 int32 类型的变量执行 CAS 操作。以下是一个简单的示例:
Go
package main
import (
"fmt"
"sync/atomic"
)
func main() {
var value int32 = 1
// 将 value 变量的值从 1 修改为 2
atomic.CompareAndSwapInt32(&value, 1, 2)
fmt.Println("value:", value)
}
在这个例子中,我们首先定义了一个 int32 类型的变量 value
,并使用 CompareAndSwapInt32()
函数将其从 1 修改为 2。CompareAndSwapInt32()
函数的第一个参数是一个指向 int32 类型变量的指针,它告诉函数要对哪个变量进行 CAS 操作。第二个参数是期望值 A,第三个参数是新值 B。如果 value
的值与期望值 A 相等,则函数会将 value
的值更新为新值 B,并返回 true,否则不会更新 value
的值,并返回 false。在这个例子中,value
的初始值是 1,期望值 A 是 1,新值 B 是 2,因此 CAS 操作会成功,value
的值会被更新为 2。
需要注意的是,CAS 操作虽然可以避免锁的使用,提高了并发性能,但是也存在一些问题,比如 ABA 问题。因此在使用 CAS 操作时,需要谨慎设计并发控制策略,以确保线程安全。
21. syn c.Pool 有什么用?
sync.Pool
是 Go 标准库中的一个对象池实现,它的作用是缓存对象,减少对象的创建和垃圾回收,从而提高程序的性能。
在程序中,创建和销毁对象是很耗费时间和资源的操作,特别是在高并发情况下。如果能够复用已经创建好的对象,就可以减少对象的创建和垃圾回收,提高程序的性能。这就是对象池的作用。
sync.Pool
的实现比较简单,它维护了两个对象池:一个是空闲对象池,用于存储可重复使用的对象;另一个是新对象池,用于存储不能重复使用的对象。在使用对象时,首先从空闲对象池中获取对象,如果空闲对象池为空,则从新对象池中获取对象,如果新对象池也为空,则创建一个新对象。使用完对象后,将对象放回空闲对象池中。
需要注意的是,sync.Pool
并不保证对象一定会被重用。如果空闲对象池中没有可用的对象,或者对象已经达到了一定的数量限制,那么 sync.Pool
会选择创建新对象。因此,在使用 sync.Pool
时,需要谨慎设计对象的数量和生命周期,以确保对象的重复使用。
以下是一个简单的示例,演示了如何使用 sync.Pool
:
Go
package main
import (
"fmt"
"sync"
)
type Object struct {
// 定义对象属性
}
func main() {
var pool = sync.Pool{
New: func() interface{} {
// 创建新对象
return new(Object)
},
}
// 获取对象
object1 := pool.Get().(*Object)
fmt.Printf("object1: %p\n", object1)
// 放回对象池中
pool.Put(object1)
// 再次获取对象
object2 := pool.Get().(*Object)
fmt.Printf("object2: %p\n", object2)
}
在这个示例中,我们首先创建了一个对象池 pool
,并指定了 New
函数,用于创建新对象。然后我们从对象池中获取一个对象 object1
,并打印它的内存地址。接着,我们将对象 object1
放回对象池中,然后再次从对象池中获取对象 object2
,并打印它的内存地址。由于对象 object1
已经放回对象池中,所以对象池会优先从空闲对象池中获取对象,因此对象 object2
的内存地址和对象 object1
的内存地址相同。