一、開篇:走進 Java 并發編程世界
????????在現代軟件開發中,充分利用多核 CPU 的計算能力至關重要,Java 并發編程為我們提供了實現這一目標的工具。從簡單的多線程任務并行執行,到復雜的高并發系統設計,掌握并發編程是進階 Java 工程師的關鍵一步。本篇作為上篇,聚焦多線程基礎、線程狀態、線程組與優先級、進程線程區別,以及synchronized
鎖的基礎與狀態體系 。
? ? ? ? 先疊個甲,由于這一塊內容是面試必問的部分,也是經常用的,內容太多,我分三篇逐步更新,從基礎線程概念到線程池、鎖等復雜場景。
二、Java 多線程入門:創建與核心邏輯
(一)創建線程的三種方式
1. 繼承 Thread 類:線程邏輯內聚
????????繼承Thread
類,重寫run
方法定義線程執行體。調用start
方法啟動新線程(直接調用run
是普通方法調用,不會新建線程 )。
class MyThread extends Thread {@Overridepublic void run() {for (int i = 0; i < 3; i++) {System.out.println(Thread.currentThread().getName() + "執行,i=" + i);}}
}public class ThreadInheritDemo {public static void main(String[] args) {MyThread thread1 = new MyThread();thread1.setName("自定義線程1");thread1.start(); MyThread thread2 = new MyThread();thread2.setName("自定義線程2");thread2.start(); }
}
運行后,兩個線程交替輸出,體現多線程并發執行特性,適合線程邏輯簡單且無需復用的場景。
2. 實現 Runnable 接口:解耦任務與線程
????????將線程執行邏輯封裝到Runnable
實現類,避免單繼承限制(Java 類僅能單繼承,但可實現多個接口 ),方便任務邏輯復用。????????
class MyRunnable implements Runnable {@Overridepublic void run() {for (int i = 0; i < 3; i++) {System.out.println(Thread.currentThread().getName() + "執行,i=" + i);}}
}public class RunnableImplDemo {public static void main(String[] args) {MyRunnable runnable = new MyRunnable();Thread threadA = new Thread(runnable, "線程A");Thread threadB = new Thread(runnable, "線程B");threadA.start();threadB.start();}
}
?Runnable
作為任務載體,被不同線程實例執行,常用于線程池、任務分發等場景。
3. 實現 Callable 接口:支持返回結果
??Callable
與Runnable
類似,但call
方法有返回值,需結合Future
、FutureTask
獲取結果,適用于需線程執行產出數據的場景。
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;class MyCallable implements Callable<Integer> {@Overridepublic Integer call() throws Exception {int sum = 0;for (int i = 1; i <= 5; i++) {sum += i;}return sum;}
}public class CallableImplDemo {public static void main(String[] args) {MyCallable callable = new MyCallable();FutureTask<Integer> futureTask = new FutureTask<>(callable);Thread thread = new Thread(futureTask, "計算線程");thread.start();try {Integer result = futureTask.get(); System.out.println("線程計算結果:" + result);} catch (InterruptedException | ExecutionException e) {e.printStackTrace();}}
}
futureTask.get()
會阻塞直到線程執行完畢返回結果,也可設置超時時間避免永久阻塞。
二)線程關鍵疑問:run 與 start、重寫 run 的本質
1. 為何重寫 run 方法?
??Thread
類默認run
方法體為空(public void run() {}
?),重寫run
是為了定義線程實際執行的業務邏輯,讓線程啟動后執行我們期望的代碼。
2. run 方法與 start 方法的區別
- run 方法:普通實例方法,調用時由當前線程(調用
run
的線程)順序執行run
內代碼,不會創建新線程。- start 方法:
Thread
類特殊方法,調用后觸發 JVM創建新線程,并由新線程執行run
邏輯。即start
是 “啟動新線程 + 執行任務”,run
只是 “執行任務(當前線程)” 。
class TestThread extends Thread {@Overridepublic void run() {System.out.println("當前線程名:" + Thread.currentThread().getName());}
}public class StartRunDistinguish {public static void main(String[] args) {TestThread thread = new TestThread();thread.setName("自定義線程");thread.run(); thread.start(); }
}
輸出:
當前線程名:main
當前線程名:自定義線程
清晰展示兩者差異,start
才是真正啟動新線程的方式。
三)控制線程的常用方法
1. sleep ():線程休眠
????????使當前線程暫停指定時間(毫秒),讓出 CPU 但不釋放對象鎖(若持有鎖)。常用于模擬延遲、協調執行節奏。
public class SleepUsage {public static void main(String[] args) {new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println(Thread.currentThread().getName() + "執行,i=" + i);try {Thread.sleep(1000); } catch (InterruptedException e) {e.printStackTrace();}}}, "睡眠線程").start();}
}
線程每隔 1 秒輸出,期間 CPU 可被其他線程使用,但鎖資源(若涉及同步代碼)不會釋放。
2. join ():線程等待
讓當前線程等待目標線程執行完畢后再繼續,用于協調線程執行順序。
public class JoinUsage {public static void main(String[] args) throws InterruptedException {Thread threadA = new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println("線程A執行,i=" + i);try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}});threadA.start();threadA.join(); System.out.println("線程A執行完畢,main線程繼續");}
}
?“main 線程繼續” 需在線程 A 執行完 3 次循環后才輸出,確保執行順序。
其實也可以這樣理解,讓A線程插隊,當前線程main在A線程執行完畢后再執行
3. setDaemon ():守護線程
????????守護線程為用戶線程服務,所有用戶線程結束后,守護線程自動終止(如 JVM 的 GC 線程 )。需在start
前設置。
public class DaemonUsage {public static void main(String[] args) {Thread daemonThread = new Thread(() -> {while (true) {System.out.println("守護線程運行中...");try {Thread.sleep(500);} catch (InterruptedException e) {e.printStackTrace();}}});daemonThread.setDaemon(true); daemonThread.start();try {Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println("main線程結束");}
}
main 線程(用戶線程)結束后,守護線程隨之停止,不會無限循環。
4. yield ():線程讓步
????????當前線程主動讓出 CPU 使用權,回到就緒狀態重新參與調度,僅為 “建議”,不保證生效,用于給同優先級線程更多執行機會。
?
public class YieldUsage {public static void main(String[] args) {Thread thread1 = new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println("線程1執行,i=" + i);Thread.yield(); }});Thread thread2 = new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println("線程2執行,i=" + i);}});thread1.start();thread2.start();}
}
線程 1 每次循環嘗試讓步,線程 2 可能更頻繁執行,但因調度不確定性,結果不絕對一致。
三、Java 線程的 6 種狀態:生命周期全解析
Java 線程狀態定義在Thread.State
枚舉中,共 6 種,理解狀態轉換對排查線程問題、優化并發邏輯至關重要。
(一)狀態枚舉與含義
- NEW(新建):線程對象已創建(如
new Thread()
?),但未調用start
,未啟動。- RUNNABLE(可運行):線程已啟動(
start
調用后),可能正在 CPU 執行,或在就緒隊列等待調度,也就是就緒狀態。- BLOCKED(阻塞):線程競爭
synchronized
鎖失敗,進入阻塞態,等待鎖釋放。- WAITING(等待):線程調用
Object.wait()
(無超時)、Thread.join()
(無超時)、LockSupport.park()
等,無時限等待喚醒。- TIMED_WAITING(計時等待):調用
Thread.sleep(long)
、Object.wait(long)
、Thread.join(long)
?、LockSupport.parkNanos/parkUntil
等,限時等待,超時自動喚醒。- TERMINATED(終止):線程執行完畢(
run
正常結束或拋未捕獲異常),生命周期結束。
(二)狀態轉換示例
通過代碼觀察線程從新建到終止的狀態變化:
public class ThreadStateAnalysis {public static void main(String[] args) throws InterruptedException {Thread thread = new Thread(() -> {try {Thread.sleep(1500); synchronized (ThreadStateAnalysis.class) {System.out.println("線程執行中,獲取鎖");}} catch (InterruptedException e) {e.printStackTrace();}});System.out.println("線程狀態(NEW):" + thread.getState()); thread.start();Thread.sleep(500); System.out.println("線程狀態(TIMED_WAITING):" + thread.getState()); Thread.sleep(1500); System.out.println("線程狀態(RUNNABLE/執行中):" + thread.getState()); thread.join(); System.out.println("線程狀態(TERMINATED):" + thread.getState()); }
}
?????????結合getState()
與線程執行邏輯,可清晰看到狀態NEW
→TIMED_WAITING
→RUNNABLE
→TERMINATED
的流轉。實際調試中,可借助 JConsole、VisualVM 等工具直觀分析復雜狀態切換。
四、線程組與線程優先級:管理與調度輔助
(一)線程組(ThreadGroup)
????????線程組用于批量管理線程,可統一設置優先級、捕獲未處理異常等。默認情況下,新建線程加入創建它的線程所在組(通常是main
線程組 )。
public class ThreadGroupManagement {public static void main(String[] args) {ThreadGroup customGroup = new ThreadGroup("自定義線程組");Thread thread1 = new Thread(customGroup, () -> {System.out.println("線程1所屬組:" + Thread.currentThread().getThreadGroup().getName());}, "線程1");Thread thread2 = new Thread(customGroup, () -> {System.out.println("線程2所屬組:" + Thread.currentThread().getThreadGroup().getName());}, "線程2");thread1.start();thread2.start();Thread[] threads = new Thread[customGroup.activeCount()];customGroup.enumerate(threads);for (Thread t : threads) {System.out.println("線程組線程:" + t.getName());}}
}
線程組輔助批量操作,但現代并發更依賴線程池,線程組使用場景逐漸減少,了解即可。
(二)線程優先級:調度 “建議”
????????線程優先級是調度器優先調度的 “建議”,范圍 1(最低)~10(最高),默認 5。優先級高的線程理論上獲取 CPU 時間片機會更多,但不保證執行順序(受操作系統調度策略影響 )。
public class ThreadPriorityControl {public static void main(String[] args) {Thread highPriority = new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println("高優先級線程執行,i=" + i);}});highPriority.setPriority(Thread.MAX_PRIORITY); Thread lowPriority = new Thread(() -> {for (int i = 0; i < 3; i++) {System.out.println("低優先級線程執行,i=" + i);}});lowPriority.setPriority(Thread.MIN_PRIORITY); highPriority.start();lowPriority.start();}
}
????????多次運行可能觀察到高優先級線程更 “活躍”,但因系統調度不確定性,不能完全依賴優先級控制執行順序,實際開發需謹慎使用。
五、進程與線程的區別:資源與執行單元
(一)核心差異對比
對比維度 |
|
| ||
資源分配單位 | 操作系統分配資源(內存、文件句柄等)的基本單位 | 進程內的執行單元,CPU 調度的基本單位 | ||
資源占用 | 獨立地址空間,資源消耗大 | 共享進程資源(內存、文件描述符等),消耗小 | ||
上下文切換成本 | 高(需切換地址空間、寄存器等) |
| ||
通信復雜度 | 進程間通信復雜(IPC:管道、socket 等) | 線程間通信簡單(共享內存) | ||
獨立性 | 進程間相互獨立,一個崩潰不影響其他進程 | 線程同屬進程,一個崩潰可能致進程崩潰 |
(二)通俗類比
以 “在線文檔編輯應用” 為例:
- 進程:整個應用是進程,操作系統為其分配獨立內存,存儲代碼、用戶數據等,是資源隔離的單位。
- 線程:拼寫檢查、自動保存、實時協作同步等功能,作為線程共享進程內存,協作完成任務。若拼寫檢查線程崩潰,可能導致整個應用(進程)異常,體現線程對進程的依賴。
?
六、synchronized 關鍵字:鎖的基礎與狀態體系
(一)鎖的基本認知:基于對象的鎖
????????Java 中每個對象均可作為鎖,常說的 “類鎖” 本質是Class
對象的鎖(Class
對象在 JVM 加載類時創建,唯一對應類元數據 )。通過synchronized
實現同步,保障多線程下共享資源的原子性、可見性。
(二)synchronized 的三種使用形式
1. 同步實例方法(鎖當前對象)
????????銀行賬戶(Account
)有一個withdraw
方法,一個人在不同設備上同時取錢,多線程可能同時取款,需保證余額正確。
代碼示例:
public class Account {private double balance;public Account(double balance) {this.balance = balance;}// 同步實例方法:鎖當前對象(this)public synchronized void withdraw(double amount) {if (balance >= amount) {try {Thread.sleep(100); // 模擬業務耗時} catch (InterruptedException e) {e.printStackTrace();}balance -= amount;System.out.println(Thread.currentThread().getName() + "取款成功,余額:" + balance);} else {System.out.println(Thread.currentThread().getName() + "取款失敗,余額不足");}}
}
- 鎖的是當前對象(
this
),若多個線程操作同一對象,會互斥。- 若多個線程操作不同對象,則互不影響(每個對象有獨立的鎖)。
2. 同步靜態方法(鎖類對象)
統計網站訪問量(靜態變量visitCount
),多線程并發訪問需保證計數正確。
代碼示例:
public class WebSite {private static int visitCount = 0;// 同步靜態方法:鎖類對象(WebSite.class)public static synchronized void incrementVisit() {visitCount++;System.out.println(Thread.currentThread().getName() + "訪問,總訪問量:" + visitCount);}
}
- 鎖的是類的
Class
對象(全局唯一),無論創建多少實例,所有線程都會互斥。- 適合保護靜態共享資源(如全局計數器、配置信息)。
3. 同步代碼塊
電商系統中,商品庫存(stock
)和訂單號生成器(orderIdGenerator
)需分別加鎖。
代碼示例:
public class ShoppingSystem {private int stock = 10;private static final Object STOCK_LOCK = new Object(); // 庫存鎖private static final Object ORDER_LOCK = new Object(); // 訂單號鎖private static int orderId = 0;// 扣減庫存public void reduceStock() {synchronized (STOCK_LOCK) { // 鎖庫存專用對象if (stock > 0) {stock--;System.out.println(Thread.currentThread().getName() + "扣減庫存成功,剩余:" + stock);} else {System.out.println(Thread.currentThread().getName() + "庫存不足");}}}// 生成訂單號public static void generateOrderId() {synchronized (ORDER_LOCK) { // 鎖訂單號專用對象orderId++;System.out.println(Thread.currentThread().getName() + "生成訂單號:" + orderId);}}
}
- 鎖對象可以是任意
Object
,推薦使用private static final
修飾,避免外部訪問。- 縮小鎖的范圍,提高并發性能(如只鎖需要保護的代碼,而非整個方法)。
結合場景更容易理解,注意理解,而非死記硬背?
(三)synchronized 的四種鎖狀態
????????JVM 對synchronized
鎖進行優化,存在四種狀態:偏向鎖、輕量級鎖、重量級鎖、無鎖,狀態隨競爭情況升級(不能降級,單向升級 )。
后面的內容我們在下一篇中講....