sync.Once和自己加锁有什么区别吗?

单例

func loadIcons() {
    icons = map[string]string{
        "left":  loadIcon("left.png"),
        "up":    loadIcon("up.png"),
        "right": loadIcon("right.png"),
        "down":  loadIcon("down.png"),
}

// Icon 被多个goroutine调用时不是并发安全的
//每个goroutine都满足串行一致的基础上自由地重排访问内存的顺序
func Icon(name string) string {
    if icons == nil {
      loadIcons()
    }
    return icons[name]
}

这个和JAVA中的单例写法基本一致。但是当初写JAVA单例时,会加上关键字 synchronize 。但是加上这个还无法保证单例一定就会只执行一次并且按预想的执行。因为计算机执行指令时满足串行一致的基础上自由地重排访问内存的顺序。解释就是:指令的执行可能在没有上下文依赖的情况下,可以先执行其他的指令。这就可能导致多协程下判断 icons == nil 时 icons 还没全部初始化完成,却走到 icons[name],此时无法返回预期结果。于是JAVA的做法就是防止指令重排顺序,JVM利用的是计算机内部的读写栅栏隔离,其实类似加锁。关键字是:[volatile](https://blog.csdn.net/xiaolyuh123/article/details/103289570 "volatile")

同理,Go的单例优化后也可以加锁:

var lock sync.Mutex
func Icon(name string) string {
    lock.Lock() //加锁
    if icons == nil {
      loadIcons()
    }
    lock.Unlock()
    return icons[name]
}

但是Go在底层已经实现了只执行一次的方法,如下:

var loadIconsOnce sync.Once
func Icon(name string) string {
    loadIconsOnce.Do(loadIcons)
    return icons[name]
}

思考:sync.Once 执行一次和自己加锁的有什么区别吗?

欢迎你的回答,等你的回答!!!


最近借着大家的评论又看了下Go的源码,之前没有注意到Go的原子操作,于是去看了下。现在再回来看就很清楚了。

本作品采用《CC 协议》,转载必须注明作者和本文链接
讨论数量: 5

加锁和单例是两个不同的东西。加锁是为了防止并发出现问题,单例是只new一次,go实现的单例也有用了锁,但是这里你问的是两个不同的东西

2年前 评论
so_easy (楼主) 2年前
deatil (作者) 2年前

file

不是还有个原子判断呢吗

2年前 评论
xing393939 2年前

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!