教妹學 Java:難以駕馭的多線程


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》系列可能有點標題黨吧?”

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

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM