深入理解Java線程
引言:為什么我們需要關(guān)注線程?
在多核處理器成為主流的今天,我們手中的手機、電腦甚至智能家居設(shè)備都擁有多個計算核心。這意味著,如果我們的程序只能在一個核心上運行,就相當于讓其他核心"閑置",無法充分發(fā)揮硬件性能。想象一下,一個餐廳只有一個服務(wù)員,即使廚房有多個廚師,顧客仍然需要排隊等待服務(wù)——這就是單線程程序的局限性。
并發(fā)編程正是為了解決這個問題而生,而線程作為并發(fā)編程的基礎(chǔ)單元,理解其工作機制對于編寫高效、穩(wěn)定的應(yīng)用程序至關(guān)重要。作為一名Java開發(fā)者,我深刻體會到,對線程的深入理解往往區(qū)分了初級和高級程序員。在這篇博客中,我將分享我對Java線程的個人理解,從基礎(chǔ)概念到底層實現(xiàn),希望能為你提供有價值的見解。
一、線程與進程:本質(zhì)區(qū)別與內(nèi)在聯(lián)系
在深入線程之前,我們需要從根本上理解線程與進程的區(qū)別。這個理解不能停留在表面,而要深入到操作系統(tǒng)層面。
進程:獨立的王國
進程可以理解為一個獨立的"程序王國",每個王國都有自己獨立的領(lǐng)土(內(nèi)存空間)、資源(打開的文件、網(wǎng)絡(luò)連接等)和法律(安全上下文)。操作系統(tǒng)為每個進程分配獨立的虛擬地址空間,這意味著:
進程A無法直接訪問進程B的內(nèi)存數(shù)據(jù)。
進程崩潰通常不會影響其他進程。
進程間通信需要特殊機制(管道、消息隊列、共享內(nèi)存等)。
線程:王國內(nèi)的協(xié)作團隊
線程則是同一個"王國"內(nèi)的不同"工作團隊",它們:
共享王國的資源(內(nèi)存、文件描述符等)。
各自執(zhí)行不同的任務(wù),但可以協(xié)作完成共同目標。
通信成本極低,因為可以直接訪問共享內(nèi)存。
技術(shù)視角的深度理解:
從操作系統(tǒng)角度看,進程是資源分配的實體,而線程是CPU調(diào)度的實體。當我們在Java中創(chuàng)建線程時,實際上是在用戶態(tài)創(chuàng)建了一個線程控制塊,然后通過系統(tǒng)調(diào)用在內(nèi)核態(tài)創(chuàng)建對應(yīng)的內(nèi)核線程(在Linux中通過clone系統(tǒng)調(diào)用)。這就是為什么線程的創(chuàng)建和銷毀比進程輕量得多。
二、Java線程的創(chuàng)建方式:選擇背后的思考
1. 繼承Thread類:簡單但不推薦
|
class MyThread extends Thread { @Override public void run() { System.out.println("線程執(zhí)行: " + Thread.currentThread().getName()); } } |
這種方式看似簡單,但實際上存在設(shè)計上的問題。Java是單繼承語言,如果繼承了Thread類,就無法繼承其他類。這違反了"組合優(yōu)于繼承"的設(shè)計原則。此外,從任務(wù)執(zhí)行的角度看,線程的執(zhí)行體(run方法)和線程本身(Thread類)應(yīng)該是兩個關(guān)注點,這種方式將它們耦合在一起。
2. 實現(xiàn)Runnable接口:推薦的標準做法
|
class MyRunnable implements Runnable { @Override public void run() { System.out.println("線程執(zhí)行: " + Thread.currentThread().getName()); } } |
為什么這是更好的選擇?
符合面向?qū)ο笤O(shè)計原則:任務(wù)與執(zhí)行機制分離。
靈活性:可以繼承其他類,實現(xiàn)其他接口。
可復用性:同一個Runnable實例可以被多個線程共享執(zhí)行。
3. 實現(xiàn)Callable接口:需要返回值的場景
|
class MyCallable implements Callable<String> { @Override public String call() throws Exception { return "線程執(zhí)行結(jié)果: " + Thread.currentThread().getName(); } } |
核心價值:
Callable的出現(xiàn)解決了Runnable無法返回結(jié)果和拋出受檢異常的問題。FutureTask作為RunnableFuture接口的實現(xiàn),既可以被Thread執(zhí)行,又可以通過Future接口獲取結(jié)果,這種設(shè)計體現(xiàn)了接口隔離原則。
4. 線程池方式:生產(chǎn)環(huán)境的必然選擇
|
ExecutorService executor = Executors.newFixedThreadPool(5); Future<String> future = executor.submit(new MyCallable()); |
為什么線程池如此重要?
直接創(chuàng)建線程的成本很高,包括:
內(nèi)存分配:每個線程需要分配棧空間(默認512KB-1MB)。
系統(tǒng)調(diào)用:需要內(nèi)核參與線程創(chuàng)建。
資源管理:線程數(shù)量無限制增長會導致系統(tǒng)資源耗盡。
線程池通過復用線程、控制并發(fā)數(shù)量、管理生命周期,解決了這些問題。
三、線程狀態(tài)與生命周期:狀態(tài)機的藝術(shù)
理解線程的狀態(tài)轉(zhuǎn)換不僅僅是記住幾個狀態(tài)名稱,而是要理解每個狀態(tài)轉(zhuǎn)換的條件和意義。
狀態(tài)轉(zhuǎn)換的深度解析
NEW → RUNNABLE:(線程生命開始)
當調(diào)用start()方法時,線程從NEW狀態(tài)進入RUNNABLE狀態(tài)。這里有個重要細節(jié):start()方法只能調(diào)用一次,否則會拋出IllegalThreadStateException。這是因為線程的生命周期是不可逆的。
RUNNABLE → BLOCKED:(鎖競爭導致)
這種情況通常發(fā)生在 synchronized 同步塊上。當線程A持有鎖,線程B嘗試獲取同一個鎖時,線程B就會進入BLOCKED狀態(tài)。這里的關(guān)鍵理解是:BLOCKED狀態(tài)只與同步的monitor鎖相關(guān)。
RUNNABLE → WAITING:(主動等待)
有三種方法會導致這種轉(zhuǎn)換:
Object.wait():釋放鎖并等待,需要其他線程調(diào)用notify()/notifyAll()
Thread.join():等待目標線程終止
LockSupport.park():底層并發(fā)工具使用
RUNNABLE → TIMED_WAITING:(主動等待)
與WAITING類似,但帶有超時時間。這是為了避免永久等待導致的死鎖。
實際開發(fā)中的意義:
理解這些狀態(tài)轉(zhuǎn)換對于調(diào)試多線程問題至關(guān)重要。當線程出現(xiàn)問題時,我們可以通過jstack等工具查看線程狀態(tài),快速定位問題原因。
四、線程同步與線程安全:秩序的藝術(shù)
可見性、原子性、有序性
在深入同步機制前,必須理解并發(fā)編程的三個核心問題:
可見性:一個線程對共享變量的修改,其他線程能夠立即看到。由于CPU緩存的存在,線程可能讀取到過期的數(shù)據(jù)。
原子性:一個或多個操作要么全部執(zhí)行成功,要么全部不執(zhí)行,不會出現(xiàn)中間狀態(tài)。
有序性:程序執(zhí)行的順序按照代碼的先后順序執(zhí)行。由于指令重排序的存在,實際執(zhí)行順序可能與代碼順序不同。
synchronized的深度理解
|
public class SynchronizedDemo { // 實例同步方法:鎖是當前對象實例 public synchronized void instanceMethod() { // 臨界區(qū) }
// 靜態(tài)同步方法:鎖是當前類的Class對象 public static synchronized void staticMethod() { // 臨界區(qū) }
// 同步代碼塊:可以指定任意對象作為鎖 public void someMethod() { synchronized(this) { // 臨界區(qū) } } } |
synchronized的實現(xiàn)原理:
在字節(jié)碼層面,通過monitorenter和monitorexit指令實現(xiàn)。
每個對象都有一個monitor(監(jiān)視器鎖)與之關(guān)聯(lián)。
鎖具有可重入性:同一個線程可以多次獲取同一把鎖。
ReentrantLock:更靈活的鎖機制
|
public class ReentrantLockDemo { private final ReentrantLock lock = new ReentrantLock(true); // 公平鎖
public void performTask() { lock.lock(); // 可以在這里使用lockInterruptibly()支持中斷 try { // 臨界區(qū) } finally { lock.unlock(); // 必須在finally塊中釋放鎖 } } } |
與synchronized的對比:
|
特性 |
synchronized |
ReentrantLock |
|---|---|---|
|
實現(xiàn)機制 |
JVM內(nèi)置 |
JDK實現(xiàn) |
|
鎖獲取 |
自動獲取釋放 |
手動控制 |
|
可中斷 |
不支持 |
支持 |
|
公平性 |
非公平 |
可選擇公平或非公平 |
|
條件變量 |
單一 |
多個 |
volatile關(guān)鍵字:輕量級的同步
|
public class VolatileExample { private volatile boolean shutdown = false;
public void shutdown() { shutdown = true; // 寫操作具有原子性和可見性 }
public void doWork() { while (!shutdown) { // 讀操作總能獲取最新值 // 執(zhí)行任務(wù) } } } |
volatile的語義:
可見性:對volatile變量的寫操作會立即刷新到主內(nèi)存。
有序性:禁止指令重排序(內(nèi)存屏障)。
不保證原子性:復合操作(如i++)仍然需要同步。
適用場景:
狀態(tài)標志位。
雙重檢查鎖定模式。
觀察者模式中的狀態(tài)發(fā)布。
五、線程間通信:協(xié)作的智慧
wait/notify機制:經(jīng)典的線程協(xié)作
|
public class WaitNotifyDemo { private boolean condition = false;
public synchronized void waitForCondition() throws InterruptedException { // 必須使用while循環(huán)檢查條件,避免虛假喚醒 while (!condition) { wait(); // 釋放鎖并等待 } // 條件滿足,執(zhí)行后續(xù)操作 doSomething(); }
public synchronized void signalCondition() { condition = true; notifyAll(); // 通知所有等待線程 } } |
wait/notify的使用要點:
必須在同步方法或同步塊中調(diào)用。
總是使用while循環(huán)檢查條件,避免虛假喚醒。
優(yōu)先使用notifyAll()而不是notify(),避免信號丟失。
Condition接口:更精確的線程控制
|
public class ConditionDemo { private final Lock lock = new ReentrantLock(); private final Condition condition = lock.newCondition(); private boolean ready = false;
public void await() throws InterruptedException { lock.lock(); try { while (!ready) { condition.await(); // 等待條件 } } finally { lock.unlock(); } }
public void signal() { lock.lock(); try { ready = true; condition.signal(); // 通知等待線程 } finally { lock.unlock(); } } } |
Condition的優(yōu)勢:
一個鎖可以關(guān)聯(lián)多個Condition。
支持更靈活的等待條件。
可以精確喚醒特定類型的等待線程。
六、線程池的核心原理:池化技術(shù)的典范
線程池的架構(gòu)設(shè)計
線程池采用了生產(chǎn)者-消費者模式:
生產(chǎn)者:提交任務(wù)的線程
消費者:工作線程
緩沖區(qū):工作隊列
|
public class ThreadPoolAnatomy { // ThreadPoolExecutor的核心構(gòu)造參數(shù) ThreadPoolExecutor executor = new ThreadPoolExecutor( 5, // 核心線程數(shù):池中保持的線程數(shù)量 10, // 最大線程數(shù):池中允許的最大線程數(shù)量 60L, // 保持時間:超出核心線程數(shù)的空閑線程存活時間 TimeUnit.SECONDS, // 時間單位 new LinkedBlockingQueue<>(100), // 工作隊列:存儲待執(zhí)行任務(wù) Executors.defaultThreadFactory(), // 線程工廠:創(chuàng)建新線程 new ThreadPoolExecutor.AbortPolicy() // 拒絕策略:無法處理任務(wù)時的策略 ); } |
任務(wù)執(zhí)行流程的深度解析
任務(wù)提交:調(diào)用execute()或submit()方法
核心線程檢查:如果當前線程數(shù) < corePoolSize,創(chuàng)建新線程
隊列檢查:如果線程數(shù) ≥ corePoolSize,嘗試將任務(wù)放入隊列
最大線程檢查:如果隊列已滿且線程數(shù) < maximumPoolSize,創(chuàng)建新線程
拒絕策略:如果隊列已滿且線程數(shù) ≥ maximumPoolSize,執(zhí)行拒絕策略
這個流程的重要性在于:它決定了線程池的行為特性。理解這個流程有助于我們根據(jù)具體場景配置合適的參數(shù)。
拒絕策略的四種選擇
AbortPolicy(默認):拋出RejectedExecutionException。
CallerRunsPolicy:由調(diào)用者線程執(zhí)行任務(wù)。
DiscardPolicy:靜默丟棄任務(wù)。
DiscardOldestPolicy:丟棄隊列中最老的任務(wù),然后重試。
七、常見問題與最佳實踐:經(jīng)驗的結(jié)晶
死鎖:四大必要條件
死鎖的發(fā)生需要同時滿足四個條件:
互斥條件:資源不能被共享
持有并等待:線程持有資源并等待其他資源
不可剝奪:資源只能由持有線程釋放
循環(huán)等待:存在線程資源的循環(huán)等待鏈
預(yù)防死鎖的策略:
按固定順序獲取鎖
使用tryLock()帶有超時機制
使用更高級的并發(fā)工具
|
public class DeadlockPrevention { private final Object lock1 = new Object(); private final Object lock2 = new Object();
public void method1() { synchronized(lock1) { // 一些操作 synchronized(lock2) { // 臨界區(qū) } } }
public void method2() { synchronized(lock1) { // 使用與method1相同的鎖順序 // 一些操作 synchronized(lock2) { // 臨界區(qū) } } } } |
上下文切換:看不見的性能殺手
上下文切換的成本包括:
直接成本:保存和恢復線程上下文。
間接成本:緩存失效、TLB刷新。
優(yōu)化建議:
避免創(chuàng)建過多線程。
使用線程池復用線程。
減少鎖競爭(鎖細化、使用并發(fā)集合)。
最佳實踐總結(jié)
命名線程:便于調(diào)試和監(jiān)控
|
ThreadFactory namedThreadFactory = new ThreadFactoryBuilder() .setNameFormat("worker-thread-%d") .build(); |
正確處理異常:
|
executor.submit(() -> { try { // 任務(wù)邏輯 } catch (Exception e) { // 記錄日志,不要吞掉異常 logger.error("Task execution failed", e); } }); |
資源清理:
|
executor.shutdown(); try { if (!executor.awaitTermination(60, TimeUnit.SECONDS)) { executor.shutdownNow(); } } catch (InterruptedException e) { executor.shutdownNow(); Thread.currentThread().interrupt(); } |
八、Java內(nèi)存模型(JMM):并發(fā)編程的理論基礎(chǔ)
happens-before關(guān)系
happens-before是JMM的核心概念,它定義了操作之間的可見性關(guān)系:
程序次序規(guī)則:線程內(nèi)按照代碼順序執(zhí)行。
監(jiān)視器鎖規(guī)則:解鎖操作happens-before后續(xù)的加鎖操作。
volatile變量規(guī)則:寫操作happens-before后續(xù)的讀操作。
線程啟動規(guī)則:Thread.start()happens-before線程內(nèi)的任何操作。
線程終止規(guī)則:線程中的所有操作happens-before其他線程檢測到該線程已經(jīng)終止。
內(nèi)存屏障
為了實現(xiàn)happens-before關(guān)系,JVM在適當?shù)奈恢貌迦雰?nèi)存屏障:
LoadLoad屏障:禁止讀操作重排序。
StoreStore屏障:禁止寫操作重排序。
LoadStore屏障:禁止讀后寫重排序。
StoreLoad屏障:禁止寫后讀重排序。
理解這些底層機制有助于我們寫出正確性更高的并發(fā)代碼。
本文基于個人實踐經(jīng)驗和深入研究總結(jié)而成,技術(shù)觀點如有不同見解歡迎交流討論。

浙公網(wǎng)安備 33010602011771號