亚洲在线久爱草,狠狠天天香蕉网,天天搞日日干久草,伊人亚洲日本欧美

為了賬號安全,請及時綁定郵箱和手機立即綁定
慕課專欄

目錄

索引目錄

Java 并發編程深度解析

原價 ¥ 58.00

立即訂閱
02 多線程帶來哪些問題
更新時間:2020-12-09 15:57:08
衡量一個人的真正品格,是看他在知道沒人看見的時候干些什么。——孟德斯鳩

既然多線程能讓我們充分發揮多處理器優勢,提升性能,那是不是啟用線程越多越好呢?在多線程編程過程中都會遇到哪些問題呢?通過本節的學習,相信你就得到答案。

1. 線程安全問題

我們先看幾個多線程的代碼栗子

樣例 1: 火車票多窗口售票

假設火車站有 10 萬張火車票,有三個售票窗口,售完為止,最后輸出我們一共售賣了多少張火車票,我們通過多線程代碼來實現它。

public class TrainTest {

    //剩余的火車票數量
    public static Integer leftTicketTotal =  10000;
    //售出的火車票數量
    public static Integer selledTicketTotal = 0;

    public static class TicketWindow implements Runnable {

        @Override
        public void run() {
             while (leftTicketTotal > 0) {
                selledTicketTotal++;
                leftTicketTotal--;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        //啟動窗口1售票線程
        Thread thread1 = new Thread(new TicketWindow());
        thread1.start();
        //啟動窗口2售票線程
        Thread thread2 = new Thread(new TicketWindow());
        thread2.start();
        //啟動窗口3售票線程
        Thread thread3 = new Thread(new TicketWindow());
        thread3.start();

        //等待三個線程執行完成
        thread1.join();
        thread2.join();
        thread3.join();
        //輸出最終火車票數量
        System.out.println("售出火車票數量:" + selledTicketTotal + " 剩余火車票數量:" + leftTicketTotal);
    }
}

運行后,我們得到的輸出結果卻是,售出火車票數與總數不一致。

售出火車票數量:9099 剩余火車票數量:-2

而且我們發現每次輸出的結果都不一樣。接下來我們分析下造成不一致的原因

我們看到代碼 selledTicketTotal++ (即 selledTicketTotal = selledTicketTotal + 1) , 實際上包括三個操作,讀取 selledTicketTotal 的值,進行加 1 操作,寫入新的值;同理 leftTicketTotal-- 也包括三個操作,讀取 leftTicketTotal 的值,進行減 1 操作,寫入新的值。這三個操作組成的 selledTicketTotal++selledTicketTotal-- 都是非原子操作的,那什么是原子操作呢?

原子操作是指不可被分割的一系列操作。

對一個變量的非原子操作往往會產生非預期的結果,比如線程 A 和線程 B 都在執行 selledTicketTotal++,線程 A 讀到 selledTicketTotal=10, 由于非原子操作是可被分割的,此時線程 B 不會等待 A 操作完成執行加 1 操作,而是同樣讀到了 selledTicketTotal=10,線程 A 和 B 以 10 做基數分別做加 1 操作,selledTicketTotal 最終結果為 11,而不是預期的 12,這就是非原子操作帶來數據不一致。

樣例 2: 水龍頭開關

假設我們向蓄水池中放水,一段時間后停止放水

public class WaterTapTest {

    public static boolean tapOpen = true;

    public static class WaterTapTask implements Runnable {

        @Override
        public void run() {
            while (tapOpen) {
                    try {
                        System.out.println("水龍頭放水");
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(new WaterTapTask());
        thread.start();

        Thread.sleep(1000);
        tapOpen = false;
        System.out.println("水龍頭停止放水");
        //一定概率下,會繼續輸出水龍頭放水
        thread.join();
    }
}

在我們將 tapOpen 設置為 false 后,水龍頭依然在放水,這就是我們要說的第二個問題,多線程下的可見性問題,即當一個線程更新了一個變量,另一個線程并不能及時得到變量修改后的值。那什么是可見性呢?

可見性:當一個線程修改了對象狀態后,其他線程能夠看到發生的狀態變化。

當讀操作和寫操作在不同的線程中執行時,我們無法確保執行讀操作的線程能實時看到其他線程寫入的值。

以上兩個例子闡述了非原子和不可見帶來的問題,這兩類問題均屬于線程安全問題。線程安全的定義:

多個線程訪問某個類時,這個類始終表現出正確的行為,那么就稱這個類是線程安全的。

綜上所述,要保證線程安全需要滿足兩大條件

  • 原子性:一系列操作,要么全部完成,要么全部不完成,不可被分割,不會結束在中間某個環節。
  • 可見性:當一個線程修改了對象狀態后,其他線程能夠看到發生的狀態變化。

保證原子性的手段有單線程、加鎖、CAS (后續章節我們會介紹到),保證可見性的手段是通過插入內存屏障 (后續章節我們會介紹) 來解決。

2. 上下文切換

Java 中的線程與 CPU 單核執行是一對一的,即單個處理器同一時間只能處理一個線程的執行;而 CPU 是通過時間片算法來執行任務的,不同的線程活躍狀態不同,CPU 會在多個線程間切換執行,在切換時會保存上一個任務的狀態,以便下次切換回這個任務時可以再加載到這個任務的狀態,這種任務的保存到加載就是一次上下文切換。線程數越多,帶來的上下文切換越嚴重,上下文切換會帶來 CPU 系統態使用率占用,這就是為什么當我們開啟大量線程,系統反而更慢的原因。

我們要減少上下文切換,有幾種手段:

  • 減少鎖等待:鎖等待意味著,線程頻繁在活躍與等待狀態之間切換,增加上下文切換,鎖等待是由對同一份資源競爭激烈引起的,在一些場景我們可以用一些手段減輕鎖競爭,比如數據分片或者數據快照等方式。
  • CAS 算法:利用 Compare and Swap, 即比較再交換可以避免加鎖。后續章節會介紹 CAS 算法。
  • 使用合適的線程數或者協程:使用合適的線程數而不是越多越好,在 CPU 密集的系統中,比如我們傾向于啟動最多 2 倍處理器核心數量的線程;協程由于天然在單線程實現多任務的調度,所以協程實際上避免了上下文切換。

3. 活躍性問題 (死鎖、饑餓)

當某些操作遲遲得不到執行時,就被認為是產生了活躍性問題,活躍性分為兩類,一類是死鎖,一類是饑餓。

死鎖是最常見的活躍性問題,除此之外還有饑餓、活鎖。當線程由于無法訪它所需的資源而不能繼續執行時,就發生了饑餓。

在多線程開發中,我們要避免線程安全問題,勢必要對共享的數據資源進行加鎖,而加鎖處理不當即會帶來死鎖。

我們以死鎖為例,看看死鎖是如何發生的:
圖片描述

我們看上面這張圖,線程 A 和線程 B 都擁有一份鎖,而線程 A 和線程 B 恰好同時去獲取對方擁有的那把鎖,導致兩個線程永遠無法執行,要避免死鎖有一個方法即獲取鎖的順序是固定的,比如只能先獲取鎖 X 再獲取鎖 Y,不允許出現相反的順序。

4. 總結

多線程能給我們帶來很多好處,比如充分利用多核處理能力,建模簡單,異步事件簡化處理。

但在多線程在運行過程中,會帶來三類問題,分別是線程安全性、上下文切換和活躍性問題,接下來章節的我們就從起因到解決再分析原理一起攻克這三座大山。

下圖是本小節的腦圖整理
圖片描述

參考資料

  1. 《Java 并發編程實戰》
  2. 維基百科
}
立即訂閱 ¥ 58.00

你正在閱讀課程試讀內容,訂閱后解鎖課程全部內容

千學不如一看,千看不如一練

手機
閱讀

掃一掃 手機閱讀

Java 并發編程深度解析
立即訂閱 ¥ 58.00

舉報

0/150
提交
取消