在前例中,客戶端發送 byebye 關閉流後,服務端調用 socket.close() 釋放資源。但若客戶端異常斷網、進程崩潰、強制殺進程,流關閉而 Socket 未關閉,長期積累導致服務端「Too many open files」崩潰。
設計 ServerScanThread 守護線程,維護一個 List<Socket>,定期檢查 socket.isInputShutdown() && socket.isOutputShutdown(),雙流均關閉則調用 socket.close() 並從列表移除。
public class ServerScanThread extends Thread {
public static List<Socket> socketList = new LinkedList<>();
public ServerScanThread() {
setDaemon(true); // 守護線程,主線程退出自動終了
start();
}
@Override
public void run() {
while (true) {
// 1. 空列表休眠 10s
while (socketList == null || socketList.isEmpty()) {
try { Thread.sleep(10_000); } catch (InterruptedException ignored) {}
}
// 2. 掃描列表
for (int i = 0; i < socketList.size(); i++) {
Socket socket = socketList.get(i);
if (socket.isInputShutdown() && socket.isOutputShutdown()) {
try {
socket.close();
socketList.remove(i--); // LinkedList 移除後索引前移,i-- 防止漏掃
System.out.println("已清理 1 個殭屍 Socket");
} catch (IOException e) {
e.printStackTrace();
}
}
}
// 3. 掃描間隔 5s
try { Thread.sleep(5_000); } catch (InterruptedException ignored) {}
}
}
}
public class Server1 {
public static void main(String[] args) throws IOException {
ServerSocket server = new ServerSocket(9880);
new ServerScanThread(); // 啟動掃描線程
while (true) {
Socket client = server.accept();
ServerScanThread.socketList.add(client); // 納入管理
System.out.println("客戶端接入: " + client.getInetAddress());
// 業務處理線程
new WriterThread("服務端", client).start();
new ReaderThread("服務端", client).start();
}
}
}
| 場景 | 掃描線程行為 | 控制台輸出 |
|---|---|---|
| 無客戶端 | 外層 while 休眠 10s 一次打印 | 這會兒集合裡沒有socket,我先睡覺10s |
| 客戶端連接但未關閉流 | 進入 for 循環掃描,條件不滿足 | 無額外輸出,CPU 低佔用 |
| 客戶端雙向關閉流 | 檢測到雙流關閉,執行 close() 並移除 | 有一個 socket 被關閉了 |



Selector 事件驅動模型天然解決大量連接管理問題,無需輪詢。HikariCP 等成熟組件,統一管理資源獲取釋放。ulimit -n、開啟 tcp_tw_reuse、tcp_tw_recycle(內核參數謹慎調優)。守護線程掃描是 BIO 模型下的務實補丁,理解其原理有助於遷移到 NIO/Netty 時做出正確架構決策。