栏目分类:
子分类:
返回
名师互学网用户登录
快速导航关闭
当前搜索
当前分类
子分类
实用工具
热门搜索
名师互学网 > IT > 软件开发 > 后端开发 > Java

sync.Once——golang单例模式

Java 更新时间: 发布时间: IT归档 最新发布 模块sitemap 名妆网 法律咨询 聚返吧 英语巴士网 伯小乐 网商动力

sync.Once——golang单例模式

之前写的一个java的单例模式

在golang中我们期望一个方法只执行一次的时候,通常会使用实例化一个sync.Once结构体,然后使用once.Do方法来包装我们需要执行的方法f。在初始化各种连接的时候非常常用。

代码示例

import (
	"sync"
)

// 单例对象的结构体
type Singleton struct {
	Age  int
	Name string
	Addr string
}

var once sync.Once

// 全局变量instance
var instance *Singleton

// 执行方法
func GetInstance() *Singleton {
	if instance == nil {
		once.Do(func() {
			instance = &Singleton{
				Age:  20,
				Name: "张三",
				Addr: "余杭区",
			}
		})
	}
	return instance
}

上述代码的 if instance == nil其实是非必要的,只是让逻辑看起来更加清晰。

源码剖析
once的内部实现非常简单,无异于我们自己实现的单例模式,先来看Once结构体的样子

// Once is an object that will perform exactly one action.
type Once struct {
	// done indicates whether the action has been performed.
	// It is first in the struct because it is used in the hot path.
	// The hot path is inlined at every call site.
	// Placing done first allows more compact instructions on some architectures (amd64/x86),
	// and fewer instructions (to calculate offset) on other architectures.
	done uint32
	m    Mutex
}

结构体中包含一个标识位done和一个锁m。m没啥说的就是锁,done用来判断方法是否已经执行过,初始状态为未执行。注意这里注释提到了一个hot path,大意为使用频次非常高的指令序列。在获取结构体中的第一个字段的时候,可以直接取消引用结构体指针。而要获取后面的字段则需要加上偏移量。所以把done放在第一个字段可以提高性能。

再来看Do方法的实现

func (o *Once) Do(f func()) {
	// Note: Here is an incorrect implementation of Do:
	//
	//	if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
	//		f()
	//	}
	//
	// Do guarantees that when it returns, f has finished.
	// This implementation would not implement that guarantee:
	// given two simultaneous calls, the winner of the cas would
	// call f, and the second would return immediately, without
	// waiting for the first's call to f to complete.
	// This is why the slow path falls back to a mutex, and why
	// the atomic.StoreUint32 must be delayed until after f returns.

	if atomic.LoadUint32(&o.done) == 0 {
		// Outlined slow-path to allow inlining of the fast-path.
		o.doSlow(f)
	}
}
func (o *Once) doSlow(f func()) {
	o.m.Lock()
	defer o.m.Unlock()
	if o.done == 0 {
		defer atomic.StoreUint32(&o.done, 1)
		f()
	}
}

在Do方法中先进行了atomic.LoadUint32(&o.done) == 0的判断,如果标识位已经改变了则意味着f已经执行过了。doSlow中使用锁保证只能有一个线程执行这个方法,在锁的临界区内又进行了一次o.done==0的判断,就是双重检查 防止等待锁的线程获得锁之后又执行了一次f(因为是在锁的临界区内,所以不需要使用atomic,直接判断即可)。

注释里提到了一个常见的错误实现

	if atomic.CompareAndSwapUint32(&o.done, 0, 1) {
		f()
	}

这种方式不能够保证标志位改变时方法f一定执行完毕。这可能会导致外层方法判断f方法已经执行完毕但实际上并没有,造成各种未知错误(如空指针)。

注意点,要执行的方法f中不能够再次调用do,这样会导致死锁。doSlow中获取了锁,doSlow又调用了f,f又去调用doSlow获取锁。

PS:golang中没有实现可重入锁,设计者认为锁的使用应该是清晰有层次的,如果出现需要可重入锁的情况,那么你应该要修改你的代码了。

PPS:如果方法f在执行过程中发生了panic,也会视为执行完成,后续不会再执行f,这一点需要特殊注意。

转载请注明:文章转载自 www.mshxw.com
本文地址:https://www.mshxw.com/it/821756.html
我们一直用心在做
关于我们 文章归档 网站地图 联系我们

版权所有 (c)2021-2022 MSHXW.COM

ICP备案号:晋ICP备2021003244-6号