Go 中的 channel 与 Java BlockingQueue 的本质区别 (2)

换做是 go 的实现:

func main() { notify := make(chan struct{}) for i := 0; i < 10; i++ { go func(i int) { for { select { case <-notify: fmt.Println("done.......",i) return case <-time.After(1 * time.Second): fmt.Println("wait notify",i) } } }(i) } time.Sleep(1 * time.Second) close(notify) time.Sleep(3 * time.Second) }

当关闭一个 channel 后,会使得所有获取 channel 的 goroutine 直接返回,不会阻塞,正是利用这一特性实现了广播通知所有 goroutine 的目的。

注意,同一个 channel 不能反复关闭,不然会出现panic。

channel 解耦

以上例子都是基于无缓冲的 channel,通常用于 goroutine 之间的同步;同时 channel 也具备缓冲的特性:

ch :=make(chan T, 100)

可以直接将其理解为队列,正是因为具有缓冲能力,所以我们可以将业务之间进行解耦,生产方只管往 channel 中丢数据,消费者只管将数据取出后做自己的业务。

同时也具有阻塞队列的特性:

当 channel 写满时生产者将会被阻塞。

当 channel 为空时消费者也会阻塞。

从上文的例子中可以看出,实现相同的功能 go 的写法会更加简单直接,相对的 Java 就会复杂许多(当然这也和这里使用的偏底层 api 有关)。

Java 中的 BlockingQueue

这些特性都与 Java 中的 BlockingQueue 非常类似,他们具有以下的相同点:

可以通过两者来进行 goroutine/thread 通信。

具备队列的特征,可以解耦业务。

支持并发安全。

同样的他们又有很大的区别,从表现上看:

channel 支持 select 语法,对 channel 的管理更加简洁直观。

channel 支持关闭,不能向已关闭的 channel 发送消息。

channel 支持定义方向,在编译器的帮助下可以在语义上对行为的描述更加准确。

当然还有本质上的区别就是 channel 是 go 推荐的 CSP 模型的核心,具有编译器的支持,可以有很轻量的成本实现并发通信。

而 BlockingQueue 对于 Java 来说只是一个实现了并发安全的数据结构,即便不使用它也有其他的通信方式;只是他们都具有阻塞队列的特征,所有在初步接触 channel 时容易产生混淆。

相同点 channel 特有
阻塞策略   支持select  
设置大小   支持关闭  
并发安全   自定义方向  
普通数据结构   编译器支持  
总结

有过一门编程语言的使用经历在学习其他语言是确实是要方便许多,比如之前写过 Java 再看 Go 时就会发现许多类似之处,只是实现不同。

拿这里的并发通信来说,本质上是因为并发模型上的不同;

Go 更推荐使用通信来共享内存,而 Java 大部分场景都是使用共享内存来通信(这样就得加锁来同步)。

带着疑问来学习确实会事半功倍。

最近和网友讨论后再补充一下,其实 Go channel 的底层实现也是通过对共享内存的加锁来实现的,这点任何语言都不可避免。

既然都是共享内存那和我们自己使用共享内存有什么区别呢?主要还是 channel 的抽象层级更高,我们使用这类高抽象层级的方式编写代码会更易理解和维护。

但在一些特殊场景,需要追求极致的性能,降低加锁颗粒度时用共享内存会更加合适,所以 Go 官方也提供有 sync.Map/Mutex 这样的库;只是在并发场景下更推荐使用 channel 来解决问题。

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/zyjzgj.html