💡超詳細 | 如何喚醒被阻塞的 socket 線程?線程阻塞原理、線程池、fork/vfork徹底講明白!
- 一、什么是阻塞?為什么線程會阻塞?
- 二、socket線程被阻塞的典型場景
- 🧠 解法思路:
- 三、線程的幾種阻塞狀態和喚醒方式一覽
- 四、如何判斷線程是繁忙還是阻塞?
- 五、就緒狀態線程在等待什么?
- 六、如何實現線程池?
- 🏗 線程池原理:
- 💡 Java 中線程池核心類:
- 七、fork 和 vfork 的區別?
- 🧠 補充:寫時復制(COW)
- 八、進階:寫時復制(COW)原理
- 九、Server 阻塞狀態示意
- 🔚 總結
一、什么是阻塞?為什么線程會阻塞?
線程阻塞是一種等待某個事件發生的狀態。比如等待 I/O 完成、鎖釋放、條件滿足、子線程結束等。
常見導致線程阻塞的情況有:
阻塞方式 | 常見場景 | 喚醒方式 |
---|---|---|
Thread.sleep(ms) | 讓出 CPU 一段時間 | 時間到了自動喚醒 |
Object.wait() | 等待被 notify() | 被 notify() / notifyAll() 喚醒 |
Thread.join() | 主線程等待子線程結束 | 子線程執行完畢自動喚醒 |
LockSupport.park() | 顯式掛起線程 | 調用 unpark(Thread) 喚醒 |
socket accept() / read() | 阻塞等待客戶端連接/數據 | 客戶端連接/發送數據 |
synchronized 鎖競爭 | 等待鎖資源 | 鎖釋放后參與競爭 |
二、socket線程被阻塞的典型場景
舉個最常見的 ServerSocket 場景:
ServerSocket server = new ServerSocket(8888);
Socket client = server.accept(); // 這里會阻塞直到有客戶端連接
當執行 accept()
時,線程會阻塞等待客戶端連接,直到有連接進入,線程才會被喚醒繼續執行。
📌 問題:如果我想手動喚醒這個被阻塞的 accept()
怎么辦?
🧠 解法思路:
- 關閉 socket:關閉 ServerSocket,會拋出異常從
accept()
中跳出。 - 使用 selector/epoll 實現非阻塞,比如
NIO
。 - 新建連接觸發 accept() 返回:可以在本地建立一個 loopback 連接觸發
accept()
返回。
// 觸發喚醒 server.accept()
Socket socket = new Socket("localhost", 8888);
三、線程的幾種阻塞狀態和喚醒方式一覽
阻塞方式 | 描述 | 喚醒方式 | 示例 |
---|---|---|---|
Thread.sleep() | 睡眠,釋放 CPU,不釋放鎖 | 時間到 | Thread.sleep(1000) |
Object.wait() | 等待 notify,釋放鎖 | notify() /notifyAll() | obj.wait() |
Thread.join() | 等待其他線程結束 | 子線程結束 | t.join() |
Thread.suspend() (已棄用) | 掛起線程 | resume() | 慎用 |
LockSupport.park() | 顯式掛起 | unpark() | 常用于 AQS |
socket.accept() | 阻塞等待連接 | 有連接到來 / 被關閉 |
四、如何判斷線程是繁忙還是阻塞?
- Linux 可用
ps -e -o pid,state,cmd
查看線程狀態:
狀態碼 | 描述 |
---|---|
R | Running(可運行) |
S | Sleeping(可中斷阻塞) |
D | Uninterruptible sleep(不可中斷阻塞) |
Z | Zombie |
T | Stopped(暫停) |
👀 繁忙線程處于 Running 狀態
😴 阻塞線程常見為 Sleeping 或 D 狀態
五、就緒狀態線程在等待什么?
就緒(Runnable)狀態的線程,已經滿足了運行條件,但等待操作系統調度器為它分配 CPU 才能執行。它處在一個“搶票”的階段。
六、如何實現線程池?
線程池是一種線程復用機制,用于提高系統并發能力和資源利用率。基本思路如下:
🏗 線程池原理:
- 初始化 N 個工作線程,進入等待狀態
- 維護一個任務隊列(生產者-消費者模型)
- 有任務時喚醒線程執行,執行完畢后重新等待
- 沒任務時線程阻塞等待
💡 Java 中線程池核心類:
ExecutorService executor = Executors.newFixedThreadPool(5);
executor.submit(() -> doTask());
自己實現線程池的基本結構如下:
// 工作線程不斷從任務隊列中拉取任務
while (true) {Runnable task = taskQueue.take(); // 阻塞等待任務task.run();
}
七、fork 和 vfork 的區別?
特性 | fork() | vfork() |
---|---|---|
共享地址空間 | ? 否 | ? 是 |
復制頁表 | ? 復制(寫時復制) | ? 不復制 |
父子并發 | ? 并發 | ? 父進程阻塞等待子進程執行完 |
安全性 | 高(地址獨立) | 低(共享,容易出錯) |
速度 | 稍慢 | 更快(節省內存) |
🧠 補充:寫時復制(COW)
現代 fork()
實現采用 Copy-On-Write 技術,父子進程共享內存頁,只有在子進程寫入內存時才真正復制,提高效率。
八、進階:寫時復制(COW)原理
- fork 后父子共享內存頁,只讀
- 子進程試圖寫內存 → 觸發頁保護異常
- 內核復制對應頁,子進程寫副本
- 父進程繼續使用原來的頁
🌟 優點:節省時間和內存,尤其適用于 fork 后馬上執行 exec 的情況
九、Server 阻塞狀態示意
Client Server| ||---- connect() -----> | (accept 阻塞)| |====> 喚醒 accept()
- Server 端
accept()
無客戶端連接時處于阻塞狀態 - 被連接時喚醒進入 RUNNABLE
- 若使用
epoll
,Server 線程始終活躍,但非阻塞式等待事件
🔚 總結
本教程詳盡梳理了線程阻塞與喚醒機制,尤其是 socket 阻塞處理、線程池設計、fork/vfork 差異 等關鍵知識點。以下是學習建議:
- 掌握阻塞類型:主動、被動、鎖等待、IO 等
- 熟練使用 ps/top/jstack 等工具分析線程狀態
- 實踐線程池模型:自己寫一個簡易線程池
- 深入理解操作系統中的進程管理機制(fork, vfork, exec)