Go 中的 channel 与 Java BlockingQueue 的本质区别

简介: 最近在实现两个需求,由于两者之间并没有依赖关系,所以想利用队列进行解耦;但在 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 来解决问题。


相关文章
|
2月前
|
安全 Java API
Java SE 与 Java EE 区别解析及应用场景对比
在Java编程世界中,Java SE(Java Standard Edition)和Java EE(Java Enterprise Edition)是两个重要的平台版本,它们各自有着独特的定位和应用场景。理解它们之间的差异,对于开发者选择合适的技术栈进行项目开发至关重要。
395 1
|
2月前
|
存储 Java Go
对比Java学习Go——函数、集合和OOP
Go语言的函数支持声明与调用,具备多返回值、命名返回值等特性,结合`func`关键字与类型后置语法,使函数定义简洁直观。函数可作为一等公民传递、赋值或作为参数,支持匿名函数与闭包。Go通过组合与接口实现面向对象编程,结构体定义数据,方法定义行为,接口实现多态,体现了Go语言的简洁与高效设计。
|
2月前
|
存储 Java 编译器
对比Java学习Go——程序结构与变量
本节对比了Java与Go语言的基础结构,包括“Hello, World!”程序、代码组织方式、入口函数定义、基本数据类型及变量声明方式。Java强调严格的面向对象结构,所有代码需置于类中,入口方法需严格符合`public static void main(String[] args)`格式;而Go语言结构更简洁,使用包和函数组织代码,入口函数为`func main()`。两种语言在变量声明、常量定义、类型系统等方面也存在显著差异,体现了各自的设计哲学。
|
2月前
|
安全 Java 编译器
对比Java学习Go——基础理论篇
本章介绍了Java开发者学习Go语言的必要性。Go语言以简单、高效、并发为核心设计哲学,摒弃了传统的类继承和异常机制,采用组合、接口和多返回值错误处理,提升了代码清晰度与开发效率。Go直接编译为静态二进制文件,启动迅速、部署简便,其基于Goroutine和Channel的并发模型相较Java的线程与锁机制更轻量安全。此外,Go Modules简化了依赖管理,与Java的Maven/Gradle形成鲜明对比,提升了构建与部署效率。
|
3月前
|
Java 测试技术
Java浮点类型详解:使用与区别
Java中的浮点类型主要包括float和double,它们在内存占用、精度范围和使用场景上有显著差异。float占用4字节,提供约6-7位有效数字;double占用8字节,提供约15-16位有效数字。float适合内存敏感或精度要求不高的场景,而double精度更高,是Java默认的浮点类型,推荐在大多数情况下使用。两者都存在精度限制,不能用于需要精确计算的金融领域。比较浮点数时应使用误差范围或BigDecimal类。科学计算和工程计算通常使用double,而金融计算应使用BigDecimal。
1709 102
|
3月前
|
消息中间件 人工智能 缓存
Go与Java Go和Java微观对比
本文对比了Go语言与Java在线程实现上的差异。Go通过Goroutines实现并发,使用`go`关键字启动;而Java则通过`Thread`类开启线程。两者在通信机制上也有所不同:Java依赖共享内存和同步机制,如`synchronized`、`Lock`及并发工具类,而Go采用CSP模型,通过Channel进行线程间通信。此外,文章还介绍了Go中使用Channel和互斥锁解决并发安全问题的示例。
230 0
|
3月前
|
Go 开发者
Go语言实战案例:使用select监听多个channel
本文为《Go语言100个实战案例 · 网络与并发篇》第5篇,详解Go并发核心工具`select`的使用。通过实际案例讲解如何监听多个Channel、实现多任务处理、超时控制和非阻塞通信,帮助开发者掌握Go并发编程中的多路异步事件处理技巧。
|
3月前
|
数据采集 编解码 监控
Go语言实战案例:使用channel实现生产者消费者模型
本文是「Go语言100个实战案例 · 网络与并发篇」第4篇,通过实战案例详解使用 Channel 实现生产者-消费者模型,涵盖并发控制、任务调度及Go语言并发哲学,助你掌握优雅的并发编程技巧。
|
4月前
|
JavaScript Java Go
Go、Node.js、Python、PHP、Java五种语言的直播推流RTMP协议技术实施方案和思路-优雅草卓伊凡
Go、Node.js、Python、PHP、Java五种语言的直播推流RTMP协议技术实施方案和思路-优雅草卓伊凡
319 0
|
4月前
|
存储 缓存 人工智能
Java int和Integer的区别
本文介绍了Java中int与Integer的区别及==与equals的比较机制。Integer是int的包装类,支持null值。使用==比较时,int直接比较数值,而Integer比较对象地址;在-128至127范围内的Integer值可缓存,超出该范围或使用new创建时则返回不同对象。equals方法则始终比较实际数值。
165 0
下一篇
oss云网关配置