Golang 和 Java 都是流行的编程语言,但它们在语言特性、性能和生态系统等方面存在一些不同点。下面是它们的比较:
- 语言特性
Golang 是一门相对新兴的语言,它的设计主要借鉴了 C 语言的风格,使用静态类型和垃圾回收机制,最大特点是支持并发编程。而 Java 曾经非常流行,是一门支持面向对象编程的语言,通过虚拟机实现跨平台开发。Java 也支持并发编程,但在语言层面对并发支持不如 Golang。
- 性能
Golang 与 Java 在性能方面有一定的差距。Golang 的性能优势在于它具有轻量级的协程和高效的垃圾收集机制,而 Java 在垃圾回收方面并不优秀,在高并发场景下可能出现大量 CPU 消耗和 GC 压力。
- 生态系统和支持
Java 的生态系统非常成熟,有丰富的类库和开源框架,如 Spring、Hibernate 等,以及许多开发工具,比如 Eclipse、IntelliJ IDEA 等。而 Golang 的生态系统相对较新,虽然也有一些类库和框架,如 Gin、Echo 等,但相比于 Java 来说还是不如。
总的来说,Golang 和 Java 是两门不同的编程语言,各有各的特点。如果需要高效实现并发编程,可以考虑使用 Golang;如果需要一个成熟的生态系统和丰富的类库支持,可以选择 Java。在实际使用中,可以根据具体场景选择合适的语言和工具,以达到最优的效果。
一、基本介绍
Golang(又称 Go)是一种开源的编程语言,由 Google 公司开发。Golang 语言的设计目标是提高程序员的开发效率和系统性能,融合了 C 和 Java 等语言的优点,包括静态类型、高效编译、垃圾回收、安全性等特点,同时也支持并发编程。
目前,Google, Facebook,Docker,Netflix ,字节跳动等公司使用 golang 进行开发。
二、 Golang 语言的优点
2.1 高效性
Golang 的编译速度非常快,生成的可执行文件也非常小,且支持并发编程,可以极大的提高程序性能。
2.2 并发能力强
Golang 提供了原生的协程 (goroutine) 和通道(channel)管理机制,支持高效的并发编程
2.3 代码简洁
Golang 的语法简洁,清晰易读,可以极大的提高开发效率,减少代码复杂度。
2.4 内存安全
Golang 的垃圾回收机制可以自动回收内存,同时还提供了多种内存安全和数据安全的机制,可以避免很多常见的安全问题。
2.5 组件丰富
Golang 具有非常丰富的标准库和开发根据,可以降低开发的复杂度。
三、调度的实现
Golang 通过协程 (goroutine)和调度器(scheduler)来完成调度的。
Golang 可以很方便的启动一个协程
例如
go func() {
// some logic
}()
而这些协程的运转主要靠调度器实现,这也是 Golang 语言需要注意的关键。
下面对调度器的实现细节进行具体分析
3.1 调度器实现的基本概念
Golang 的调度器是一种 M:N 的调度器,其中 M 代表内核线程的数量,N 代表协程的数量。和普通调度器不同, golang 的调度器会根据应用程序的变化自动调整 M 和 N 的值。
3.2 G-M 模型
Golang 调度器的实现采用了 G-M 模型。G 代表 goroutine 即一个协程,他执行在 M (内核线程)绑定的 P (processer )对象上。调度器有一个全局的运行队列,存储所有准备运行的协程。协程 G 被创建就会被放入运行队列。当协程 G 执行时,他会被调度器从运行队列取出,被 M 对应的 P 执行。直到执行时间超过预定的时间片,或是协程主动调用了 runtime.Gosched 方法后,他会被调度器放回执行队列,换一个协程来运行。
3.3 自适应线程
Golang 的调度器采用了自适应线程机制。当一个 M 无线程可运行时,调度器会创建一个线程绑定在 M 上。当发现一个线程处于空闲状态时,调度器会将对应的 M 和线程都销毁,以便资源得到回收。
3.4 抢占式调度
Golang 的调度器使用了抢占式调度的机制,协程的执行可能会被更高优先级的协程中断。Golang使用内置的 runtime.Goexit 函数或通道来进行阻塞,实现协程的切换。
3.5 堆栈管理
调度器在每个协程的栈上设置了边界,超过时会自动触发扩容操作。协程的栈也会被标记,方便之后的回收。
四、 回收机制
Golang 的回收机制主要基于标记-清除 算法。
4.1 go1.3 —— 标记清除算法
流程:
- 标记:GC 从根对象开始,遍历所有对象堆,将可达的对象标为存活的对象。Golang 设置有指针结构,在内存中维护了指向堆对象的指针,遍历效率较高。
- 清除:GC 扫描整个堆,将未被标记的对象标为垃圾。这些垃圾对象会被释放。
缺点:
会导致 STW(程序暂停)。即垃圾回收时,程序会出现卡顿,严重影响性能。
4.2 go1.5 —— 三色标记法
设定了 白色,灰色,黑色标记表
流程:
- 新创建的对象标记为白色
- 从根节点出发,将遍历到的对象从白色集合移到灰色集合
- 遍历灰色集合,将灰色对象应用的白色对象放入灰色集合。将原灰色集合的对象放到黑色集合,
- 重复,知道灰色表中无对象。
- 回收所有剩下的白色对象。
比标记清除算法的优化
标记流程不需要 STW
存在的问题
当白色对象被黑色对象引用,灰色对象对白色对象的引用消失时。白色对象会被误回收。
解决方案
强三色不变式:不允许黑色对象引用白色对象
实现:插入写屏障(A引用 B 时,将 B 标为灰色)
缺点:限制的了堆对象,限制不了栈对象。因为栈数量较多,每个协程都有,开屏障会让开销过大,所以不能插入
弱三色不变式:只有当该白色对象同时是灰色对象的引用下游时,它才能被黑色对象引用。
实现:删除写屏障(被删除的对象,假如自身为白色,会被标记为灰色)
缺点:回收精度下降,这个对象不存在引用后,还能存活一轮 GC,直到下一轮才会被回收。影响内存。
4.3 go1.8 —— 三色标记 + 混合写屏障
混合写屏障
GC 开始时会将栈上的可达对象标记为黑色。
GC 期间,栈上创建的对象和引用白色对象都会被标为黑色。
堆上被删除或添加的白色对象会被标为灰色
4.4 Golang 的 GC 触发
- 定量触发:默认配置是堆内存到上次 GC 的内存两倍时触发。默认值是 100%。即增长 100 %的堆内存才会触发 GC。
- 定时触发:由 runtime.forcegcperiod 变量控制。默认两分钟。
- 手动触发:调用 runtime.GC 函数触发
- 空间不足时触发:内存无空闲空间时,创建 32KB 以下可能触发,以上必然触发。
五、 Channel 的实现
Channel 本质上是一个带缓冲的队列,并有首尾指针。读写时,首尾指针的位置会被更改。
缓冲区慢时,写入会被阻塞,缓冲区空时,读取会被阻塞
写入只能串行进行。
goroutine 调用时,当有数据或缓冲区可用时。调度器会按照规则调用 goroutine。
数据完整性保证:数据块是原子性传输。
六、 Golang 性能优化
6.1 减少内存分配,避免垃圾回收
使用内存池,用标准库提供的 sync.pool 类型,来创建一个内存池进行内存复用。
避免拼接字符串,这个会导致内存额外分配。使用 bytes.Buffer 来优化
使用引用类型。传递时就只需要传递指针,减少内存分配。如 slice,map,channel
6.2 利用并发优势
Golang 天生支持协程和通道,可以很方便的并发变成,将需要并发的任务拆分成多个协程处理,来提高效率
6.3 函数调用优化
采用行内函数,减少不必要的类型转换,避免多次分配内存的方式优化性能。
6.4 优化数据结构
通过使用合适的数据结构,来减少内存分配,遍历次数,来提高程序性能。
七、 一些题目
7.1 如何使用两个 goroutine 交替打出 1,2
import (
"fmt"
"sync"
)
func main() {
letter, number := make(chan bool), make(chan bool)
wait := sync.WaitGroup{}
go func() {
i := 1
for {
select {
case <-number:
fmt.Println(i)
letter <- true
break
default:
break
}
}
}()
wait.Add(1)
go func(wait *sync.WaitGroup) {
i := 2
for {
select {
case <-letter:
fmt.Println(i)
number <- true
break
default:
break
}
}
}(&wait)
number <- true
wait.Wait()
}
7.2 make 和 new 的区别
make 用于创建数组,切片和 map ,返回的是类型的引用。且会对创建的数据类型进行初始化,包括长度和容量。
new 用于创建结构体对象,并分配内存,返回的是分配类型的指针。不会初始化内存。
func main() {
// 使用 make 分配内存
s := make([]int, 3, 5)
fmt.Println(s) // [0 0 0]
// 使用 new 分配内存
p := new(int)
fmt.Println(*p) // 0
}
7.3 单个 package 中, init 函数与常量,全局变量的执行顺序
常量 -》 全局变量 -》 init 函数
7.4 slice 的扩容逻辑
小于 1024 时,翻倍扩容
大于 1024 时,每次扩 四分之一,直到符合需要。
扩容时,先分配足够大的内存,再复制数据过去。