关于golang:Go-channel-VS-Java-BlockingQueue

71次阅读

共计 4635 个字符,预计需要花费 12 分钟才能阅读完成。

前言

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

正文完
 0