之前写到基于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 {
// something about logging
}

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 {
// something about logging
}

var once sync.Once
var logger *Logger

func NewLogger() *Logger {
once.Do(func() {
logger = new(Logger)
// init logger here
})
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
}
// Slow-path.
o.m.Lock()
defer o.m.Unlock()
if o.done == 0 {
defer atomic.StoreUint32(&o.done, 1)
f()
}
}

这种方法使用了锁。这里通过锁确保任何时候只有单个线程进入临界区,也就是只有一个线程在执行函数f,同时原子性地标记f被执行过。

对比上述两种方法,方法二更简洁。简洁。