前言

最近在实现两个需求,由于两者之间并没有依赖关系,所以想利用队列进行解耦;但在 Go 的标准库中并没有现成可用并且并发安全的数据结构;但 Go 提供了一个更加优雅的解决方案,那就是 channel

channel 应用

GoJava 的一个很大的区别就是并发模型不同,Go 采用的是 CSP(Communicating sequential processes) 模型;用 Go 官方的说法:

Do not communicate by sharing memory; instead, share memory by communicating.

翻译过来就是:不用使用共享内存来通信,而是用通信来共享内存。

而这里所提到的通信,在 Go 里就是指代的 channel

只讲概念并不能快速的理解与应用,所以接下来会结合几个实际案例更方便理解。

futrue task

Go 官方没有提供类似于 JavaFutureTask 支持:

    public static void main(String[] args) throws InterruptedException, ExecutionException {
ExecutorService executorService = Executors.newFixedThreadPool(2);
Task task = new Task();
FutureTask<String> futureTask = new FutureTask<>(task);
executorService.submit(futureTask);
String s = futureTask.get();
System.out.println(s);
executorService.shutdown();
}
} class Task implements Callable<String> {
@Override
public String call() throws Exception {
// 模拟http
System.out.println("http request");
Thread.sleep(1000); return "request success";
}
}

但我们可以使用 channel 配合 goroutine 实现类似的功能:

func main() {
ch := Request("https://github.com")
select {
case r := <-ch:
fmt.Println(r)
}
}
func Request(url string) <-chan string {
ch := make(chan string)
go func() {
// 模拟http请求
time.Sleep(time.Second)
ch <- fmt.Sprintf("url=%s, res=%s", url, "ok")
}()
return ch
}

goroutine 发起请求后直接将这个 channel 返回,调用方会在请求响应之前一直阻塞,直到 goroutine 拿到了响应结果。

goroutine 互相通信

   /**
* 偶数线程
*/
public static class OuNum implements Runnable {
private TwoThreadWaitNotifySimple number; public OuNum(TwoThreadWaitNotifySimple number) {
this.number = number;
} @Override
public void run() {
for (int i = 0; i < 11; i++) {
synchronized (TwoThreadWaitNotifySimple.class) {
if (number.flag) {
if (i % 2 == 0) {
System.out.println(Thread.currentThread().getName() + "+-+偶数" + i); number.flag = false;
TwoThreadWaitNotifySimple.class.notify();
} } else {
try {
TwoThreadWaitNotifySimple.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
} /**
* 奇数线程
*/
public static class JiNum implements Runnable {
private TwoThreadWaitNotifySimple number; public JiNum(TwoThreadWaitNotifySimple number) {
this.number = number;
} @Override
public void run() {
for (int i = 0; i < 11; i++) {
synchronized (TwoThreadWaitNotifySimple.class) {
if (!number.flag) {
if (i % 2 == 1) {
System.out.println(Thread.currentThread().getName() + "+-+奇数" + i); number.flag = true;
TwoThreadWaitNotifySimple.class.notify();
} } else {
try {
TwoThreadWaitNotifySimple.class.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}

这里截取了”两个线程交替打印奇偶数“的部分代码。

Java 提供了 object.wait()/object.notify() 这样的等待通知机制,可以实现两个线程间通信。

go 通过 channel 也能实现相同效果:

func main() {
ch := make(chan struct{})
go func() {
for i := 1; i < 11; i++ {
ch <- struct{}{}
//奇数
if i%2 == 1 {
fmt.Println("奇数:", i)
}
}
}() go func() {
for i := 1; i < 11; i++ {
<-ch
if i%2 == 0 {
fmt.Println("偶数:", i)
}
}
}() time.Sleep(10 * time.Second)
}

本质上他们都是利用了线程(goroutine)阻塞然后唤醒的特性,只是 Java 是通过 wait/notify 机制;

而 go 提供的 channel 也有类似的特性:

  1. channel 发送数据时(ch<-struct{}{})会被阻塞,直到 channel 被消费(<-ch)。

以上针对于无缓冲 channel

channel 本身是由 go 原生保证并发安全的,不用额外的同步措施,可以放心使用。

广播通知

不仅是两个 goroutine 之间通信,同样也能广播通知,类似于如下 Java 代码:

    public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
synchronized (NotifyAll.class){
NotifyAll.class.wait();
}
System.out.println(Thread.currentThread().getName() + "done....");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();
}
Thread.sleep(3000);
synchronized (NotifyAll.class){
NotifyAll.class.notifyAll();
}
}

主线程将所有等待的子线程全部唤醒,这个本质上也是通过 wait/notify 机制实现的,区别只是通知了所有等待的线程。

换做是 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 后,会使得所有获取 channelgoroutine 直接返回,不会阻塞,正是利用这一特性实现了广播通知所有 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 来解决问题。

Go 中的 channel 与 Java BlockingQueue 的本质区别的更多相关文章

  1. JSP中脚本、声明和表达式的本质区别

     JSP脚本元素 使用JSP脚本元素可以将Java代码嵌入到JSP页面里,这些Java代码将出现在由当前JSP页面生成的Servlet中,使JSP将静态内容与动态内容分离出来.脚本元素包含:  1. ...

  2. objc中的类方法和实例方法有什么本质区别和联系

    类方法: 类方法是属于类对象的 类方法只能通过类对象调用 类方法中的self是类对象 类方法可以调用其他的类方法 类方法中不能访问成员变量 类方法中不能直接调用对象方法 类方法是存储在元类对象的方法缓 ...

  3. js中基本数据类型与引用数据类型的本质区别

    代码 /** * 基本数据类型:string, number, boolean, null, undefined. * * 说明: * 基本数据类型的变量是保存在栈内存中的,基本数据类型的值 * 直接 ...

  4. Java NIO中的Channel接口

    1. Channel  通道,可以将指定文件的部分或全部直接映射成Buffer. 不能直接读写Channel中的数据,Channel只能与ByteBuffer交互. 读数据时,把Channel中的数据 ...

  5. netty中的Channel、ChannelPipeline

    一.Channel与ChannelPipeline关系 每一个新创建的 Channel 都将会被分配一个新的 ChannelPipeline.这项关联是永久性 的:Channel 既不能附加另外一个 ...

  6. java中的NIO和IO到底是什么区别?20个问题告诉你答案

    摘要:NIO即New IO,这个库是在JDK1.4中才引入的.NIO和IO有相同的作用和目的,但实现方式不同,NIO主要用到的是块,所以NIO的效率要比IO高很多. 本文分享自华为云社区<jav ...

  7. netty系列之:netty中的Channel详解

    目录 简介 Channel详解 异步IO和ChannelFuture Channel的层级结构 释放资源 事件处理 总结 简介 Channel是连接ByteBuf和Event的桥梁,netty中的Ch ...

  8. Kotlin中变量不同于Java: var 对val(KAD 02)

    原文标题:Variables in Kotlin, differences with Java. var vs val (KAD 02) 作者:Antonio Leiva 时间:Nov 28, 201 ...

  9. Java 7 中的Switch 谈 Java版本更新和反编译知识

    Java 7 中的Switch 谈 Java版本更新和反编译知识          学习编程,享受生活,大家好,我是追寻梦的飞飞.今天主要讲述的是Java7中的更新Switch实现内部原理和JAD反编 ...

随机推荐

  1. LVS跨网段DR模型

    客户端IP地址:172.16.8.147 路由器服务器IP地址:172.16.8.167内网IP地址:192.168.1.3 一.将客户端的网关修改为路由服务器IP地址 vim ifcfg-ens33 ...

  2. 【Web前端HTML5&CSS3】08-盒模型补充

    笔记来源:尚硅谷Web前端HTML5&CSS3初学者零基础入门全套完整版 目录 盒模型补充及田径场实战 1. 盒子大小 2. 轮廓 3. 阴影 4. 圆角 圆 椭圆 盒模型补充及田径场实战 1 ...

  3. Linux中169.254.0.0/24的路由来自哪里

    在Linux中,发现每次系统启动时,都会将(169.254.0.0/16)路由启动并将其添加到路由表中.但是并不知道这条路由具有什么功能和它到底来自于哪里? [root@master01 ~]# ro ...

  4. 【转载】在Linux系统下用dd命令制作ISO镜像U盘启动盘

    #### 将U盘插入USB接口 #umount /dev/sdb* #dd if=/iso存放路径/XXX.iso of=/dev/sdb bs=1M ##### [转载]在Linux系统下用dd命令 ...

  5. ActiveMQ FileServer漏洞(详细)

    半个月前,巡检时发现服务器出现不明进程,对其进行了处理,由于当时没有做详细记录,在这里把大致过程描述一下. 症状: ps命令发现出现几个不明进程, 1.于/tmp下运行的,名称随机的进程.占用CPU高 ...

  6. CentOS 7.3 安装指南 作者: Matei Cezar 译者: LCTT geekpi

    CentOS 7.3 安装指南 作者: Matei Cezar 译者: LCTT geekpi | 2016-12-20 09:12   评论: 11 收藏: 4 分享: 1 基于 Red Hat 企 ...

  7. Pulsar 下一代消息平台

    引言 提起成熟的消息队列或消息引擎,毋庸置疑,大多数人的第一反应一定是 Kafka. Kafka 能够彻底满足海量数据场景下高吞吐.高并发需求,在短短几年内,已经被阿里.腾讯.百度.字节跳动.Netf ...

  8. 020.Python生成器和生成器函数

    一 生成器 1.1 基本概念 元组推导式是是生成器(generator) 生成器定义 生成器可以实现自定义,迭代器是系统内置的,不能够更改 生成器的本质就是迭代器,只不过可以自定义. 生成器有两种定义 ...

  9. python文件处理(对比和筛选)版本2

    场景:对比两个txt文件的差异,将对比结果写入html,将不同部分写入另一个txt #!/user/bin/python #!coding=utf-8 # -*- coding: utf-8 -*- ...

  10. 韩小韩 API接口

    官方网址:https://api.vvhan.com/ 天气API接口: https://api.vvhan.com/api/weather Bing每日图片API接口: https://api.vv ...