Go 中的 channel 與 Java BlockingQueue 的本質(zhì)區(qū)別

image

前言

最近在實(shí)現(xiàn)兩個(gè)需求弯囊,由于兩者之間并沒有依賴關(guān)系逗栽,所以想利用隊(duì)列進(jìn)行解耦誊爹;但在 Go 的標(biāo)準(zhǔn)庫中并沒有現(xiàn)成可用并且并發(fā)安全的數(shù)據(jù)結(jié)構(gòu)芬首;但 Go 提供了一個(gè)更加優(yōu)雅的解決方案悦即,那就是 channel吮成。

channel 應(yīng)用

GoJava 的一個(gè)很大的區(qū)別就是并發(fā)模型不同,Go 采用的是 CSP(Communicating sequential processes) 模型辜梳;用 Go 官方的說法:

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

翻譯過來就是:不用使用共享內(nèi)存來通信粱甫,而是用通信來共享內(nèi)存。

而這里所提到的通信作瞄,在 Go 里就是指代的 channel茶宵。

只講概念并不能快速的理解與應(yīng)用,所以接下來會(huì)結(jié)合幾個(gè)實(shí)際案例更方便理解宗挥。

futrue task

Go 官方?jīng)]有提供類似于 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 實(shí)現(xiàn)類似的功能:

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 發(fā)起請求后直接將這個(gè) channel 返回乌庶,調(diào)用方會(huì)在請求響應(yīng)之前一直阻塞种蝶,直到 goroutine 拿到了響應(yīng)結(jié)果。

goroutine 互相通信

   /**
     * 偶數(shù)線程
     */
    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() + "+-+偶數(shù)" + i);

                            number.flag = false;
                            TwoThreadWaitNotifySimple.class.notify();
                        }

                    } else {
                        try {
                            TwoThreadWaitNotifySimple.class.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }


    /**
     * 奇數(shù)線程
     */
    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() + "+-+奇數(shù)" + i);

                            number.flag = true;
                            TwoThreadWaitNotifySimple.class.notify();
                        }

                    } else {
                        try {
                            TwoThreadWaitNotifySimple.class.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                }
            }
        }
    }

這里截取了”兩個(gè)線程交替打印奇偶數(shù)“的部分代碼瞒大。

Java 提供了 object.wait()/object.notify() 這樣的等待通知機(jī)制螃征,可以實(shí)現(xiàn)兩個(gè)線程間通信。

go 通過 channel 也能實(shí)現(xiàn)相同效果:

func main() {
    ch := make(chan struct{})
    go func() {
        for i := 1; i < 11; i++ {
            ch <- struct{}{}
            //奇數(shù)
            if i%2 == 1 {
                fmt.Println("奇數(shù):", i)
            }
        }
    }()

    go func() {
        for i := 1; i < 11; i++ {
            <-ch
            if i%2 == 0 {
                fmt.Println("偶數(shù):", i)
            }
        }
    }()

    time.Sleep(10 * time.Second)
}

本質(zhì)上他們都是利用了線程(goroutine)阻塞然后喚醒的特性透敌,只是 Java 是通過 wait/notify 機(jī)制盯滚;

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

  1. channel 發(fā)送數(shù)據(jù)時(shí)(ch<-struct{}{})會(huì)被阻塞,直到 channel 被消費(fèi)(<-ch)酗电。

以上針對于無緩沖 channel魄藕。

channel 本身是由 go 原生保證并發(fā)安全的,不用額外的同步措施顾瞻,可以放心使用泼疑。

廣播通知

不僅是兩個(gè) 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();
        }
    }

主線程將所有等待的子線程全部喚醒退渗,這個(gè)本質(zhì)上也是通過 wait/notify 機(jī)制實(shí)現(xiàn)的,區(qū)別只是通知了所有等待的線程蕴纳。

換做是 go 的實(shí)現(xiàn):

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)
}

當(dāng)關(guān)閉一個(gè) channel 后会油,會(huì)使得所有獲取 channelgoroutine 直接返回,不會(huì)阻塞古毛,正是利用這一特性實(shí)現(xiàn)了廣播通知所有 goroutine 的目的翻翩。

注意,同一個(gè) channel 不能反復(fù)關(guān)閉稻薇,不然會(huì)出現(xiàn)panic嫂冻。

channel 解耦

以上例子都是基于無緩沖的 channel,通常用于 goroutine 之間的同步塞椎;同時(shí) channel 也具備緩沖的特性:

ch :=make(chan T, 100)

可以直接將其理解為隊(duì)列桨仿,正是因?yàn)榫哂芯彌_能力,所以我們可以將業(yè)務(wù)之間進(jìn)行解耦案狠,生產(chǎn)方只管往 channel 中丟數(shù)據(jù)服傍,消費(fèi)者只管將數(shù)據(jù)取出后做自己的業(yè)務(wù)。

同時(shí)也具有阻塞隊(duì)列的特性:

  • 當(dāng) channel 寫滿時(shí)生產(chǎn)者將會(huì)被阻塞骂铁。
  • 當(dāng) channel 為空時(shí)消費(fèi)者也會(huì)阻塞吹零。

從上文的例子中可以看出,實(shí)現(xiàn)相同的功能 go 的寫法會(huì)更加簡單直接拉庵,相對的 Java 就會(huì)復(fù)雜許多(當(dāng)然這也和這里使用的偏底層 api 有關(guān))灿椅。

Java 中的 BlockingQueue

這些特性都與 Java 中的 BlockingQueue 非常類似,他們具有以下的相同點(diǎn):

  • 可以通過兩者來進(jìn)行 goroutine/thread 通信。
  • 具備隊(duì)列的特征阱扬,可以解耦業(yè)務(wù)泣懊。
  • 支持并發(fā)安全。

同樣的他們又有很大的區(qū)別麻惶,從表現(xiàn)上看:

  • channel 支持 select 語法,對 channel 的管理更加簡潔直觀信夫。
  • channel 支持關(guān)閉窃蹋,不能向已關(guān)閉的 channel 發(fā)送消息。
  • channel 支持定義方向静稻,在編譯器的幫助下可以在語義上對行為的描述更加準(zhǔn)確警没。

當(dāng)然還有本質(zhì)上的區(qū)別就是 channel 是 go 推薦的 CSP 模型的核心,具有編譯器的支持振湾,可以有很輕量的成本實(shí)現(xiàn)并發(fā)通信杀迹。

BlockingQueue 對于 Java 來說只是一個(gè)實(shí)現(xiàn)了并發(fā)安全的數(shù)據(jù)結(jié)構(gòu),即便不使用它也有其他的通信方式押搪;只是他們都具有阻塞隊(duì)列的特征树酪,所有在初步接觸 channel 時(shí)容易產(chǎn)生混淆。

相同點(diǎn) channel 特有
阻塞策略 支持select
設(shè)置大小 支持關(guān)閉
并發(fā)安全 自定義方向
普通數(shù)據(jù)結(jié)構(gòu) 編譯器支持

總結(jié)

有過一門編程語言的使用經(jīng)歷在學(xué)習(xí)其他語言是確實(shí)是要方便許多大州,比如之前寫過 Java 再看 Go 時(shí)就會(huì)發(fā)現(xiàn)許多類似之處续语,只是實(shí)現(xiàn)不同。

拿這里的并發(fā)通信來說厦画,本質(zhì)上是因?yàn)椴l(fā)模型上的不同疮茄;

Go 更推薦使用通信來共享內(nèi)存,而 Java 大部分場景都是使用共享內(nèi)存來通信(這樣就得加鎖來同步)根暑。

帶著疑問來學(xué)習(xí)確實(shí)會(huì)事半功倍力试。

最近和網(wǎng)友討論后再補(bǔ)充一下,其實(shí) Go channel 的底層實(shí)現(xiàn)也是通過對共享內(nèi)存的加鎖來實(shí)現(xiàn)的排嫌,這點(diǎn)任何語言都不可避免畸裳。

既然都是共享內(nèi)存那和我們自己使用共享內(nèi)存有什么區(qū)別呢?主要還是 channel 的抽象層級更高躏率,我們使用這類高抽象層級的方式編寫代碼會(huì)更易理解和維護(hù)躯畴。

但在一些特殊場景,需要追求極致的性能薇芝,降低加鎖顆粒度時(shí)用共享內(nèi)存會(huì)更加合適蓬抄,所以 Go 官方也提供有sync.Map/Mutex 這樣的庫;只是在并發(fā)場景下更推薦使用 channel 來解決問題夯到。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末嚷缭,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌阅爽,老刑警劉巖路幸,帶你破解...
    沈念sama閱讀 207,113評論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異付翁,居然都是意外死亡简肴,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,644評論 2 381
  • 文/潘曉璐 我一進(jìn)店門百侧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來砰识,“玉大人,你說我怎么就攤上這事佣渴”枥牵” “怎么了?”我有些...
    開封第一講書人閱讀 153,340評論 0 344
  • 文/不壞的土叔 我叫張陵辛润,是天一觀的道長膨处。 經(jīng)常有香客問我,道長砂竖,這世上最難降的妖魔是什么真椿? 我笑而不...
    開封第一講書人閱讀 55,449評論 1 279
  • 正文 為了忘掉前任,我火速辦了婚禮晦溪,結(jié)果婚禮上瀑粥,老公的妹妹穿的比我還像新娘。我一直安慰自己三圆,他們只是感情好狞换,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,445評論 5 374
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著舟肉,像睡著了一般修噪。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上路媚,一...
    開封第一講書人閱讀 49,166評論 1 284
  • 那天黄琼,我揣著相機(jī)與錄音,去河邊找鬼整慎。 笑死脏款,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的裤园。 我是一名探鬼主播撤师,決...
    沈念sama閱讀 38,442評論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼拧揽!你這毒婦竟也來了剃盾?” 一聲冷哼從身側(cè)響起腺占,我...
    開封第一講書人閱讀 37,105評論 0 261
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎痒谴,沒想到半個(gè)月后衰伯,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,601評論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡积蔚,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,066評論 2 325
  • 正文 我和宋清朗相戀三年意鲸,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片尽爆。...
    茶點(diǎn)故事閱讀 38,161評論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡临扮,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出教翩,到底是詐尸還是另有隱情,我是刑警寧澤贪壳,帶...
    沈念sama閱讀 33,792評論 4 323
  • 正文 年R本政府宣布饱亿,位于F島的核電站,受9級特大地震影響闰靴,放射性物質(zhì)發(fā)生泄漏彪笼。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,351評論 3 307
  • 文/蒙蒙 一蚂且、第九天 我趴在偏房一處隱蔽的房頂上張望配猫。 院中可真熱鬧,春花似錦杏死、人聲如沸泵肄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,352評論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽腐巢。三九已至,卻和暖如春玄括,著一層夾襖步出監(jiān)牢的瞬間冯丙,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,584評論 1 261
  • 我被黑心中介騙來泰國打工遭京, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留胃惜,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 45,618評論 2 355
  • 正文 我出身青樓哪雕,卻偏偏與公主長得像船殉,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子热监,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,916評論 2 344

推薦閱讀更多精彩內(nèi)容