美文网首页
【Go - 单例示例】

【Go - 单例示例】

作者: wn777 | 来源:发表于2024-07-24 01:21 被阅读0次

    需求

    实现一个单例,需要考虑如下的内容,

    • 最基础的,每次返回相同实例,
    • 线程安全,
    • 生命周期,资源管理,程序退出后释放资源,

    参照以上3点需求,以下是一个完整的示例,

    代码示例

    package main
    
    import (
        "context"
        "fmt"
        "sync"
    
        "go.mongodb.org/mongo-driver/mongo"
        "go.mongodb.org/mongo-driver/mongo/options"
    )
    
    var (
        singletonMongoClient *mongo.Client
        once                 sync.Once
    )
    
    func getSingletonMongoClient() *mongo.Client {
    
        once.Do(func() {
            // 创建连接到 MongoDB 的客户端
            uri := "mongodb://localhost:27017"
            client, err := mongo.Connect(context.TODO(), options.Client().
                ApplyURI(uri))
    
            if err != nil {
                panic(err)
            }
            singletonMongoClient = client
        })
        return singletonMongoClient
    }
    
    func main() {
        client1 := getSingletonMongoClient()
        if client1 == nil {
            panic("client1 is nil")
        }
    
        fmt.Println("client1 address %p", client1)
    
        defer func() {
            if err := client1.Disconnect(context.TODO()); err != nil {
                panic(err)
            }
        }()
    
        client2 := getSingletonMongoClient()
        fmt.Println("client2 address %p", client2)
    
        if client1 == client2 {
            fmt.Println("client1 and client2 are the same")
        }
    }
    
    

    📢注意: 在使用多线程或多协程的情况下,这里可以使用sync.Once 保证在多线程情况下,也只执行一次,这个非常方便。

    sync.Once的优势

    对比下Java中实现单例 ,熟悉Java的朋友 ,可能了解 Java 领域一个经典的案例,利用双重检查创建单例对象。

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

    这个示例初看一点问题没有,但是实际隐藏一个坑,问题就出在了new操作上,我们以为的new操作是这样的:

    1.分配一块内存空间
    2.在这块内存空间上初始化Singleton实例对象
    3.把这个对象的内存地址赋值给instance变量
    

    但实际上由于指令重排,优化后的过程是这样的:

    1.分配一块内存空间
    2.把这快内存空间的内存地址赋值给instance变量
    3.在这块内存空间上初始化Singleton实例对象
    

    那么这样调换顺序后会发生什么呢?

    我们假设线程 A 先执行 getInstance() 方法,当执行完指令 2 时恰好发生了线程切换,切换到了线程 B 上;如果此时线程 B 也执行 getInstance() 方法,那么线程 B 在执行第一个判断时会发现 instance != null ,所以直接返回 instance,而此时的 instance 是没有初始化过的,如果我们这个时候访问 instance 的成员变量就可能触发空指针异常。

    解决方案

    针对指令重排序问题:在Java中,由于编译器优化,指令重排序的可能性,可能会导致instance变量在构造函数执行完毕之前就被设置为非null值,使得另一个线程可能会得到一个尚未完全构造好的实例。为了避免这个问题,instance变量应该被声明为volatile,以防止指令重排序。

    优化代码

    public class Singleton {
      private static volatile Singleton instance; // 添加volatile关键字防止指令重排序
    
      private Singleton() {} // 私有构造函数防止外部实例化
    
      static Singleton getInstance() {
        if (instance == null) {
          synchronized(Singleton.class) {
            if (instance == null)
              instance = new Singleton();
          }
        }
        return instance;
      }
    }
    

    可以看出,Java中单例代码不多,细节不少,稍不注意可能踩一坑。

    对比Go中sync.Once的封装,为我们避免了多线程下的种种问题,为单例的实现提供了便利的帮助。

    相关文章

      网友评论

          本文标题:【Go - 单例示例】

          本文链接:https://www.haomeiwen.com/subject/kspuhjtx.html