Go中Channel与Java中BlockingQueue的本质区别本文转载请联系跨界捷公众号。前言最近在实现两个需求。由于两者之间没有依赖关系,所以想用队列来解耦;但是Go的标准库中并没有现成的并发安全的数据结构;但是Go提供了一个更优雅的解决方案,就是channels。通道应用Go和Java一个很大的区别是并发模型不同。Go采用CSP(Communicatingsequentialprocesses)模型;用Go的官方术语来说:不要通过共享内存进行通信;相反,通过通信共享内存。翻译过来就是:不使用共享内存来通信,而是使用通信来共享内存。这里所说的通信指的是Go中的channel。只谈概念是无法快速理解和应用的,结合几个实际案例会更容易理解。futuretaskGo官方不提供类Java的FutureTask支持:);executorService.submit(futureTask);Strings=futureTask.get();System.out.println(s);executorService.shutdown();}}classTaskimplementsCallable{@OverridepublicStringcall()throwsException{//模拟httpSystem。out.println("httprequest");Thread.sleep(1000);return"requestsuccess";}}但是我们可以使用channel和goroutine来实现类似的功能:funcmain(){ch:=Request("https://github.com")select{caser:=<-ch:fmt.Println(r)}}funcRequest(urlstring)<-chanstring{ch:=make(chanstring)gofunc(){//模拟http请求time.Sleep(time.second)ch<-fmt.Sprintf("url=%s,res=%s",url,"ok")}()return}goroutine在发起请求后会直接返回这个通道,调用者会一直阻塞直到出发例程得到响应。goroutine互相关通信/***偶数线程序*/publicstaticclassOuNumimplementsRunnable{privateTwoThreadWaitNotifySimplenumber;publicOuNum(TwoThreadWaitNotifySimplenumber){this.number=number;}@Overridepublicvoidrun(){for(inti=0;i<11;i++){synchronized(TwoThreadWaitNotifySimplenumber)){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(InterruptedExceptione){e.printStackTrace();}}}}}}/***奇数线程序*/publicstaticclassJiNumimplementsRunnable{privateTwoThreadWaitNotifySimplenumber;publicJiNum(TwoThreadWaitNotifySimplenumber){this.number=number;}@Overridepublicvoidrun(){for(inti=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();}}否则{尝试{TwoThreadWaitNotifySimple.class.wait();}catch(InterruptedExceptione){e.printStackTrace();}}}}}}这里截取Java提供的“双线程交替打印奇偶数”的部分代码object.wait()/类似object.notify()的等待通知机制可以实现两个线程之间的通信。Go也可以通过channel实现同样的效果:funcmain(){ch:=make(chanstruct{})gofunc(){fori:=1;i<11;i++{ch<-struct{}{}//oddifi%2==1{fmt.Println("odd:",i)}}}()gofunc(){fori:=1;i<11;i++{<-chifi%2==0{fmt.Println("Evennumber:",i)}}}()time.Sleep(10*time.Second)}本质上都是利用了thread(goroutine)阻塞再唤醒的特性,而Java使用的是wait/notify机制;而go提供的channel也有类似的特点:向channel发送数据时(ch<-struct{}{}),会一直阻塞,直到channel被消费(<-ch)。以上是针对无缓冲通道的。channel本身是go自己保证并发安全的,不需要额外的同步措施,所以可以放心使用。广播通知不仅仅是两个goroutine之间的通信,也是广播通知,类似于下面的Java代码:publicstaticvoidmain(String[]args)throwsInterruptedException{for(inti=0;i<10;i++){newThread(()->{try{synchronized(NotifyAll.class){NotifyAll.class.wait();}System.out.println(Thread.currentThread().getName()+"done....");}catch(InterruptedExceptione){e.printStackTrace();}}).start();}Thread.sleep(3000);synchronized(NotifyAll.class){NotifyAll.class.notifyAll();}}主线程会唤醒所有等待的子线程,这本质上是通过wait/notify机制实现的,区别在于通知所有等待的线程。相反,它是go的实现:funcmain(){notify:=make(chanstruct{})fori:=0;i<10;i++{gofunc(iint){for{select{case<-notify:fmt.Println("完成......",i)returncase<-time.After(1*time.Second):fmt.Println("waitnotify",i)}}}(i)}time.Sleep(1*time.Second)close(notify)time.Sleep(3*time.Second)}当一个channel关闭时,所有获取该channel的goroutine都会直接返回,不会阻塞。正是这个特性向所有目标goroutines广播通知。注意同一个通道不能重复关闭,否则会出现panic。通道解耦上面的例子都是基于无缓冲的通道,通常用于goroutines之间的同步;同时,channels也有缓冲的特性:ch:=make(chanT,100)可以直接理解为一个queue,正是因为它有缓冲的能力,所以我们才可以进行业务解耦。生产者只是把数据扔进通道,消费者只是把数据拿出来做自己的事情。同时,它还具有阻塞队列的特性:当通道满时,生产者会被阻塞。当通道为空时,消费者也会阻塞。从上面的例子可以看出,Go实现同样功能的写法会更简单直接,而Java会复杂很多(当然这也和这里使用的底层API有关).Java中BlockingQueue的这些特性与Java中的BlockingQueue非常相似。它们有以下相同点:goroutine/thread通信可以通过两者进行。借助队列的特性,可以实现业务解耦。支持并发安全。同样,它们之间也有很大的区别,从性能上看:channel支持select语法,对channel的管理更加简洁直观。通道支持关闭,不能向关闭的通道发送消息。Channel支持方向的定义,在编译器的帮助下,可以在语义上更准确地描述行为。当然本质上的区别在于,channel是go推荐的CSP模型的核心,有编译器的支持,可以用很轻的代价实现并发通信。对于Java来说,BlockingQueue只是一个实现并发安全的数据结构。即使不用,也有其他通讯方式;只是它们都具有阻塞队列的特点,所以在最初接触通道时很容易混淆。同点channel-specific阻塞策略支持selectsettingsize支持关闭并发security自定义方向通用数据结构compiler支持总结使用一种编程语言的经验,学习其他语言确实方便很多,比如之前写Java的时候看的在Go中,有很多相似之处,只是不同的实现。以这里的并发通信为例,本质上是因为并发模型的不同;Go推荐使用通信来共享内存,而Java大部分场景使用共享内存来通信(所以必须加锁才能同步)。带着问题学习,确实会事半功倍。