之前写到基于CAS实现的锁,也写过各类语言实现单例模式,其中包括线程安全的实现和线程不安全的实现。线程安全的实现大致思路都是加锁,其中性能较好的是double-check的加锁方法。另外对于Java可以使用synchronized实现封锁,它本质上也是使用了锁;还有一类方式是使用静态内部类,由于类的加载机制,这样实现的单例模式也是线程安全的。类的加载机制也是使用了锁,哈哈,又绕回来。今天聊聊不用锁实现线程安全的单例模式。使用Go语言。
在基于CAS实现的锁一文中,提到过通过原子操作实现锁,其原理大致就是通过变量标记锁的状态,线程在获取锁是原子地通过CAS操作修改锁的状态,一旦修改成功,锁获取成功,锁的释放也类似。这就是基于CAS实现锁的核心。
既然锁的实现是基于CAS,而线程安全的单例模式的实现需要锁,于是干脆绕过封锁过程,直接实现CAS实现单例模式。通过一个变量标记某类是否创建了实例,例如0表示没有创建,1表示已经创建。而该变量的修改是基于CAS原子地进行,这样,当多个线程尝试把该变量从0修改为1时,只有一个线程操作成功。于是确保某类只能创建一个实例。
使用代码表达如下,我们以创建日志记录对象为例:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| package main
import ( "fmt" "sync" "sync/atomic" )
type Logger struct { }
var exists int32 = 0 var logger *Logger
func NewLogger() *Logger { if atomic.CompareAndSwapInt32(&exists, 0, 1) { logger = &Logger{} return logger } else { return logger } }
|
简单验证它是否线程安全
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27
| func test() {
var wg sync.WaitGroup
wg.Add(1000)
channel := make(chan *Logger, 1000) for i := 0; i < 1000; i++ { go func() { channel <- NewLogger() wg.Done() }()
}
wg.Wait()
for i := 0; i < 1000; i++ { v := <-channel if v != logger { panic("线程不安全")
} }
fmt.Println("所有goroutine创建的实例唯一") }
|
当把exists变量从0改为1后(没有操作把它改回去),再也没有线程创建新的logger,确保全局只有一个logger。如果我们能保证创建过程在程序运行期间只运行一次(无论是单线程还是多线程下)那么,程序的生命周期中只有一个实例对象。使用sync.Once
对象满足这个过程。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| package main
import ( "fmt" "sync" )
type Logger struct { }
var once sync.Once var logger *Logger
func NewLogger() *Logger { once.Do(func() { logger = new(Logger) }) return logger }
func main() { logger1 := NewLogger() logger2 := NewLogger()
fmt.Println(logger1 == logger2) }
|
当然,从源码来看
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| type Once struct { m Mutex done uint32 }
func (o *Once) Do(f func()) { if atomic.LoadUint32(&o.done) == 1 { return } o.m.Lock() defer o.m.Unlock() if o.done == 0 { defer atomic.StoreUint32(&o.done, 1) f() } }
|
这种方法使用了锁。这里通过锁确保任何时候只有单个线程进入临界区,也就是只有一个线程在执行函数f,同时原子性地标记f被执行过。
对比上述两种方法,方法二更简洁。简洁。