教妹學 Java:難以駕馭的多執行緒

沉默王二發表於2019-06-06

00、故事的起源

“二哥,上一篇《集合》的反響效果怎麼樣啊?”三妹對她提議的《教妹學 Java》專欄很關心。

“這篇文章的瀏覽量要比第一篇《泛型》好得多。”

“這是個好訊息啊,說明更多人接受了二哥的創作。”三妹心花怒放了起來。

“也許沒什麼對比性。”

“沒有對比性?我翻看了一下二哥 7 個月前寫的文章,是真的水啊,嘻嘻。”三妹賣了一個萌,繼續說道,“說實話,竟然還有讀者願意看,真的是不可思議。”

“你是想捱揍嗎?”

“別啊。我是說,二哥現在的讀者真的很幸運,因為他們看到了更高質量的文章。”三妹繼續肆無忌憚地說著她的真心話。

“是啊,比以前好多了,但我還要更加地努力,這次的主題是《多執行緒》,三妹你準備好了嗎?”

“早準備好了。讓我繼續來提問吧,二哥你繼續回答。”三妹已經躍躍欲試了。

01、二哥,什麼是執行緒啊?

三妹,聽哥給你慢慢講啊。

要想了解執行緒,得先了解程式,因為執行緒是程式的一個單元。你看,我這臺電腦同時開了很多個程式,比如說打字用的這個輸入法、寫作用的這個瀏覽器,聽歌用的這個音樂播放器。

這些程式同時可能幹幾件事,比如說這個音樂播放器,一邊滾動著歌詞,一邊播放著音訊。也就是說,在一個程式內部,可能同時執行著多個執行緒(Thread),每個執行緒負責著不同的任務。

由於每個程式至少要幹一件事,所以,一個程式至少有一個執行緒。在 Java 的程式當中,至少會有一個 main 方法,也就是所謂的主執行緒。

可以同時執行多個執行緒,執行方式和多個程式是一樣的,都是由作業系統決定的。作業系統可以在多個執行緒之間進行快速地切換,讓每個執行緒交替地執行。切換的時間越短,程式的效率就越高。

程式和執行緒之間的關係可以用一句通俗的話講,就是“程式是爹媽,管著眾多的執行緒兒女。”

02、二哥,為什麼要用多執行緒啊?

三妹,先去給哥泡杯咖啡,再來聽哥給你慢慢地講。

多執行緒作為一種多工、併發的工作方式,好處多多。

第一,減少應用程式的響應時間。

對於計算機來說,IO 讀寫和網路通訊相對是比較耗時的任務,如果不使用多執行緒的話,其他耗時少的任務也必須要等待這些任務結束後才能執行。

第二,充分利用多核 CPU 的優勢。

作業系統可以保證當執行緒數不大於 CPU 數目時,不同的執行緒執行於不同的 CPU 上。不過,即便執行緒數超過了 CPU 數目,作業系統和執行緒池也會盡最大可能地減少執行緒切換花費的時間,最大可能地發揮併發的優勢,提升程式的效能。

第三,相比於多程式,多執行緒是一種更“高效”的多工執行方式。

對於不同的程式來說,它們具有獨立的資料空間,資料之間的共享必須通過“通訊”的方式進行。而執行緒則不需要,同一程式下的執行緒之間共享資料空間。

當然了,如果兩個執行緒存取相同的物件,並且每個執行緒都呼叫了一個修改該物件狀態的方法,將會帶來新的問題。

什麼問題呢?我們來通過下面的示例進行說明。

public class Cmower {

    public static int count = 0;

    public static int getCount() {
        return count;
    }

    public static void addCount() {
        count++;
    }

    public static void main(String[] args) {
        ExecutorService executorService = new ThreadPoolExecutor(10, 1000, 60L, TimeUnit.SECONDS,
                new ArrayBlockingQueue<Runnable>(10));

        for (int i = 0; i < 1000; i++) {
            Runnable r = new Runnable() {

                @Override
                public void run() {
                    Cmower.addCount();
                }
            };
            executorService.execute(r);
        }
        executorService.shutdown();
        System.out.println(Cmower.count);
    }

}

我們建立了一個執行緒池,通過 for 迴圈讓執行緒池執行 1000 個執行緒,每個執行緒呼叫了一次 Cmower.addCount() 方法,對 count 值進行加 1 操作,當 1000 個執行緒執行完畢後,在控制檯列印 count 的值。

其結果會是什麼呢?

998、997、998、996、996

但幾乎不會是我們想要的答案 1000。

03、二哥,為什麼答案不是 1000 呢?

三妹啊,咖啡泡得太濃了。不過,濃一點的好處是更提神了。

程式在執行過程中,會將運算需要的資料從實體記憶體中複製一份到 CPU 的快取記憶體當中,計算結束之後,再將快取記憶體中的資料重新整理到實體記憶體當中。

count++ 來說。當執行緒執行這個語句時,會先從實體記憶體中讀取 count 的值,然後複製一份到快取記憶體當中,CPU 執行指令對 count 進行加 1 操作,再將快取記憶體中 count 的最新值重新整理到實體記憶體當中。

在多核 CPU 中,每個執行緒可能執行於不同的 CPU 中,因此每個執行緒在執行時會有專屬的快取記憶體。假設執行緒 A 正在對 count 進行加 1 操作,此時執行緒 B 的快取記憶體中 count 的值仍然是 0 ,進行加 1 操作後 count 的值為 1。最後兩個執行緒把最新值 1 重新整理到實體記憶體中,而不是理想中的 2。

這種被多個執行緒訪問的變數被稱為共享變數,他們通常需要被保護起來。

04、二哥,那該怎麼保護共享變數呢?

三妹啊,等我喝口咖啡提提神。

針對上例中出現的 count,可以按照下面的方式進行改造。

public static AtomicInteger count = new AtomicInteger();

public static int getCount() {
    return count.get();
}

public static void addCount() {
    count.incrementAndGet();
}

使用支援原子操作(即一個操作或者多個操作要麼全部執行,並且執行的過程不會被任何因素打斷,要麼就都不執行)的 AtomicInteger 代替基本型別 int。

簡單分析一下 AtomicInteger 類,該類原始碼中可以看到一個有趣的變數 unsafe

private static final Unsafe unsafe = Unsafe.getUnsafe();

Unsafe 是一個可以執行不安全、容易犯錯操作的特殊類。AtomicInteger 使用了 Unsafe 的原子操作方法 compareAndSwapInt() 對資料進行更新,也就是所謂的 CAS。

public final native boolean compareAndSwapInt(Object o, long offset,
                                                int expected,
                                                int x);

引數 o 是要進行 CAS 操作的物件(比如說 count),引數 offset 是記憶體位置,引數 expected 是期望的值,引數 x 是需要更新到的值。

一般的同步方法會從地址 offset 讀取值 A,執行一些計算後獲得新值 B,然後使用 CAS 將 offset 的值從 A 改為 B。如果 offset 處的值尚未同時更改,則 CAS 操作成功。

CAS 允許執行“讀-修改-寫”的操作,而無需擔心其他執行緒同時修改了變數,因為如果其他執行緒修改變數,那麼 CAS 會檢測它(並失敗),演算法可以對該操作重新計算。

AtomicInteger 類的原始碼中還有一個值得注意的變數 value

private volatile int value;

value 使用了關鍵字 volatile 來保證可見性——當多個執行緒訪問同一個變數時,一個執行緒修改了這個變數的值,其他執行緒能夠立即看得到修改的值。

當一個共享變數被 volatile 修飾後,它被修改後的值會立即更新到實體記憶體中,當有其他執行緒需要讀取時,會去實體記憶體中讀取新值。

而沒有被 volatile 修飾的共享變數不能保證可見性,因為不確定這些變數會在什麼時候被寫入實體記憶體中,當其他執行緒去讀取時,讀到的可能還是原來的舊值。

特別需要注意的是,volatile 關鍵字只保證變數的可見性,不能保證原子性。

05、故事的未完待續

“二哥,《多執行緒》就先講到這吧,再多我就吸收不了了!”三妹的態度很誠懇。

“可以。”

“二哥,我記得上次你說要給大號投稿,結果怎麼樣了?”三妹關切地問。

“唉,都不好意思說,只收獲了兩個點讚的表情符號,可能還是基於同情心。嚇得我不敢再投稿了,先堅持寫吧!”

“結局這麼慘淡嗎,真的沒有一個號要轉載嗎?我看那個投稿群有三百多個公號呢。”三妹很傷心。

“《教妹學 Java》系列可能有點標題黨吧?”

“二哥,既然決定要寫,請不要懷疑自己。至少三妹很喜歡這種風格啊。”聽完三妹語重心長的話,我心底的那種自我懷疑又煙消雲散了。

 

相關文章