目錄
- 🚀前言
- 🤔什么是多線程?
- 💻創建線程
- 💯創建方法一:繼承Thread類
- 💯創建方法二:實現Runnable接口
- 💯創建方法三:實現Callable接口
- 💯三種方法對比
- 🦜Thread的常用方法
- ??線程安全與線程同步
- 💯先搞懂:什么是線程安全?(附比喻)
- 💯線程同步:給多線程定“排隊規則”
- 🎯方式1:同步代碼塊(synchronized塊)
- 🎯方式2:同步方法(synchronized方法)
- 🎯方式3:Lock鎖(顯式鎖)
- 💯三種方式對比總結
- 💯同步的“代價”
- 🌟線程池
- 💯認識線程池
- 💯創建線程池
- 💯處理Runnable任務
- 💯處理Callable任務
- 💯通過Executors創建線程池
- 💯線程數配置公式
- ??并發與并行
- 🌰 并發執行(單核CPU場景)
- 🌰 并行執行(多核CPU場景)
🚀前言
大家好!我是 EnigmaCoder。
- 在Java編程中,“多線程”是一個高頻出現的概念,也是處理并發任務的核心技術。如果你想理解程序如何“同時”處理多個任務(比如一邊下載文件一邊刷新界面),那多線程就是繞不開的知識點。今天我們就從多線程的定義、創建線程、線程安全與同步、線程池等多個維度,聊聊Java中的多線程。
🤔什么是多線程?
定義:多線程(Multithreading
)是指在一個程序(進程)中,同時運行多個獨立的執行單元(線程),這些線程共享程序的內存資源(如變量、方法),但擁有各自的執行路徑。
- 簡單說,線程是進程(一個正在運行的程序,比如你的Java程序、瀏覽器)中的“小任務”。一個進程至少有一個線程(稱為“主線程”),而多線程就是給一個進程“拆分”出多個并行的小任務,讓它們協同完成工作。
- 舉個生活例子:進程像一家餐廳,主線程是餐廳的“基礎運營”(開門、開燈);多線程就像餐廳里同時工作的服務員、廚師、收銀員——他們共享餐廳的資源(食材、餐具),但各自執行不同的任務,最終共同完成“服務顧客”的目標。
要理解多線程的價值,先得搞清楚它和“多進程”的區別:
- 多進程:多個獨立的程序同時運行(比如同時開著微信和瀏覽器),進程間內存不共享,通信成本高(需要通過網絡或文件等方式)。
- 多線程:同一個程序內的多個任務,共享內存(變量、對象等),通信成本低,且創建/切換線程的資源消耗遠低于進程。
優點:
-
避免阻塞,提升用戶體驗
比如一個Java桌面程序,如果用單線程(只有主線程),當執行一個耗時操作(如下載大文件)時,主線程會被“卡住”,界面會變成“無響應”狀態。而多線程可以把下載任務交給“子線程”,主線程繼續處理界面刷新,用戶完全感知不到卡頓。 -
利用多核CPU,提高效率
現代CPU都是多核的,單線程只能用一個核心,多線程可以讓不同線程跑在不同核心上,真正實現“并行計算”。比如處理大量數據時,多線程拆分任務后,效率可能提升數倍。 -
簡化復雜任務的拆分
有些任務天然適合拆分(比如同時處理100個用戶的請求),多線程可以讓每個請求對應一個線程,邏輯更清晰,無需手動協調任務順序。
缺點:
- 線程安全問題:多個線程共享資源時,可能出現“搶資源”的情況。比如兩個線程同時修改一個變量,可能導致結果錯亂(專業稱“競態條件”)。
- 復雜度提升:需要處理線程間的協調(如等待、喚醒),調試難度也更高(線程執行順序不確定)。
💻創建線程
💯創建方法一:繼承Thread類
實現步驟:
- 創建自定義線程類:繼承
java.lang.Thread
類,并重寫run()
方法 - 實例化線程:創建該自定義線程類的對象
- 啟動線程:調用線程對象的
start()
方法
代碼示例:
public class ThreadDemo1 {public static void main(String[] args) {Thread t1 =new MyThread();t1.start();for(int i=0;i<5;i++){System.out.println("主線程輸出:"+i);}}
}
class MyThread extends Thread {@Overridepublic void run (){for(int i=0;i<5;i++){System.out.println("子線程輸出:"+i);}}
}
注意事項:
- 調用
start()
方法后,JVM
會自動執行run()
方法中的邏輯 - 只有調用
start()
方法才是啟動一個新的線程執行,直接調用run()
方法將不會創建新線程,此時相當于還是單線程執行。 - 不要把主線程任務放在子線程之前,否則一定是主線程先跑完再跑完子線程。
優缺點:
- 優點:編碼簡單。
- 缺點:線程類已經繼承Thread,無法繼承其他類,不利于功能的擴展。
💯創建方法二:實現Runnable接口
實現步驟:
- 創建自定義線程類
MyRunnable
,實現Runnable
接口并重寫其run()
方法 - 實例化
MyRunnable
任務對象 - 將任務對象作為參數傳遞給
Thread
類構造函數 - 調用
Thread
實例的start()
方法啟動新線程
代碼示例:
public class ThreadDemo2 {public static void main(String[] args) {Runnable r =new MyRunnable();Thread t1 = new Thread(r);t1.start();for(int i=0;i<5;i++){System.out.println("主線程輸出:"+i);}}
}
class MyRunnable implements Runnable {@Overridepublic void run (){for(int i=0;i<5;i++){System.out.println("子線程輸出:"+i);}}
}
優缺點:
- 優點:任務類只是實現接口,可以繼續繼承其他類,實現其他接口,擴展性強。
- 缺點:需要多一個Runnable對象。
💯創建方法三:實現Callable接口
實現步驟:
-
創建任務對象:通過定義一個實現
Callable
接口的類,重寫其call方法來封裝業務邏輯和返回數據。然后將Callable
對象包裝成FutureTask
線程任務對象。 -
提交任務:將創建的
FutureTask
對象傳遞給Thread
對象進行執行。 -
啟動線程:調用
Thread
對象的start
方法啟動線程執行任務。 -
獲取結果:待線程執行完成后,通過調用
FutureTask
的get
方法獲取任務執行結果。
代碼示例
public class Test {public static void main(String[] args) {Callable<String> c1=new MyCallable(100);FutureTask<String> f1 =new FutureTask<>(c1);Thread t1 = new Thread(f1);t1.start();Callable<String> c2=new MyCallable(50);FutureTask<String> f2 =new FutureTask<>(c2);Thread t2 = new Thread(f2);t2.start();try {System.out.println(f1.get());} catch (Exception e) {e.printStackTrace();}try {System.out.println(f2.get());}catch(Exception e){e.printStackTrace();}}
}class MyCallable implements Callable<String> {private int n;public MyCallable(int n){this.n=n;}public String call() throws Exception{int sum=0;for(int i=1;i<=n;i++){sum+=i;}return "子線程計算1-"+n+"的和是:"+sum;}}
注意事項:
- 如果主線程發現某個線程還沒有執行完畢,會讓出CPU,等這個線程執行完畢后,再向下執行。
FutureTask的API
FutureTask提供的構造器 | 說明 |
---|---|
public FutureTask<>(Callable call) | 把Callable對象封裝成FutureTask對象 |
FutureTask提供的方法 | 說明 |
---|---|
public V get() throws Exception | 獲取線程執行call方法返回的結果 |
優缺點:
- 優點:線程任務類只是實現接口,可以繼續繼承類和實現接口,擴展性強。可以在線程執行完畢后獲取線程執行的結果。
- 缺點:編碼會更加復雜。
💯三種方法對比
方式 | 優點 | 缺點 |
---|---|---|
繼承Thread類 | 編程比較簡單,可以直接使用Thread類中的方法 | 擴展性較差,不能再繼承其他的類,不能返回線程執行的結果 |
實現Runnable接口 | 擴展性強,實現該接口的同時還可以繼承其他的類 | 編程相對復雜,不能返回線程執行的結果 |
實現Callable接口 | 擴展性強,實現該接口的同時還可以繼承其他的類。可以得到線程執行的結果 | 編程相對復雜 |
🦜Thread的常用方法
- Thread 常用方法
方法簽名 | 說明 |
---|---|
public void run() | 線程執行的任務邏輯(需重寫,定義線程要做的事) |
public void start() | 啟動線程(JVM 會自動調用 run 方法,真正開啟新線程執行) |
public String getName() | 獲取線程名稱(默認格式:Thread-索引 ,如 Thread-0 ) |
public void setName(String name) | 設置線程名稱(自定義線程標識) |
public static Thread currentThread() | 獲取當前執行的線程對象(區分主線程/子線程) |
public static void sleep(long time) | 讓當前線程休眠 time 毫秒(休眠后自動繼續執行) |
public final void join() | 讓當前線程等待調用者執行完畢(如主線程等子線程,需處理中斷異常) |
- Thread 常見構造器
構造器簽名 | 說明 |
---|---|
public Thread(String name) | 直接創建線程并指定名稱(適合繼承 Thread 類的場景) |
public Thread(Runnable target) | 封裝 Runnable 對象為線程(解耦任務與線程,推薦使用) |
public Thread(Runnable target, String name) | 封裝 Runnable 對象并指定線程名稱(靈活場景) |
- 綜合示例:Thread 方法+構造器全場景演示
// 1. 定義 Runnable 任務(解耦線程邏輯)
class MyRunnable implements Runnable {@Overridepublic void run() {// 獲取當前線程信息Thread current = Thread.currentThread();System.out.println(current.getName() + " 執行 Runnable 任務");try {// 休眠 2 秒(模擬耗時操作)Thread.sleep(2000);} catch (InterruptedException e) {e.printStackTrace();}System.out.println(current.getName() + " 休眠結束,任務完成");}
}// 2. 繼承 Thread 類(直接定義線程邏輯)
class MyThread extends Thread {public MyThread(String name) {super(name); // 調用父類構造器設置線程名稱}@Overridepublic void run() {Thread current = Thread.currentThread();System.out.println(current.getName() + " 執行 Thread 子類任務");try {Thread.sleep(1000); // 休眠 1 秒} catch (InterruptedException e) {e.printStackTrace();}System.out.println(current.getName() + " 休眠結束");}
}// 3. 主類:演示所有方法+構造器
public class ThreadDemo {public static void main(String[] args) {// ========== 構造器 1:Thread(String name) —— 繼承 Thread 類 ==========MyThread thread1 = new MyThread("子類線程");// ========== 構造器 2:Thread(Runnable target) —— 封裝 Runnable ==========Thread thread2 = new Thread(new MyRunnable()); // 名稱默認:Thread-1// ========== 構造器 3:Thread(Runnable target, String name) —— 自定義名稱 ==========Thread thread3 = new Thread(new MyRunnable(), "命名任務線程");// ========== 啟動線程(必須用 start(),直接調 run() 是普通方法!) ==========System.out.println("=== 啟動線程 ===");thread1.start();thread2.start();thread3.start();// ========== 主線程操作:getName()/setName() ==========Thread mainThread = Thread.currentThread();System.out.println("主線程原名稱:" + mainThread.getName()); // 默認:mainmainThread.setName("自定義主線程");System.out.println("主線程新名稱:" + mainThread.getName());// ========== 演示 join():主線程等待 thread1 完成 ==========try {System.out.println("主線程等待「子類線程」完成...");thread1.join(); // 主線程進入等待System.out.println("「子類線程」已完成,主線程繼續");} catch (InterruptedException e) {e.printStackTrace();}// ========== 演示 sleep():主線程休眠 3 秒 ==========try {System.out.println("主線程開始休眠 3 秒");Thread.sleep(3000);System.out.println("主線程休眠結束");} catch (InterruptedException e) {e.printStackTrace();}// ========== 對比:直接調用 run()(非線程啟動!) ==========System.out.println("=== 直接調用 run()(無新線程)===");MyRunnable runnable = new MyRunnable();runnable.run(); // 在主線程中執行,不會開啟新線程}
}
- 示例運行效果 :
- 線程啟動:
thread1
(子類線程)、thread2
(默認名)、thread3
(命名任務線程)通過start()
啟動,各自開啟新線程執行run()
。
- 方法調用:
getName()
/setName()
:主線程名稱從main
改為自定義主線程
。join()
:主線程等待thread1
完成后再繼續。sleep()
:主線程休眠 3 秒,模擬耗時操作。
- 關鍵對比:
start()
啟動新線程,run()
直接調用僅為普通方法(無新線程)。
- 小結
場景 | 正確用法 | 常見誤區 |
---|---|---|
啟動線程 | thread.start() | 直接調用 thread.run() (無新線程) |
定義線程邏輯 | 實現 Runnable (解耦優先) | 過度使用繼承 Thread (單繼承限制) |
線程命名 | 構造器指定或 setName() | 依賴默認名稱(不利于調試) |
線程等待 | thread.join() | 忽略中斷異常處理 |
??線程安全與線程同步
上面我們聊了多線程的基礎,知道它能讓程序“一心多用”。但如果多個線程同時搶著用同一個資源,就可能出亂子——這就是“線程安全”問題。而“線程同步”就是給多線程定規矩,讓它們有序訪問資源。下面咱們用生活化的例子,聊聊這兩個概念和三種同步方式。
💯先搞懂:什么是線程安全?(附比喻)
線程安全:多個線程同時操作共享資源時,無論線程執行順序如何,最終結果都和“單線程執行”的結果一致,就叫線程安全。反之,結果錯亂就是“線程不安全”。
舉個最直觀的例子:
假設你和3
個朋友(4
個線程)一起搶10
張演唱會門票(共享資源),每個人都在同時喊“我要1
張”。如果沒有規則,可能出現:
- 最后統計時,明明只有10張票,卻被搶走了
11
張(超賣); - 或者有人喊了“要
1
張”,但票沒減少(漏賣)。
這就是典型的“線程不安全”——共享資源被多線程亂搶,結果錯亂。
為什么會這樣?
因為線程操作資源的過程(比如“判斷剩余票數→減少1張”)不是“一步完成”的,而是分成幾步(讀、改、寫)。多個線程可能在“讀”之后、“寫”之前插隊,導致數據錯亂。
💯線程同步:給多線程定“排隊規則”
線程同步的核心是:讓多個線程“有序訪問”共享資源,避免同時操作。就像給搶票的人定規則:“每次只能一個人去查票、買票,其他人排隊等”。
同步的本質是“加鎖”:把共享資源的操作過程“鎖住”,一個線程操作時,其他線程必須等它完成并“解鎖”后才能繼續。
接下來講三種最常用的同步方式:
🎯方式1:同步代碼塊(synchronized塊)
定義:用synchronized(鎖對象)
包裹需要同步的代碼,只有拿到“鎖對象”的線程才能執行塊內代碼,執行完自動釋放鎖。
synchronized(同步鎖){訪問共享資源的核心代碼
}
比喻:
就像食堂打飯,勺子(鎖對象)只有一個。大家想打飯(執行代碼塊),必須先拿到勺子(獲得鎖),打完飯(執行完)把勺子放回(釋放鎖),下一個人才能拿。
代碼示例:解決搶票問題
public class Ticket implements Runnable {private int ticketCount = 10; // 共享的10張票private Object lock = new Object(); // 鎖對象(任意對象都可)@Overridepublic void run() {while (true) {// 同步代碼塊:鎖住"查票+賣票"的核心操作synchronized (lock) { if (ticketCount > 0) {// 模擬網絡延遲(放大線程安全問題)try { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "賣出1張,剩余:" + (--ticketCount));} else {break;}}}}public static void main(String[] args) {Ticket ticket = new Ticket();// 4個線程(4個人)搶票new Thread(ticket, "線程1").start();new Thread(ticket, "線程2").start();new Thread(ticket, "線程3").start();new Thread(ticket, "線程4").start();}
}
說明:
lock
是鎖對象,必須是多個線程“共享”的同一個對象(否則鎖不住)。- 同步代碼塊只鎖“必要的代碼”(查票+賣票),范圍越小,效率越高(別把整個循環鎖住,不然和單線程沒區別)。
注意事項:
- 對于實例方法建議使用
this
作為鎖對象。 - 對于靜態方法建議使用字節碼(
類名.class
)對象作為鎖對象。
🎯方式2:同步方法(synchronized方法)
定義:在方法聲明處加synchronized
關鍵字,整個方法成為同步方法。
- 非靜態同步方法:鎖對象是
this
(當前對象)。 - 靜態同步方法:鎖對象是當前類的
Class
對象(類名.class)。
修飾符 synchronized 返回值類型 方法名稱(形參列表){操作共享資源的代碼
}
比喻:
就像公共電話亭,電話亭(同步方法)本身就是“鎖”。一個人進去打電話(執行方法),會把門反鎖(獲得鎖),打完電話出來(方法結束)才開鎖,下一個人才能進。
代碼示例:用同步方法解決搶票問題
public class Ticket implements Runnable {private int ticketCount = 10;// 同步方法:整個方法被鎖住,鎖對象是this(當前Ticket對象)private synchronized void sellTicket() {if (ticketCount > 0) {try { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "賣出1張,剩余:" + (--ticketCount));}}@Overridepublic void run() {while (ticketCount > 0) {sellTicket(); // 調用同步方法}}public static void main(String[] args) {Ticket ticket = new Ticket();new Thread(ticket, "線程1").start();new Thread(ticket, "線程2").start();new Thread(ticket, "線程3").start();new Thread(ticket, "線程4").start();}
}
說明:
同步方法比同步代碼塊更簡潔,直接把整個方法設為同步。但要注意:如果方法里有不需要同步的代碼,會降低效率(相當于整個電話亭都排隊,哪怕只是進去拿個東西)。
🎯方式3:Lock鎖(顯式鎖)
定義:JDK 5后新增的java.util.concurrent.locks.Lock
接口(常用實現類ReentrantLock
),需手動調用lock()
加鎖、unlock()
釋放鎖,更靈活。
比喻:
就像租共享單車,你需要手動掃碼開鎖(lock()
),用完后手動關鎖(unlock()
)。比同步代碼塊/方法更靈活(比如可以中途解鎖)。
代碼示例:用Lock解決搶票問題
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;public class Ticket implements Runnable {private int ticketCount = 10;// 創建Lock鎖對象private Lock lock = new ReentrantLock();@Overridepublic void run() {while (true) {lock.lock(); // 加鎖try {if (ticketCount > 0) {try { Thread.sleep(100); } catch (InterruptedException e) {}System.out.println(Thread.currentThread().getName() + "賣出1張,剩余:" + (--ticketCount));} else {break;}} finally {lock.unlock(); // 釋放鎖(必須放finally里,確保一定釋放)}}}public static void main(String[] args) {Ticket ticket = new Ticket();new Thread(ticket, "線程1").start();new Thread(ticket, "線程2").start();new Thread(ticket, "線程3").start();new Thread(ticket, "線程4").start();}
}
說明:
lock()
和unlock()
必須成對出現,unlock()
放finally
里,避免線程異常時鎖沒釋放,導致死鎖。- 比
synchronized
更靈活:支持嘗試獲取鎖(tryLock()
)、可中斷鎖等,適合復雜場景。
💯三種方式對比總結
方式 | 語法 | 鎖釋放 | 靈活性 | 適用場景 |
---|---|---|---|---|
同步代碼塊 | synchronized(鎖對象) | 自動釋放 | 中等(指定鎖) | 部分代碼需要同步時 |
同步方法 | synchronized 修飾方法 | 自動釋放 | 低(鎖固定) | 整個方法需要同步時 |
Lock鎖 | lock() +unlock() | 手動釋放 | 高(靈活控制) | 復雜同步場景(如嘗試鎖、超時) |
💯同步的“代價”
同步能解決線程安全問題,但也有成本:
- 線程需要排隊等待鎖,會降低并發效率(就像大家都排隊打飯,速度肯定比各打各的慢)。
- 過度同步可能導致“死鎖”(比如線程A拿著鎖1等鎖2,線程B拿著鎖2等鎖1,互相卡死)。
所以,同步不是“越多越好”,而是“按需使用”:只給真正需要同步的代碼加鎖,平衡安全性和效率。
🌟線程池
在多線程編程里,線程池 是提升效率的關鍵武器。它像一家“勞務公司”——提前養一批線程(工人)待命,任務(活)來了直接分配,避免頻繁招人(創建線程)、裁人(銷毀線程)的資源浪費。
💯認識線程池
- 生活場景類比
假設你開了家餐廳:
- 不用線程池:顧客點餐時現招服務員(創建線程),點完餐辭退(銷毀線程)。高峰期頻繁招人→效率低、資源浪費。
- 用線程池:提前培訓3個固定服務員(核心線程),再備2個臨時工(最大線程擴展),顧客排隊(任務隊列)。任務來了直接分配,用完回池待命→效率翻倍!
- 技術核心價值
線程池通過 “線程復用、隊列緩沖、拒絕策略” 解決三大問題:
問題 | 線程池如何解決? |
---|---|
線程創建銷毀開銷大 | 提前創建線程,復用現有線程 |
線程數失控(OOM) | 限制最大線程數,任務排隊緩沖 |
任務突發無預案 | 配置拒絕策略(任務爆倉時如何處理) |
💯創建線程池
線程池的“靈魂類”是 ThreadPoolExecutor
,像給“勞務公司”定規則:招多少固定工人、最多擴多少臨時工、任務咋排隊…
- 構造參數解析(類比餐廳管理)
創建線程池時,需設置7個核心參數,每個參數對應餐廳運營規則:
參數名 | 作用(技術解釋) | 餐廳類比 |
---|---|---|
corePoolSize | 核心線程數(一直保留的線程) | 固定員工數(3個長期服務員) |
maximumPoolSize | 最大線程數(核心+臨時工總數) | 最多雇5人(3固定+2臨時) |
keepAliveTime | 臨時工空閑超時時間 | 臨時工沒事做→30秒后辭退 |
TimeUnit | 時間單位(秒/分等) | 時間標準(秒) |
workQueue | 任務隊列(存放待處理任務) | 顧客排隊區(最多排3人) |
threadFactory | 線程工廠(如何創建線程) | 招聘流程(統一培訓工人) |
RejectedExecutionHandler | 拒絕策略(任務滿時如何處理新任務) | 排隊滿了→拒絕新顧客 |
- 任務拒絕策略:四種應急預案
當線程池忙到極限(核心線程+臨時工都在干活,隊列也排滿)時,新任務怎么處理?這就需要 拒絕策略——相當于“餐廳排隊滿了,如何應對新顧客”。
策略類 | 說明(餐廳類比) | 代碼示例效果 |
---|---|---|
AbortPolicy (默認) | 直接拒絕,拋 RejectedExecutionException 異常 | 新顧客被拒,餐廳拋“無法接待”異常 |
DiscardPolicy | 默默丟棄任務,不拋異常 | 新顧客被無視,餐廳繼續忙 |
DiscardOldestPolicy | 丟棄隊列中最久的任務,把新任務加入隊列 | 趕走最早排隊的顧客,讓新顧客進隊 |
CallerRunsPolicy | 讓提交任務的線程(主線程)自己執行任務 | 老板親自幫新顧客點餐(主線程執行) |
- 代碼示例:拒絕策略實戰(處理Runnable任務)
模擬“餐廳忙到爆”場景:核心3線程、隊列3任務、最多5線程→當提交第9個任務時觸發拒絕策略。
import java.util.concurrent.*;public class RejectPolicyDemo {public static void main(String[] args) {// 測試四種拒絕策略(解開注釋切換)testRejectPolicy(new ThreadPoolExecutor.AbortPolicy());// testRejectPolicy(new ThreadPoolExecutor.DiscardPolicy());// testRejectPolicy(new ThreadPoolExecutor.DiscardOldestPolicy());// testRejectPolicy(new ThreadPoolExecutor.CallerRunsPolicy());}private static void testRejectPolicy(RejectedExecutionHandler policy) {System.out.println("===== 測試策略:" + policy.getClass().getSimpleName() + " =====");ThreadPoolExecutor pool = new ThreadPoolExecutor(3, // 核心3人5, // 最多5人30, TimeUnit.SECONDS,new ArrayBlockingQueue<>(3), // 隊列最多3個任務Executors.defaultThreadFactory(),policy // 設置拒絕策略);// 提交9個任務(3核心+3隊列+2臨時=8 → 第9個觸發拒絕)for (int i = 1; i <= 9; i++) {final int taskId = i;try {pool.execute(() -> { // 執行Runnable任務System.out.println(Thread.currentThread().getName() + " 處理任務:" + taskId);try { Thread.sleep(1000); } catch (InterruptedException e) {}});} catch (RejectedExecutionException e) {System.out.println("任務 " + taskId + " 被拒絕!策略:" + policy.getClass().getSimpleName());}}pool.shutdown();}
}
- 任務執行流程
提交任務時,線程池按以下邏輯處理(對應餐廳流程):
- 核心線程先干活:3個核心線程空閑→直接分配任務。
- 核心忙→任務排隊:核心線程占滿→任務進隊列(最多3個)。
- 隊列滿→招臨時工:隊列排滿→創建臨時線程(最多擴到5個)。
- 全忙+隊列滿→觸發拒絕策略:臨時工也占滿→按配置的策略拒絕新任務。
💯處理Runnable任務
Runnable
是“無返回值任務”的代表,用 execute()
方法提交給線程池。
- 代碼示例:提交Runnable任務
// 定義一個Runnable任務(像“點餐任務”)
class OrderTask implements Runnable {private int taskId;public OrderTask(int taskId) { this.taskId = taskId; }@Overridepublic void run() {System.out.println(Thread.currentThread().getName() + " 處理訂單:" + taskId);try { Thread.sleep(1000); } catch (InterruptedException e) {}}
}// 提交任務到線程池
public static void main(String[] args) {ThreadPoolExecutor pool = ...; // 同前創建的線程池(可配置拒絕策略)for (int i = 1; i <= 5; i++) {pool.execute(new OrderTask(i)); // 執行Runnable任務}pool.shutdown();
}
- 方法總結
方法 | 作用 | 適用任務類型 |
---|---|---|
execute(Runnable) | 提交無返回值任務 | Runnable |
💯處理Callable任務
Callable
是“有返回值任務”的代表,用 submit()
提交,通過 Future
獲取結果。
- 代碼示例:計算1~100的和(帶返回值)
import java.util.concurrent.*;public class CallableDemo {public static void main(String[] args) throws Exception {// 1. 創建線程池(復用之前的配置,可含拒絕策略)ThreadPoolExecutor pool = new ThreadPoolExecutor(3, 5, 30, TimeUnit.SECONDS,new ArrayBlockingQueue<>(3),Executors.defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy());// 2. 定義Callable任務(計算1~100的和)Callable<Integer> sumTask = () -> {int sum = 0;for (int i = 1; i <= 100; i++) sum += i;return sum; // 返回結果};// 3. 提交任務,獲取Future(結果的“憑證”)Future<Integer> future = pool.submit(sumTask);// 4. 獲取結果(任務未完成時,get()會阻塞等待)System.out.println("計算結果:" + future.get()); // 輸出5050// 5. 關閉線程池pool.shutdown();}
}
- 方法總結
方法 | 作用 | 適用任務類型 |
---|---|---|
submit(Callable<T>) | 提交有返回值任務,返回 Future<T> | Callable |
Future.get() | 獲取任務結果(阻塞等待或超時等待) | - |
💯通過Executors創建線程池
Executors
是線程池“工具類”,像“快捷模板”,但不適合生產環境(阿里開發手冊強制禁用!)。
- 常用快捷方法
方法名 | 特點(技術解釋) | 餐廳類比 | 潛在風險 |
---|---|---|---|
newFixedThreadPool(3) | 固定3個核心線程,隊列無界 | 固定3個服務員,排隊不限 | 任務堆積→內存溢出(OOM) |
newSingleThreadExecutor() | 單線程,隊列無界 | 只有1個服務員,排隊不限 | 同上(隊列無界) |
newCachedThreadPool() | 線程數彈性擴容(最多 Integer.MAX_VALUE ) | 無限招臨時工 | 線程數爆炸→OOM |
- 代碼示例:FixedThreadPool
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;public class ExecutorsDemo {public static void main(String[] args) {// 快捷創建:固定3個線程的線程池ExecutorService pool = Executors.newFixedThreadPool(3);// 提交任務(用法和 `ThreadPoolExecutor` 一致)for (int i = 1; i <= 5; i++) {final int taskId = i;pool.execute(() -> {System.out.println(Thread.currentThread().getName() + " 處理任務:" + taskId);try { Thread.sleep(1000); } catch (InterruptedException e) {}});}pool.shutdown();}
}
- 為什么“不推薦”?
看 阿里巴巴Java開發手冊 強制要求:
線程池不允許用
Executors
創建,必須用ThreadPoolExecutor
!
原因:
FixedThreadPool
/SingleThreadExecutor
:隊列是Integer.MAX_VALUE
(無限排隊),任務堆積會撐爆內存(OOM)。CachedThreadPool
:線程數上限是Integer.MAX_VALUE
(無限招人),線程太多也會OOM。
💯線程數配置公式
線程池的核心/最大線程數配置,必須根據任務類型調整,否則會嚴重影響效率。先理解兩種任務類型:
- 任務類型定義
類型 | 特點(餐廳類比) | 示例任務 |
---|---|---|
CPU密集型 | 任務瘋狂“占用CPU”(如計算、加密),線程幾乎不空閑 | 復雜數學運算、圖片壓縮 |
IO密集型 | 任務大部分時間“等IO”(如讀寫文件、網絡請求),CPU空閑 | 數據庫查詢、文件上傳、接口調用 |
- 配置公式(基于CPU核心數
N
)
線程池的核心思路:讓CPU盡可能不空閑,同時避免線程過多導致切換開銷。
任務類型 | 配置公式 | 原理說明 |
---|---|---|
CPU密集型 | 核心線程數 = N + 1 | 避免線程等待時CPU完全空閑,+1應對偶爾阻塞 |
IO密集型 | 核心線程數 = N * 2 (或 N * 5 等) | 利用IO等待的空閑時間,多線程并行處理任務 |
- 代碼示例:根據任務類型配置線程池
import java.util.concurrent.*;public class TaskTypeConfig {public static void main(String[] args) {int cpuCore = Runtime.getRuntime().availableProcessors(); // 獲取CPU核心數System.out.println("CPU核心數:" + cpuCore);// 1. CPU密集型任務:核心數 = cpuCore + 1ThreadPoolExecutor cpuPool = new ThreadPoolExecutor(cpuCore + 1, cpuCore + 1, // 最大線程數同核心(CPU沒空,無需臨時工)30, TimeUnit.SECONDS,new ArrayBlockingQueue<>(100));// 2. IO密集型任務:核心數 = cpuCore * 2ThreadPoolExecutor ioPool = new ThreadPoolExecutor(cpuCore * 2, cpuCore * 4, // 最大線程數適當擴展(應對突發IO任務)30, TimeUnit.SECONDS,new ArrayBlockingQueue<>(100));// 提交任務(按類型分配)cpuPool.execute(() -> heavyCalculation()); // CPU密集型ioPool.execute(() -> fetchData()); // IO密集型}// CPU密集型任務:瘋狂計算private static void heavyCalculation() {long result = 0;for (long i = 0; i < 1000000000L; i++) {result += i;}System.out.println("計算結果:" + result);}// IO密集型任務:模擬網絡請求private static void fetchData() {try {Thread.sleep(1000); // 模擬IO等待System.out.println("網絡數據獲取完成");} catch (InterruptedException e) {}}
}
- 小結:線程池配置實戰指南
場景 | 任務類型 | 核心線程數公式 | 拒絕策略建議 | 典型案例 |
---|---|---|---|---|
計算密集(如加密) | CPU密集型 | CPU核心數 + 1 | AbortPolicy (拋異常提醒) | 大數據排序、視頻編碼 |
網絡/IO 密集(如接口調用) | IO密集型 | CPU核心數 * 2 | CallerRunsPolicy (降級執行) | 微服務調用、文件上傳 |
??并發與并行
在多線程的世界里,并發和并行是經常被混淆的概念,但它們的本質區別直接影響程序的效率。
1.定義:宏觀與微觀的區別
簡單說,兩者的核心差異在于 “任務是否真正同時執行”,用表格對比更清晰:
概念 | 技術解釋(CPU視角) | 生活化比喻 |
---|---|---|
并發 | 多個任務交替執行(單核CPU也能實現) | 餐廳1個服務員交替招呼3桌客人 |
并行 | 多個任務同時執行(需要多核CPU支持) | 餐廳3個服務員同時招呼3桌客人 |
- 直觀示例:處理任務的兩種方式
假設你需要完成3件事:
- 任務A:煮咖啡(需5分鐘,大部分時間等水開,CPU空閑)
- 任務B:煎雞蛋(需3分鐘,全程占用CPU,不能停)
- 任務C:烤面包(需4分鐘,全程占用CPU,不能停)
🌰 并發執行(單核CPU場景)
像1個服務員處理3桌客人:
- 啟動任務A(放水煮咖啡)→ 水開前CPU空閑,切換到任務B煎雞蛋(3分鐘全程占用CPU)→
- 任務B完成后,切換到任務C烤面包(4分鐘全程占用CPU)→
- 最后回到任務A,咖啡煮好收尾。
特點:
- 宏觀上:3個任務“同時進行”(你感覺在同步推進);
- 微觀上:CPU核心在任務間快速交替切換,實際同一時間只做一件事。
🌰 并行執行(多核CPU場景)
像3個服務員同時開工:
- 服務員1負責任務A(煮咖啡,等水開時CPU自動空閑)→
- 服務員2負責任務B(煎雞蛋,全程占用1個CPU核心)→
- 服務員3負責任務C(烤面包,全程占用另1個CPU核心)。
特點:
- 宏觀+微觀上:3個任務真正同時執行,效率翻倍(前提是CPU有多個核心)。
- Java多線程中的體現
- 單核CPU:無論創建多少線程,線程池里的任務都只能并發執行(線程交替占用唯一的CPU核心)。
- 多核CPU:線程池中的多個線程可以并行執行(不同核心同時跑任務),剩余線程繼續并發交替(充分利用多核+等待時間)。
- 小結
理解這兩個概念,才能設計出高效的多線程方案:
維度 | 并發(Concurrency) | 并行(Parallelism) |
---|---|---|
執行本質 | 交替執行(宏觀同時,微觀交替) | 同時執行(宏觀+微觀都同時) |
CPU依賴 | 單核即可實現 | 必須多核支持 |
典型場景 | IO密集型任務(利用等待時間) | CPU密集型任務(榨干多核性能) |
實戰邏輯:
- 處理網絡請求、文件讀寫等IO密集型任務→ 用并發讓線程交替利用等待時間(線程池配多線程,哪怕單核也能高效);
- 處理加密計算、大數據排序等CPU密集型任務→ 用并行讓多核同時開工(線程池核心數貼近CPU核心數,避免切換開銷)。
這就是線程池配置要區分任務類型的底層邏輯——并發與并行的協同,才是多線程的最大價值!