Java 并發基礎之并發編程通識

引言:為何并發編程如此重要?

在現代軟件開發中,并發編程已不再是陽春白雪,而是開發者必備的核心技能之一。隨著多核 CPU 的普及,以及業務系統對高性能、高吞吐量和快速響應的持續追求,充分利用計算資源、提升程序運行效率變得至關重要。并發編程正是實現這一目標的關鍵手段。

什么是并發 (Concurrency)?

并發指的是在同一個時間段內,能夠處理多個任務的能力。注意,這并不意味著多個任務在同一時刻同時執行(那是并行),而是在宏觀上看起來是同時在推進。例如,一個 Web 服務器需要同時處理來自多個用戶的請求,一個 GUI 應用需要在響應用戶操作的同時執行后臺計算,這些都是并發的典型場景。

為何需要并發?

  1. 提升性能:
    • 利用多核 CPU: 對于計算密集型任務,可以通過并發將其分解到多個核心上并行執行,縮短總處理時間。
    • 提高資源利用率: 對于 I/O 密集型任務(如網絡請求、磁盤讀寫),當一個任務等待 I/O 時,CPU 可以切換去執行其他任務,避免空閑浪費。
  2. 改善響應性: 在圖形用戶界面 (GUI) 或交互式應用中,將耗時操作(如文件下載)放到后臺線程執行,可以保持主線程(UI 線程)的流暢響應,提升用戶體驗。
  3. 簡化復雜問題: 某些問題天然適合分解為多個并發執行的獨立單元,使得程序設計更模塊化、更易于理解和管理(例如生產者-消費者模型)。
    image.png

然而,并發編程并非銀彈,它引入了復雜性,帶來了諸多挑戰,如線程安全問題、死鎖、資源競爭等。掌握并發編程的基礎知識,理解其核心概念、挑戰以及 Java 提供的解決方案,是編寫健壯、高效并發程序的基石。本文旨在梳理 Java 并發編程的基礎通識,為讀者構建一個清晰的知識框架。

一、核心基礎概念

在深入探討 Java 并發機制之前,需要先明確幾個基本概念。

1. 進程 (Process) 與線程 (Thread)

  • 進程: 操作系統進行資源分配和調度的基本單位。一個進程是內存中一個正在運行的程序實例,它擁有獨立的內存空間(地址空間)、文件句柄、系統資源等。進程間通信 (IPC) 相對復雜且開銷較大。
  • 線程: 進程內的一個執行單元,是 CPU 調度的基本單位。一個進程可以包含多個線程,它們共享該進程的內存空間(堆內存、方法區等)和系統資源(如文件句柄),但每個線程擁有自己獨立的程序計數器、虛擬機棧和本地方法棧。線程間的通信相對簡單高效,但共享資源也帶來了同步問題。

Java 程序啟動時至少會創建一個主線程(執行 main 方法的線程)。開發者可以通過 new Thread() 或線程池等方式創建更多線程。

2. 線程的生命周期 (Java specific)

在 Java 中,一個線程從創建到消亡會經歷不同的狀態,定義在 Thread.State 枚舉中:

  1. NEW (新建): 使用 new Thread() 創建,但尚未調用 start() 方法。
  2. RUNNABLE (可運行): 調用 start() 方法后,線程進入可運行狀態。它包含了操作系統線程狀態中的 Running (運行中)Ready (就緒)。處于此狀態的線程可能正在 JVM 中執行,也可能在等待操作系統分配 CPU 時間片。
    image.png
  3. BLOCKED (阻塞): 線程等待獲取一個監視器鎖 (Monitor Lock) 時進入此狀態。通常發生在進入 synchronized 方法或代碼塊時,鎖被其他線程持有。當獲取到鎖后,會轉換回 RUNNABLE
  4. WAITING (無限期等待): 線程等待其他線程執行特定動作時進入此狀態。常見觸發條件:
    • 調用無超時的 Object.wait()
    • 調用無超時的 Thread.join()
    • 調用 LockSupport.park()
      需要被其他線程顯式喚醒(如 Object.notify(), Object.notifyAll(), LockSupport.unpark())才能回到 RUNNABLE
  5. TIMED_WAITING (有限期等待): 與 WAITING 類似,但等待有時間限制。超時后會自動返回 RUNNABLE。常見觸發條件:
    • 調用帶超時的 Thread.sleep(long millis)
    • 調用帶超時的 Object.wait(long timeout)
    • 調用帶超時的 Thread.join(long millis)
    • 調用帶超時的 LockSupport.parkNanos(), LockSupport.parkUntil()
  6. TERMINATED (終止): 線程的 run() 方法執行完畢或因未捕獲的異常退出后,進入此狀態。線程生命周期結束。

理解線程狀態對于調試并發問題(如死鎖、性能瓶頸)非常有幫助。

3. 并發 (Concurrency) 與并行 (Parallelism)

  • 并發 (Concurrency): 指邏輯上同時處理多個任務的能力。在單核 CPU 上,通過時間片輪轉快速切換任務,使得宏觀上感覺任務在同時進行。
  • 并行 (Parallelism): 指物理上同時執行多個任務的能力。這需要多核 CPU 或分布式系統的支持,多個任務在同一時刻真正地一起運行。

關系: 并行是并發的一種實現方式。并發的目標是充分利用資源、提高響應,可以在單核或多核上實現;而并行必須依賴多核或多機才能實現,是提升絕對速度的關鍵。Java 并發編程既可以用于實現并發(提高資源利用率和響應性),也可以用于實現并行(利用多核加速計算)。

二、并發編程的三大挑戰

編寫正確的并發程序之所以困難,主要源于以下三個核心問題:

1. 原子性 (Atomicity) 問題

原子性指一個或多個操作,要么全部執行且執行過程不被任何因素打斷,要么就都不執行。

在并發環境中,如果一個操作不是原子的,那么在執行過程中可能被其他線程干擾,導致數據不一致,這就是競爭條件 (Race Condition)

經典示例: count++ 操作。

public class UnsafeCounter {
    private int count = 0;

    public void increment() {
        count++; // 非原子操作
    }

    public int getCount() {
        return count;
    }

    public static void main(String[] args) throws InterruptedException {
        UnsafeCounter counter = new UnsafeCounter();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        });
        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 10000; i++) {
                counter.increment();
            }
        });

        t1.start();
        t2.start();
        t1.join(); // 等待 t1 結束
        t2.join(); // 等待 t2 結束

        System.out.println("Final count: " + counter.getCount()); // 結果通常小于 20000
    }
}
image.png

count++ 看似簡單,實際包含三個步驟:

  1. 讀取 count 的當前值。
  2. 將值加 1。
  3. 將新值寫回 count

如果兩個線程同時執行 increment(),可能發生以下情況:

  • 線程 A 讀取 count (假設為 10)。
  • 線程 B 讀取 count (也為 10)。
  • 線程 A 計算 10 + 1 = 11。
  • 線程 B 計算 10 + 1 = 11。
  • 線程 A 將 11 寫回 count
  • 線程 B 將 11 寫回 count

兩次 increment() 操作后,count 預期應為 12,但結果卻是 11。這就是原子性被破壞導致的競爭條件。

解決方案: 需要使用鎖或其他原子操作機制(如 synchronized, Lock, AtomicInteger)來保證 count++ 的原子性。

2. 可見性 (Visibility) 問題

可見性指當一個線程修改了共享變量的值,其他線程能夠立即得知這個修改。

在現代多核 CPU 架構中,每個核心都有自己的高速緩存 (Cache)。線程對共享變量的操作可能先在自己的工作內存(CPU Cache 的抽象)中進行,然后再擇機寫回主內存。這會導致一個線程修改了變量值,但該值尚未寫回主內存,或者其他線程仍然讀取的是自己緩存中的舊值,從而產生可見性問題。

示例:

public class VisibilityProblem {
    private boolean stopRequested = false; // 共享變量

    public void requestStop() {
        stopRequested = true; // 線程 A 修改
        System.out.println("Stop requested.");
    }

    public void run() {
        System.out.println("Worker thread started.");
        while (!stopRequested) { // 線程 B 讀取
            // do work...
            // 可能因為可見性問題,一直讀取到 false,導致循環無法停止
        }
        System.out.println("Worker thread stopped.");
    }

    public static void main(String[] args) throws InterruptedException {
        VisibilityProblem worker = new VisibilityProblem();
        Thread workerThread = new Thread(worker::run);
        workerThread.start();

        Thread.sleep(1000); // 讓 worker 線程跑一會兒

        Thread stopperThread = new Thread(worker::requestStop);
        stopperThread.start();

        // 如果存在可見性問題,workerThread 可能永遠不會結束
        workerThread.join(5000); // 等待 worker 線程結束,設置超時
        if (workerThread.isAlive()) {
            System.out.println("Worker thread did not stop due to visibility issue!");
            // 在實際情況中需要更健壯的停止機制
        }
    }
}

在這個例子中,stopperThread 修改 stopRequestedtrue,但 workerThread 可能由于緩存原因一直讀取到 false,導致死循環。

解決方案: 使用 volatile 關鍵字修飾 stopRequested,或使用鎖 (synchronized, Lock) 來保證修改的可見性。

還有 72% 的精彩內容
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
支付 ¥9.99 繼續閱讀
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,345評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,494評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,283評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,953評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,714評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,186評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,255評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,410評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,940評論 1 335
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,776評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,976評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,518評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,210評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,642評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,878評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,654評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,958評論 2 373

推薦閱讀更多精彩內容

  • 引言 并發編程是一個經典的話題,由于摩爾定律已經改變,芯片性能雖然仍在不斷提高,但相比加快 CPU 的速度,計算機...
    you的日常閱讀 1,878評論 0 53
  • 目錄 概念 并行是指兩個或者多個事件在同一時刻發生(cpu多核);而并發是指兩個或多個事件在同一時間間隔內發生 饑...
    后來丶_a24d閱讀 972評論 0 13
  • 前言 并發在大型項目中的應用很廣,可以有效提升應用性能,充分使用硬件資源。同時,也是Java面試的主要內容。 重排...
    佛貝魯先生閱讀 140評論 0 0
  • 1、并行與并發的區別 1)并行指多個事件在同一個時刻發生;并發指在某時刻只有一個事件在發生,某個時間段內由于 CP...
    執著的逗比閱讀 592評論 0 2
  • 一、Java為什么要多線程? 為了合理利用 CPU 的高性能,平衡【CPU、內存、I/O 設備】的速度差異,計算機...
    柚子過來閱讀 587評論 0 0