优化一个已有的 Go 程序-对象池| 青训营

583 阅读6分钟

前言

在实际应用场景中,无论是什么语言,优化一直是一个很重要的点,也是笔试面试经常会考到的点。因此,以夏令营为契机,通过对象池的方式,来对Go进行一个优化,并且和Java做一个简单的对比

Go

Sync.Pool

sync.Pool 是 Go 语言标准库中的一个类型,用于提供临时对象的池化管理。它主要用于临时对象的分配和重用,以降低内存分配和垃圾回收的压力,从而提高程序性能

sync.Pool 的主要作用如下:

对象的池化sync.Pool 允许程序员将一些对象放入池中,这些对象在需要的时候可以被重用,而不是频繁地创建和销毁。这 对于那些创建和销毁开销较大的对象(如大型结构体或连接对象)来说,可以减少资源的浪费

临时对象的分配和回收sync.Pool 可以用于缓存那些在一段时间内会被频繁创建和销毁的临时对象。这可以减少内存分配和垃圾回收的开销,因为对象可以被多次重用,而不是每次都重新分配

sync.Pool 主要提供了以下两个方法:

  • func (p *Pool) Get() interface{}:从池中获取一个对象,如果池为空,则会返回 nil
  • func (p *Pool) Put(x interface{}):将一个对象放回池中,以便后续重用。

在使用sync.Pool 时需要注意以下几点:

  • 池中的对象并不是无限的,它们会随着时间被自动垃圾回收。
  • 对象的生命周期不受程序控制,因此不要依赖于对象的长期存在。
  • sync.Pool 在多个 Goroutine 之间共享,因此要确保存储在池中的对象是并发安全的,或者在取出对象后,对其进行适当的同步

总体来说,sync.Pool 是 Go 语言中用于对象池化管理的工具,对于需要频繁创建和销毁临时对象的场景,它可以提高程序性能,减少内存分配和垃圾回收的开销

下面,我将通过具体的例子,进行实际编程的说明

Sync.Pool 使用示例

package BDtest1  
  
import (  
  "fmt"  
  "sync"  
)  
  
var pool *sync.Pool  
func init() {  
  pool = &sync.Pool{  
    New: func() interface{} {  
      fmt.Println("创造一个对象")
      return "hello bb"  
    },  
  }  
}
func main() {  
  obj := pool.Get().(string)  
  fmt.Print(obj)  
  pool.Put(obj)  
  
  obj = pool.Get().(string)  
  fmt.Println(obj)  
}

在这段代码中,我们首先定义了一个指向 sync.Pool 类型的变量 pool,这将用于存储对象池的实例

之后,定义一个init() 函数,作为Go语言中的特殊函数,用于初始化程序。在这里,它被用来初始化对象池。

其中,pool = &sync.Pool{} 创建了一个名为 pool 的全局变量,它是一个指向 sync.Pool 类型的指针。在对象池的初始化中,New 字段被设置为一个匿名函数。这个匿名函数是sync.Pool对象在需要时用来创建新对象的函数。最后,return "hello bb" 返回一个字符串 "hello bb" 作为示例对象,这个对象将被添加到对象池中。

这段代码的效果是,初始化了一个 sync.Pool 对象池,当你第一次从对象池中获取对象时,这个匿名函数会被调用来创建一个新对象。在这个示例中,它创建的对象是一个字符串 "hello bb"。这个机制允许你在需要的时候动态地创建对象,以便在程序中高效地重用它们,从而提高性能

接着,在主函数中,使用对象,运行的结果如下:

image.png

可以看到,第一次获取对象时,New函数被调用,创建了一个新的对象。然后,我们将对象归还到池中,并再次获取对象,这时应该从池中获取,而不是创建新的对象。

同时,由于Sync.Pool是并发安全的,所以多个goroutine可以同时访问同一个Sync.Pool对象,从而共享池中的对象

具体使用

创建 Sync.Pool 对象时,我们需要提供一个 New 函数作为初始化函数,该函数用于创建一个新的对象。简单的定义方式如下:

func NewObject() interface{} {      
  return &Object{}  
}

NewObject 函数用于创建一个新的 Object 对象,并返回该对象。接下来就可以具体地创建对象:

pool := sync.Pool{     
  New: NewObject,  
}

而对于获取和放回对象,只需要简单的GetPut就可以实现

实现原理

我们之前已经知道了Sync.Pool其实就是对象池,现在来具体讲一下其实现方式

在 Sync.Pool 中,对象池是使用 sync.Pool结构体来实现的,其中有两个字段,分别是new和pool。new 字段是一个函数类型,用于创建一个新的对象。pool 字段是 sync.Pool 结构体的实际存储对象池的地方。

感觉上来说,new是标号,但是其实按照函数类型来进行分类的,然后所有的对象都存储在pool的池子里。但其实不然,sync.Pool 内部会维护一个通用的对象池,而不管 new 字段创建的对象的具体类型是什么。这意味着我们可以在一个 sync.Pool 中存储和重用不同类型的对象,只要它们的 new 函数返回的都是 interface{} 类型的值

编程验证

我们编写好使用对象池和不使用对象池的代码,执行两遍后,结果如下:

image.png

不难看出,添加了对象池后,运行时间有所改善

Java中对象池的实现

其实总结来看,对象池更像是一个设计模式,主要功能是缓存一组已经初始化的对象,以供随时可以使用。对象池大多数场景下都是缓存着创建成本过高或者需要重复创建使用的对象,从池子中取对象的时间是可以预测的,但是新建一个对象的时间是不确定的

在Java中,我们可以创建一个列表或队列来存储对象,以及编写逻辑来获取和释放对象,同时利用synchronized关键字或java.util.concurrent包中的工具来确保线程安全

下面是一个简单的对象池的手写模板:

public class ObjectPool<T> {  
  private List<T> pool;  
  public ObjectPool(int size, Supplier<T> objectFactory){  
    pool = new ArrayList<>();  
    for(int i=0;i<size;++i){  
      pool.add(objectFactory.get());  
    }  
  }  
  public T getObject(){  
  }  
  public void releaseObject(T obj){  
  }  
}

其中,Supplier<T> 是Java函数式编程接口,它代表一个供应商,可以用于产生(或供应)某种类型的值 T。在对象池的上下文中,Supplier<T> 用于提供对象的工厂函数,也就是用于创建新对象的函数

当然,自从Java8之后,Java的并发包提供了一些用于对象池管理的类,例如java.util.concurrent.ArrayBlockingQueue,使用如下:

BlockingQueue<MyObject> objectPool = new ArrayBlockingQueue<>(10); 
objectPool.add(new MyObject()); 
MyObject obj = objectPool.take();   
objectPool.put(obj);

这样和Go的使用方式也就很类似了

总结

Go中的对象池方式sync.Pool是Go标准库中的一个工具,用于存储和复用临时对象,以减少垃圾回收的压力。而Java中的对象池技术先前多用的是Apache Commons Pool或自定义实现。但总体设计思想和使用方式的差别并不大