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

这篇具有很好参考价值的文章主要介绍了【从零单排Golang】第十五话:用sync.Once实现懒加载的用法和坑点。希望对大家有所帮助。如果存在错误或未考虑完全的地方,请大家不吝赐教,您也可以点击"举报违法"按钮提交疑问。

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

// 参考: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个线程读取并打印,就可以这样子写:

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就能够实现:

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判断。比如下面一段代码是有问题的:

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判断,就能满足懒加载单例/配置之类的逻辑。文章来源地址https://www.toymoban.com/news/detail-693646.html

到了这里,关于【从零单排Golang】第十五话:用sync.Once实现懒加载的用法和坑点的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处: 如若内容造成侵权/违法违规/事实不符,请点击违法举报进行投诉反馈,一经查实,立即删除!

领支付宝红包 赞助服务器费用

相关文章

  • 【Android从零单排系列四十八】《Android中自定义activity的实现方法》

    目录 前言 一  activity介绍 二  activity的缺点 三  自定义activity的步骤 四 自定义activity的demo 小伙伴们,在前面的文章中,我们谈到了Android开发中的自定义view的基本概念及方法等,本文我们实际举例自定义一个activity。 Activity(活动)是Android应用程序中的核心组件之一,它代

    2024年02月15日
    浏览(37)
  • Golang实践:用Sync.Map实现简易内存缓存系统

    定义了一个Cache结构体,其中使用sync.Map作为底层数据结构来存储缓存项。Set方法用于设置缓存项,指定键、值以及过期时间。Get方法用于获取缓存项,如果缓存项存在且未过期,则返回值和true,否则返回nil和false。方法的接受者为指针类型,是为了对Cache对象进行操作,并在

    2024年04月15日
    浏览(42)
  • Go语言入门记录:从基础到变量、函数、控制语句、包引用、interface、panic、go协程、Channel、sync下的waitGroup和Once等

    程序入口文件的包名必须是main,但主程序文件所在文件夹名称不必须是 main ,即我们下图 hello_world.go 在 main 中,所以感觉 package main 写顺理成章,但是如果我们把 main 目录名称改成随便的名字如 filename 也是可以运行的,所以迷思就在于写在文件开头的那个 package main 和 java

    2024年02月11日
    浏览(35)
  • 【Android从零单排系列十七】《Android视图控件——WebView》

    目录 前言 一 WebView基本介绍 二 WebView使用方法 三 WebView常见属性及方法 四 简单案例 五 总结 小伙伴们,在上文中我们介绍了Android视图组件ProgressDialog,本文我们继续盘点,介绍一下视图控件的WebView。 WebView是Android平台上的一个控件,用于在应用程序中显示Web页面 在布局文

    2024年02月11日
    浏览(60)
  • 【Android从零单排系列十八】《Android视图控件——VideoView》

    目录 前言 一 VideoView基本介绍 二 VideoView使用方法 三 VideoView常见属性及方法 四 VideoView简单Demo 五 总结 小伙伴们,在上文中我们介绍了Android视图组件WebView,本文我们继续盘点,介绍一下视图控件的VideoView。 videoView是Android平台上用于播放视频的控件,它提供了一些常见属性

    2024年02月09日
    浏览(41)
  • 【Android从零单排系列十四】《Android视图控件——RatingBar》

    目录 前言 一 RatingBar基本介绍 二 RatingBar使用方法 三 RatingBar常用属性方法 四 总结 小伙伴们,在上文中我们介绍了Android视图组件ProgressBar,本文我们继续盘点,介绍一下视图控件的RatingBar。 RatingBar是Android平台上的一个UI组件,用于让用户通过评级操作选择分数或等级。 R

    2024年02月12日
    浏览(37)
  • 【Android从零单排系列三十三】《Android布局介绍——FrameLayout》

    目录 前言 一 FrameLayout基本介绍 二 FrameLayout使用方法 三 FrameLayout常见属性及方法 四 FrameLayout简单案例 五 总结 小伙伴们,在上文中我们介绍了Android布局AbsoluteLayout,本文我们继续盘点介绍Android开发中另一个常见的布局,帧布局FrameLayout。 FrameLayout是Android中的一种布局容器,

    2024年02月12日
    浏览(39)
  • 【Android从零单排系列二十六】《Android视图控件——ScrollView》

    目录 前言 一 ScrollView基本介绍 二 ScrollView使用方法 三 ScrollView常见属性及方法 四 ScrollView简单案例 五 总结 小伙伴们,在上文中我们介绍了Android视图组件RecyclerView,本文我们继续盘点,介绍一下视图控件的ScrollView。 ScrollView是Android平台上的一个可滚动视图容器,它用于在一

    2024年02月12日
    浏览(41)
  • 【Android从零单排系列二十二】《Android视图控件——GridView》

    目录 前言 一 GridView基本介绍 二 GridView使用方法 三 GridView常见属性及方法 四 总结 小伙伴们,在上文中我们介绍了Android视图组件ExpandableListView,本文我们继续盘点,介绍一下视图控件的GridView。 GridView是一个在Android中常用的布局控件,它可以以网格形式展示数据,类似于表

    2024年02月10日
    浏览(39)
  • 【Android从零单排系列四十四】《聊一下Android数据权限permission》

    目录 前言 一.Android 数据权限基本介绍 二 Android 权限分类 三 Android 权限清单 四 Android 动态申请权限DEMO 小伙伴们,在前面的几篇文章中,我们谈到了Android开发中的几种数据存储方式及数据持久化,本文我们介绍下Android开发中的另一部分内容,权限管理。 在Android中,权限管

    2024年02月12日
    浏览(71)

觉得文章有用就打赏一下文章作者

支付宝扫一扫打赏

博客赞助

微信扫一扫打赏

请作者喝杯咖啡吧~博客赞助

支付宝扫一扫领取红包,优惠每天领

二维码1

领取红包

二维码2

领红包