面试题答案
一键面试原理区别
- sync.Once:
- sync.Once 内部使用一个标志位(done)和一个互斥锁(m)。首次调用 Do 方法时,互斥锁会被锁定,检查标志位,如果标志位为未完成状态,则执行传入的函数,执行完后设置标志位为已完成,最后解锁互斥锁。后续调用 Do 方法时,直接检查标志位,若已完成则不再执行函数,从而确保只执行一次初始化操作。
- 传统双重检查锁定(DCL):
- 在传统 DCL 实现中,会在获取锁之前先检查实例是否已经创建(第一次检查),如果未创建则获取锁,再次检查实例是否创建(第二次检查),若仍未创建则创建实例。这种方式旨在减少锁的竞争,因为大部分情况下第一次检查就能确定实例已存在,无需获取锁。
线程安全性区别
- sync.Once:
- Go 语言的 sync.Once 是完全线程安全的。由于 Go 语言的内存模型保证了对共享变量的读写操作的原子性和可见性,sync.Once 内部的实现利用了这种特性,确保在多线程环境下初始化操作只执行一次,且不同线程看到的实例状态一致。
- 传统双重检查锁定(DCL):
- 在早期的 Java 等语言中,传统 DCL 存在线程安全问题。因为在多线程环境下,可能会出现指令重排序的情况。例如,在实例初始化过程中,对象可能会先被分配内存但还未完全初始化,此时另一个线程通过第一次检查发现实例不为空,就直接使用了未完全初始化的实例,导致错误。后来在 Java 5.0 及之后,通过使用 volatile 关键字修饰实例变量,解决了指令重排序问题,从而保证了线程安全性。但在其他一些语言中,如果没有类似的内存屏障机制,传统 DCL 可能无法保证完全的线程安全。
性能区别
- sync.Once:
- sync.Once 的性能较好。在首次初始化时,虽然需要获取和释放互斥锁,但之后的调用只需检查标志位,无需再次获取锁,开销较小。而且 Go 语言的运行时系统对互斥锁的实现进行了优化,使得锁的竞争开销相对较低。
- 传统双重检查锁定(DCL):
- 传统 DCL 的性能取决于实际的使用场景。如果初始化操作非常频繁,由于每次获取锁都有一定的开销,即使通过双重检查减少了锁的竞争,整体性能可能仍不如 sync.Once。但如果初始化操作很少发生,且多线程环境下竞争不激烈,DCL 可以通过减少锁的获取次数来提高性能,因为大部分情况下可以通过第一次检查避免获取锁。然而,由于 volatile 关键字可能会带来额外的内存屏障开销,对性能也有一定影响。