Java并發第一篇(從零開始:一文讀懂Java并發編程核心基礎)

從零開始:一文讀懂Java并發編程核心基礎

    • 一. 為什么需要并發編程?
    • 二. 并發編程的“另一面”:挑戰與代價
      • 2.1 頻繁的上下文切換
      • 2.2 線程安全問題(如:死鎖)
    • 三. 夯實基礎:必須掌握的核心概念與操作
      • 3.1 厘清基本概念
      • 3.2 創建你的第一個線程
        • 方式一:繼承 `Thread` 類
        • 方式二:實現 `Runnable` 接口 (推薦)
        • 方式三:實現 `Callable` 接口 (可獲取返回值)
      • 3.3 線程的生命周期:狀態轉換
      • 3.4 線程間的“對話”:基本操作
        • `sleep()` 與 `wait()` 的經典對比
        • `join()` - 線程的協作
        • `interrupt()` - 優雅的通知機制
        • `yield()` - 主動的讓步
      • 3.5 默默的守護者:Daemon線程

為什么需要用到并發?凡事總有好壞兩面,這其中的權衡(trade-off)是什么,也就是說并發編程具有哪些缺點?以及在進行并發編程時,我們應該了解和掌握的核心概念又是什么?這篇文章將主要圍繞這三個問題,為你揭開Java并發編程的神秘面紗。

一. 為什么需要并發編程?

你可能會奇怪,我們討論的是軟件編程,為什么會扯到硬件的發展?這要從著名的“摩爾定律”說起。

在很長一段時間里,摩爾定律預示著單核處理器的計算能力會呈指數級增長。然而,大約在2004年,物理極限的瓶頸開始顯現,單純提升單核頻率變得異常困難。聰明的硬件工程師們轉變了思路:不再追求單個計算單元的極致速度,而是將多個計算單元整合到一顆CPU中。這就是“多核CPU”時代的到來。

如今,家用的i7處理器擁有8個甚至更多的核心已是常態,服務器級別的CPU核心數則更為龐大。硬件已經鋪好了路,但如何才能榨干這些核心的性能呢?

答案就是并發編程。

頂級計算機科學家Donald Ervin Knuth曾半開玩笑地評價:“在我看來,并發這種現象或多或少是由于硬件設計者無計可施了,他們將摩爾定律的責任推給了軟件開發者。”

這句評價一語中的。正是多核CPU的普及,催生了并發編程的浪潮。通過并發編程,我們可以:

  • 充分利用多核CPU的計算能力:將復雜的計算任務分解,讓多個核心同時工作,從而大幅提升程序性能。想象一下處理一張高清圖片,如果串行處理數百萬個像素點會非常耗時,但如果將圖片分成幾塊,交由不同的核心并行處理,速度就會成倍提升。
  • 方便進行業務拆分,提升應用響應速度:在很多業務場景中,并發是天生的需求。例如,在網上購物下單時,系統需要同時完成檢查庫存、生成訂單、扣減優惠券、通知物流等多個操作。如果這些操作串行執行,用戶需要等待很長時間。而通過并發技術,這些操作可以被拆分到不同的線程中“同時”進行,極大地縮短了用戶的等待時間,提升了體驗。

正是這些顯著的優點,使得并發編程成為現代軟件開發者必須掌握的關鍵技能。

二. 并發編程的“另一面”:挑戰與代價

既然并發編程如此強大,我們是否應該在所有場景下都使用它呢?答案顯然是否定的。它是一把雙刃劍,在帶來性能提升的同時,也引入了新的復雜性和挑戰。

2.1 頻繁的上下文切換

在我們看來,多個線程似乎是“同時”執行的,但這在單核CPU上只是一種宏觀上的錯覺。CPU會為每個線程分配一個極短的時間片(通常是幾十毫秒),然后快速地在不同線程間輪換。這個切換過程,被稱為上下文切換

切換時,系統需要保存當前線程的運行狀態(如程序計數器、寄存器值等),以便下次輪到它時能恢復現場。這個保存和恢復的過程本身是有性能開銷的。如果線程數量過多,或者切換過于頻繁,上下文切換消耗的時間甚至可能超過線程真正執行任務的時間,導致程序性能不升反降。

如何減少上下文切換?

  • 無鎖并發編程:例如ConcurrentHashMap的分段鎖思想,讓不同線程處理不同數據段,減少鎖競爭。
  • CAS算法:Java的Atomic包使用了CAS(比較并交換)這種樂觀鎖機制,它在很多場景下能避免加鎖帶來的阻塞和上下文切換。
  • 使用最少線程:創建適量的線程,避免大量線程處于空閑等待狀態。
  • 使用協程:在單線程內實現多任務調度,這是更輕量級的“線程”。

2.2 線程安全問題(如:死鎖)

這是并發編程中最棘手、也最容易出錯的地方。當多個線程訪問共享資源(也稱為“臨界區”)時,如果沒有恰當的同步機制,就可能導致數據錯亂、狀態不一致,甚至出現死鎖

死鎖是指兩個或多個線程無限期地互相等待對方釋放資源,導致所有相關的線程都無法繼續執行。

來看一個經典的死鎖示例:

public class DeadLockDemo {private static final String resource_a = "資源A";private static final String resource_b = "資源B";public static void main(String[] args) {Thread threadA = new Thread(() -> {synchronized (resource_a) {System.out.println(Thread.currentThread().getName() + " 獲得了 " + resource_a);try {// 等待一會兒,確保threadB能獲得resource_bThread.sleep(1000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(Thread.currentThread().getName() + " 嘗試獲得 " + resource_b + "...");synchronized (resource_b) {System.out.println(Thread.currentThread().getName() + " 獲得了 " + resource_b);}}}, "線程A");Thread threadB = new Thread(() -> {synchronized (resource_b) {System.out.println(Thread.currentThread().getName() + " 獲得了 " + resource_b);System.out.println(Thread.currentThread().getName() + " 嘗試獲得 " + resource_a + "...");synchronized (resource_a) {System.out.println(Thread.currentThread().getName() + " 獲得了 " + resource_a);}}}, "線程B");threadA.start();threadB.start();}
}

在這個例子中,線程A獲得了resource_a的鎖,然后嘗試去獲得resource_b的鎖;而線程B同時獲得了resource_b的鎖,并嘗試獲得resource_a的鎖。兩者互相持有對方需要的鎖,并等待對方釋放,從而陷入了永久的等待,形成了死鎖。

我們可以使用JDK自帶的jpsjstack命令來診斷這種情況,jstack會明確地告訴你“Found 1 deadlock”。

如何避免死鎖?

  1. 避免一個線程同時獲取多個鎖:盡量減少鎖的持有范圍和時間。
  2. 保證加鎖順序:確保所有線程都按照相同的順序來獲取鎖。
  3. 使用定時鎖:使用lock.tryLock(timeout),當等待超時后線程可以主動放棄,而不是無限期阻塞。
  4. 將鎖和資源隔離:對于數據庫鎖,確保加鎖和解鎖在同一個數據庫連接中完成。

三. 夯實基礎:必須掌握的核心概念與操作

了解了并發的優缺點后,讓我們深入到實踐層面,看看在Java中到底該如何使用和操作線程。

3.1 厘清基本概念

  • 同步 vs 異步 (Synchronous vs Asynchronous):這通常用來描述一次方法調用。

    • 同步:調用方發起調用后,必須原地等待被調用方法執行完畢并返回結果,才能繼續執行后續代碼。就像你去實體店買東西,必須排隊、付款、拿到商品后才能離開。
    • 異步:調用方發起調用后,不等待結果,立即返回并繼續執行后續代碼。被調用的方法在后臺執行,完成后通過回調、通知等方式告訴調用方。就像網購,你下單后就可以去做別的事了,快遞到了會通知你去取。
  • 并發 vs 并行 (Concurrency vs Parallelism)

    • 并發:指多個任務在一段時間內都得到了執行,它們在宏觀上是“同時”發生的,但在微觀上可能是通過時間片快速交替執行的。好比一個人在同時處理做飯、接電話、看孩子三件事,他需要不停地在任務間切換。
    • 并行:指多個任務在同一時刻真正地同時執行。這必須在多核CPU上才能實現。好比三個人,一人做飯,一人接電話,一人看孩子,他們是真正在同一時間做著不同的事。
  • 阻塞 vs 非阻塞 (Blocking vs Non-blocking):這通常用來形容線程間的相互影響。

    • 阻塞:一個線程的操作導致它自身被掛起,等待某個條件滿足(如等待I/O完成、等待獲取鎖)。在此期間,它不會占用CPU。
    • 非阻塞:一個線程的操作不會導致自身被掛起,無論操作是否成功都會立即返回。

3.2 創建你的第一個線程

一個Java程序從main()方法啟動時,JVM就已經創建了多個線程(如主線程、垃圾回收線程等)。要在我們自己的程序中創建線程,主要有以下三種方式:

方式一:繼承 Thread

這是最直接的方式,通過繼承Thread并重寫run()方法來定義任務。

class MyThread extends Thread {@Overridepublic void run() {System.out.println("通過繼承Thread類創建線程");}
}// 使用
MyThread thread = new MyThread();
thread.start(); // 必須調用start()來啟動新線程
  • 優點:實現簡單,易于理解。
  • 缺點:Java是單繼承的,如果你的類已經繼承了其他類,就無法再繼承Thread,這極大地限制了其靈活性。
方式二:實現 Runnable 接口 (推薦)

這是更常用、也更受推薦的方式。它將“任務”(Runnable)和“執行任務的載體”(Thread)解耦開來。

class MyRunnable implements Runnable {@Overridepublic void run() {System.out.println("通過實現Runnable接口創建線程");}
}// 使用
MyRunnable myRunnable = new MyRunnable();
Thread thread = new Thread(myRunnable);
thread.start();
  • 優點
    • 任務與線程解耦,結構更清晰。
    • 避免了單繼承的限制,你的任務類還可以繼承其他類。
    • 多個線程可以共享同一個Runnable實例,方便實現資源共享。
  • 缺點:代碼比方式一稍微多一點。
方式三:實現 Callable 接口 (可獲取返回值)

Runnablerun()方法沒有返回值,也不能拋出受檢異常。如果你的任務需要一個執行結果或可能拋出異常,Callable是更好的選擇。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;class MyCallable implements Callable<String> {@Overridepublic String call() throws Exception {Thread.sleep(2000); // 模擬耗時任務return "通過實現Callable接口返回的結果";}
}// 使用
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());System.out.println("主線程繼續做其他事情...");// 在需要結果時,調用get()方法阻塞等待
String result = future.get(); 
System.out.println(result);
executor.shutdown();
  • 優點
    • 可以獲得任務的返回值。
    • 可以向外拋出異常。
  • 說明Callable通常與線程池(ExecutorService)配合使用,submit()方法返回一個Future對象,你可以用它來跟蹤任務狀態并獲取結果。

3.3 線程的生命周期:狀態轉換

一個Java線程在其生命周期中,會經歷多種狀態的變遷。這些狀態定義在java.lang.Thread.State枚舉中。

在這里插入圖片描述

  • NEW (新建): new Thread()之后,但還未調用start()
  • RUNNABLE (可運行): 調用start()后,線程進入就緒隊列,等待CPU調度。它可能正在運行,也可能在等待運行。
  • BLOCKED (阻塞): 線程等待獲取一個synchronized監視器鎖。
  • WAITING (無限等待): 線程調用Object.wait()Thread.join()等方法后進入此狀態,需要被其他線程顯式喚醒。
  • TIMED_WAITING (計時等待): 與WAITING類似,但有超時限制,時間到了會自動返回RUNNABLE狀態。
  • TERMINATED (終止): run()方法執行完畢或因異常退出。

3.4 線程間的“對話”:基本操作

sleep()wait() 的經典對比

sleep()是讓線程“睡一會”,而wait()是讓線程“等通知”。這是面試高頻題,也是理解線程協作的關鍵。

特性Thread.sleep(long millis)Object.wait()
所屬類Thread (靜態方法)Object (實例方法)
鎖的釋放不釋放對象鎖釋放對象鎖
使用前提任何地方都可以調用必須在synchronized代碼塊或方法中
喚醒方式時間到期后自動喚醒需要其他線程調用notify()notifyAll()
join() - 線程的協作

join()方法允許一個線程等待另一個線程執行完成。就像接力賽跑,你必須等前一個隊友跑完把接力棒交給你,你才能開始跑。

// 在main線程中
Thread worker = new Thread(() -> {System.out.println("工作線程正在處理任務...");try { Thread.sleep(3000); } catch (InterruptedException e) {}
});
worker.start();
worker.join(); // main線程會在這里暫停,直到worker線程執行完畢
System.out.println("工作線程已結束,主線程繼續執行。");
interrupt() - 優雅的通知機制

interrupt()并非強制中斷線程,而是一種協作式的“打招呼”機制。它會設置目標線程的中斷標志位。

  • 如果線程正在sleepwaitjoin,它會立即被喚醒并拋出InterruptedException,同時清除中斷標志位
  • 如果線程正在正常運行,它需要自己通過Thread.currentThread().isInterrupted()來檢查這個標志,并決定如何響應。
final Thread busyThread = new Thread(() -> {while (true) {} // 死循環,消耗CPU
}, "busyThread");busyThread.start();
busyThread.interrupt(); // 設置中斷標志// 等待片刻后檢查
System.out.println("busyThread isInterrupted: " + busyThread.isInterrupted()); // 輸出: true
yield() - 主動的讓步

yield()是一個靜態方法,它會向線程調度器暗示:當前線程愿意讓出CPU,給其他同等優先級的線程一個執行機會。但這僅僅是一個建議,調度器可能會忽略它,所以它不保證當前線程一定會暫停。

3.5 默默的守護者:Daemon線程

Java中的線程分為兩類:用戶線程(User Thread)和守護線程(Daemon Thread)。

  • 用戶線程:是我們平時創建的普通線程,執行系統的業務邏輯。
  • 守護線程:在后臺運行,為其他線程(主要是用戶線程)提供服務。最典型的例子就是垃圾回收(GC)線程。

當JVM中所有的用戶線程都執行完畢后,無論是否還有守護線程在運行,JVM都會退出。

Thread daemonThread = new Thread(() -> {while (true) {System.out.println("我是守護線程,正在后臺守護...");try {Thread.sleep(1000);} catch (InterruptedException e) {}}
});daemonThread.setDaemon(true); // 必須在start()之前設置
daemonThread.start();System.out.println("Main線程即將結束...");
// Main線程(用戶線程)結束后,JVM會退出,daemonThread也會隨之終止

一個重要的注意事項:守護線程在JVM退出時會被強制終止,其finally代碼塊不保證一定會被執行。因此,不要在守護線程的finally中執行關鍵的資源釋放操作。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/web/89307.shtml
繁體地址,請注明出處:http://hk.pswp.cn/web/89307.shtml
英文地址,請注明出處:http://en.pswp.cn/web/89307.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【刪庫跑路】一次刪除pip的所有第三方庫

進入命令行&#xff0c;先list看下庫存pip list導出所有的第三方庫至一文件列表pip freeze >requirements.txt按照列表卸載所有庫pip uninstall -r requirements.txt -y再list看下&#xff0c;可見庫存已清空

python 【技術面試題和HR面試題】?列表操作、條件判斷、循環、函數定義編程題

1.技術面試題 &#xff08;1&#xff09;解釋Linux中的進程、線程和守護進程的概念&#xff0c;以及如何管理它們&#xff1f; 答&#xff1a; 進程 概念&#xff1a;程序運行的實例&#xff0c;有獨立資源&#xff08;如內存&#xff09;&#xff0c;是系統調度的基本單位。 管…

Debian 12中利用dpkg命令安裝MariaDB 11.8.2

MariaDB 11.8解決了2038問題&#xff0c;即在32位系統中將timestamp從2038-01-19 03:14:07 UTC擴展到2106-02-07 06:28:15 UTC&#xff0c;向后延長了68年。由于寫此文時Debian 12的源中還沒有MariaDB 11.8,采用源碼編譯又太費時&#xff0c;可用二進制碼或dpkg安裝 .下面簡要記…

Go語言高并發聊天室(三):性能優化與壓力測試

Go語言高并發聊天室&#xff08;三&#xff09;&#xff1a;性能優化與壓力測試 &#x1f3af; 本篇目標 在前兩篇文章中&#xff0c;我們完成了聊天室的基礎功能。本篇將深入性能優化&#xff0c;實現真正的高并發&#xff1a; &#x1f50d; 性能瓶頸分析? 關鍵優化技術&…

【leetcode】852. 山脈數組的封頂索引

文章目錄題目題解1. 遍歷2. 二分查找題目 852. 山脈數組的封頂索引 給定一個長度為 n 的整數 山脈 數組 arr &#xff0c;其中的值遞增到一個 峰值元素 然后遞減。 返回峰值元素的下標。 你必須設計并實現時間復雜度為 O(log(n)) 的解決方案。 示例 1&#xff1a; 輸入&a…

Java期末考試準備

文章目錄Java期末考試準備一、Java的輸入.next()輸入.nextLine()輸入區別補充二、Java的輸出三、類中常寫方法toString()equals()其他四、容器/數組五、繼承六、靜態屬性、方法.七、抽象類八、接口九、初始化模塊十、泛型考完結束語Java學習歷程注:這篇文章本來是寫給同學的&am…

飛算JavaAI進階:重塑Java開發范式的AI革命

引言&#xff1a;當代碼生成進入"自動駕駛"時代 在2025年的Java開發領域&#xff0c;一場由AI驅動的革命正在重塑傳統開發范式。當GitHub Copilot還在通過代碼補全提升效率時&#xff0c;飛算JavaAI已實現從需求分析到完整工程代碼生成的"端到端"閉環。這款…

如何在銀河麒麟桌面系統中啟用 sudo 密碼的星號反饋

引文 我們在銀河麒麟桌面操作系統上使用 sudo 命令時&#xff0c;都遇到過這樣的困擾&#xff1a;輸入密碼時光標一動不動&#xff0c;屏幕上沒有任何提示&#xff08;沒有星號 *&#xff0c;也沒有任何字符&#xff09;&#xff1f;就像在黑暗中摸索鑰匙孔一樣&#xff0c;心里…

二刷 黑馬點評 秒殺優化

優化邏輯 把耗時較短的邏輯判斷放入redsi中&#xff0c;比如庫存是否足夠以及是否一人一單&#xff0c;只要這樣的邏輯完成&#xff0c;就代表一定能下單成功&#xff0c;我們就將結果返回給用戶&#xff0c;然后我們再開一個線程慢慢執行隊列中的信息 問題&#xff1a; 如何快…

HANA SQLScript中的變量類型匯總

在 SAP HANA SQLScript 中&#xff0c;可以使用多種變量類型&#xff0c;包括標量&#xff08;Scalar&#xff09;類型、表類型和結構化類型。以下是各種變量類型的詳細說明和示例。1. 標量變量&#xff08;Scalar Variables&#xff09; 標量變量是用于存儲單個值&#xff08;…

基于 Amazon Nova Sonic 和 MCP 構建語音交互 Agent

1、引言 隨著人工智能技術的飛速發展&#xff0c;自然語言處理和語音交互技術正在深刻改變人機交互的方式。語音交互正從簡單的“機械應答”向更自然的“類人對話”演進 。傳統的語音系統通常采用模塊化架構&#xff0c;將語音處理流程割裂為 ASR&#xff08;自動語音識別&…

項目的存量接口怎么低成本接入MCP?

項目的存量接口怎么低成本接入MCP&#xff1f; 老項目里的一些接口&#xff0c;如何低成本的接入MCP&#xff08;0成本不可能&#xff09;&#xff0c;變成MCP server 的tools&#xff1f; 先拋出這個問題&#xff1f;評論區的xdm如果有懂的&#xff0c;可以打在評論區&#xf…

用圖片生成高保真3D模型!Hi3DGen以法線為橋,為高清三維幾何生成另辟蹊徑

主頁&#xff1a;http://qingkeai.online/ 原文&#xff1a;用圖片生成高保真3D模型&#xff01;Hi3DGen以法線為橋&#xff0c;為高清三維幾何生成另辟蹊徑 隨著從二維圖像構建高保真三維模型的需求日益增長&#xff0c;現有方法由于域間隙的限制以及 RGB 圖像固有的模糊性&a…

Charles抓包工具中文安裝和使用詳解,快速掌握API調試與網絡優化

Charles抓包工具中文安裝和使用詳解 在軟件開發中&#xff0c;調試API請求、捕獲網絡流量以及優化應用性能是開發者日常工作中不可或缺的環節。Charles抓包工具作為業內領先的網絡調試工具&#xff0c;以其功能強大、易用性高、支持HTTPS流量解密等特點&#xff0c;廣泛應用于A…

Java :List,LinkedList,ArrayList

文章目錄List常用方法List集合的遍歷方式ArrayList底層的原理LinkedList底層原理常用方法List常用方法 //1.創建一個ArrayList集合對象&#xff08;有序、有索引、可以重復&#xff09; List<String> list new ArrayList<>(); list.add("蜘蛛精"); list…

LLM面試題及講解 4

LLM面試題及講解 4 目錄 LLM面試題及講解 4 題目講解 一、基礎概念與理論 二、模型訓練與優化 三、應用與實踐 四、前沿研究與趨勢 大型語言模型(LLM)的核心特征是什么? LLM與傳統NLP技術的本質區別是什么? Transformer架構的基本組成部分有哪些?其在LLM中為何重要? BERT…

Harmony-Next鴻蒙實戰開發項目-仿小米商城App----V2

1.、簡介 本項目是Harmony-Next原生開發&#xff0c;真實網絡請求。采用V2等狀態管理裝飾器。包含&#xff08;首頁、分類、發現、購物車、我的、登錄、搜索&#xff0c;搜索結果&#xff0c;商品詳情等&#xff09;.包含V2對接口返回數據的深度監聽。 2、頁面展示&#xff1…

python閉包和裝飾器(超詳解)

目錄 一、閉包的概念 1.概念 2.閉包的特征 3.閉包的作用 二、裝飾器 1.什么是裝飾器 2.裝飾器的作用 1.統計代碼耗時 2.對代碼進行權限檢查 3.記錄日志 3.閉包和裝飾器的關系 4.注意事項&#xff1a; 一、閉包的概念 1.概念 閉包&#xff08;Closure&#xff09;指…

解決hadoop常用到的問題

1.namenode無法啟動問題 報錯1. ERROR: Attempting to operate on hdfs namenode as root ERROR: but there is no HDFS_NAMENODE_USER defined. 原因&#xff1a;不能用 root 用戶直接啟動 Hadoop 的 HDFS 組件&#xff08;NameNode / DataNode / SecondaryNameNode&#xff0…

深度學習G3周:CGAN入門(生成手勢圖像)

&#x1f368; 本文為&#x1f517;365天深度學習訓練營中的學習記錄博客&#x1f356; 原作者&#xff1a;K同學啊 基礎任務&#xff1a; 1.條件生成對抗網絡&#xff08;CGAN&#xff09;的基本原理 2.CGAN是如何實現條件控制的 3.學習本文CGAN代碼&#xff0c;并跑通代碼…