【从零单排Golang】第十五话:用sync.Once实现懒加载的用法和坑点

原文摘自本人CSDN博客:【从零单排Golang】第十五话:用sync.Once实现懒加载的用法和坑点

在使用Golang做后端开发的工程中,我们通常需要声明一些一些配置类或服务单例等在业务逻辑层面较为底层的实例。为了节省内存或是冷启动开销,我们通常采用lazy-load懒加载的方式去初始化这些实例。初始化单例这个行为是一个非常经典的并发处理的案例,比如在java当中,我们可能用到建立双重锁+volatile的方式保证初始化逻辑只被访问一次,并且所有线程最终都可以读取到初始化完成的实例产物。这段经典的代码可以按如下的方式编写:

java 复制代码
// 参考:https://blog.csdn.net/qq_27489007/article/details/84966680

public class Singleton {
    private volatile static Singleton uniqueSingleton;
 
    private Singleton() {
    }
 
    public Singleton getInstance() {
        if (null == uniqueSingleton) {
            synchronized (Singleton.class) {
                if (null == uniqueSingleton) {
                    uniqueSingleton = new Singleton();
                }
            }
        }
        return uniqueSingleton;
    }
}

但在Golang里面,实现懒加载的方式可以简单的多,用内置的sync.Once就能满足。假设我们有一个user单例,需要被1000个线程读取并打印,就可以这样子写:

go 复制代码
type User struct {
    Name string `json:"name"`
    Age  int    `json:"age"`
}

var user *User
var userOnce sync.Once

func initUser() {
    user = &User{}
    cfgStr := `{"name":"foobar","age":18}`
    if err := json.Unmarshal([]byte(cfgStr), user); err != nil {
        panic("load user err: " + err.Error())
    }
}

func getUser() *User {
    userOnce.Do(initUser)
    return user
}

func TestSyncOnce(t *testing.T) {
    var wg sync.WaitGroup
    for i := 1; i < 1000; i++ {
        wg.Add(1)
        go func(n int) {
            defer wg.Done()
            curUser := getUser()
            t.Logf("[%d] got user: %+v", n, curUser)
        }(i)
    }
    wg.Wait()
}

这段代码里,首先是通过var userOnce sync.Once声明了一个sync.Once实例,然后在getUser当中,我们声明了userOnce.Do(initUser)这个操作。假设一个goroutine最先到达这个操作,就会上锁并执行initUser,其它goroutine到达之后,得等第一个goroutine执行完initUser之后,才会继续return user。这样,就能一来保证initUser只会执行一次,二来所有goroutine都能够最终读到初始化完成的user单例。

sync.Once的工作机理也很简单,通过一个锁和一个flag就能够实现:

go 复制代码
func (o *Once) Do(f func()) {
	if atomic.LoadUint32(&o.done) == 0 { // 如果是1表示已经完成了,跳过
		o.doSlow(f)
	}
}

func (o *Once) doSlow(f func()) {
	o.m.Lock() // 只有1个goroutine能拿到锁,其它的等待
	defer o.m.Unlock()
	if o.done == 0 { // 如果还是0表示第一个来的,不是0就表示已经有goroutine做完了
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}

最后也需要注意,sync.Once使用上面有一个坑点,不能也不需要像java一样为单例提前做nil判断。比如下面一段代码是有问题的:

go 复制代码
func initUser() {
    user = &User{} // 先给一个zero-value实例
    cfgStr := `{"name":"foobar","age":18}` // 然后加载json内容,完成初始化
    if err := json.Unmarshal([]byte(cfgStr), user); err != nil {
        panic("load user err: " + err.Error())
    }
}

func getUser() *User {
    if user == nil {
        userOnce.Do(initUser)
    }
    return user
}

由于Golang没有volatile关键字,不能控制单例在内存的可见性,那么多goroutine并发时,就有可能出现这样的执行时序:

  • goroutine-A过了getUseruser == nil判断,进入到了initUser逻辑,走到了cfgStr := XXX一行
  • 此时切换到goroutine-B,因为goroutine-AinitUser已经走过了user = &User{}一行,所以跳过了user == nil判断,直接返回没有完全初始化的user实例,然后一直往下运行,就没切回给goroutine-A

这样的结果,就导致有goroutine拿到未初始化完成的实例往后运行,后面就出问题了。所以实战当中需要留意,用sync.Once时,不能也不需要加这些nil判断,就能满足懒加载单例/配置之类的逻辑。

相关推荐
风月歌10 分钟前
小程序项目之校园二手交易平台小程序源代码(源码+文档)
java·数据库·mysql·小程序·毕业设计·源码
西京刀客11 分钟前
go语言-切片排序之sort.Slice 和 sort.SliceStable 的区别(数据库分页、内存分页场景注意点)
后端·golang·sort·数据库分页·内存分页
少许极端16 分钟前
算法奇妙屋(二十)-回文子串/子序列问题(动态规划)
java·算法·动态规划·图解·回文串·回文序列
有味道的男人17 分钟前
1688数据采集:官方API与网页爬虫实战指南
java·服务器·爬虫
计算机毕设VX:Fegn089521 分钟前
计算机毕业设计|基于springboot + vue汽车销售系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·汽车·课程设计
仅此,22 分钟前
前端接收了id字段,发送给后端就变了
java·前端·javascript·spring·typescript
@淡 定23 分钟前
AQS在锁实现中的应用详解
java
hashiqimiya25 分钟前
androidstudio模拟器安装apk文件
java
IT枫斗者37 分钟前
Java 开发实战:从分层架构到性能优化(Spring Boot + MyBatis-Plus + Redis + JWT)
java·spring boot·sql·mysql·性能优化·架构
聆风吟º38 分钟前
【Spring Boot 报错已解决】Spring Boot项目启动报错 “Main method not found“ 的全面分析与解决方案
android·spring boot·后端