Java Socket「飢餓死鎖」問題
前言
Socket是指網路上端到端的通訊機制,一般基於傳輸層協議。本文以Java為例,專門討論TCP協議的Socket,UDP協議的Socket也可以參考。
飢餓-讀
考慮這樣一個情況:一個簡單的HTTP客戶端訪問伺服器資源的程式在運行了一段時間後,莫名的停住了;或者是一個簡單的JDBC查詢資料庫記錄的程式在運行了一段時間後,沒有動靜了。此時,dump下Java目前的執行狀態,基本上能看到如下程式碼片段:
Thread %d: (state = IN_NATIVE)
- java.net.SocketInputStream.socketRead0(java.io.FileDescriptor, byte[], int, int, int) @bci=0 (Compiled frame; information may be imprecise)
- java.net.SocketInputStream.read(byte[], int, int, int) @bci=87 (Compiled frame)
- java.net.SocketInputStream.read(byte[], int, int) @bci=11 (Compiled frame)
- java.net.SocketInputStream.read(byte[]) @bci=5 (Compiled frame)
- ... ... ...
多次dump發現,程序一直阻塞在socketRead0函式,也就是說程序已經「餓死」了。至於為什麼會「飢餓」到死,這個原因就複雜了,可能是服務端自己產生了死鎖,也可能是服務端執行異常,跳過了往客戶端傳送資料的程式碼。但是作為客戶端,讀不到資料可能不是最糟糕的,最糟糕的是阻塞導致其它程式碼不能執行。
這就需要打破這種阻塞狀態。Java的Socket API 提供了這個機制。
void setSoTimeout(int timeout)
Socket的setSoTimeout方法,用於設定讀取超時,單位是毫秒。預設情況下,值為0,即無限等待直到讀取資料。如果設定為一個合理的值,比如30000,則讀取超時為30秒,即30秒之後,如果資料還未到達,則丟擲一個超時異常。
HTTP和JDBC底層都是基於TCP的Socket機制實現,所以不管是HTTP客戶端,還是JDBC客戶端,一般都會提供這個引數的配置。
死鎖-寫
死鎖-寫有一個經典的場景。
Server端 ↓
public class Server {
public static void main (String[] args) throws Exception {
int count = 0;
ServerSocket ss = new ServerSocket(6060);
Socket s = ss.accept();
while (true) {
s.getOutputStream().write(new byte[1024 * 8]);
s.getInputStream().read();
System.out.println("server: " + ++count);
}
}
}
Client端 ↓
public class Client {
public static void main(String[] args) throws Exception {
int count = 0;
Socket s = new Socket("localhost", 6060);
while (true) {
s.getOutputStream().write(new byte[1024 * 8]);
s.getInputStream().read();
System.out.println("client: " + ++count);
}
}
}
兩個程式執行幾次後,就不再列印任何訊息了,即兩個程序處於死鎖狀態了。造成死鎖的原因自然是滿足了死鎖的四個必要條件:
- 互斥;Server端和Client端都既是生產者,也是消費者,互斥的使用兩個TCP緩衝佇列。
- 請求與保持;由於write方法每次寫8KB,而read方法每次只讀1B,所以兩個緩衝佇列都很快被填滿。此時,兩端都請求空的緩衝區,並保持著對方空緩衝的釋放權利。
- 不剝奪;這兩個程式請求資源是非搶佔式的,顯然不會被剝奪。
- 環路等待;Server端和Client端的程式都阻塞在write,互相等待對方釋放空的緩衝區。
解決死鎖的辦法也很簡單,將write和read分離在不同的執行緒,寫歸寫,讀歸讀,打破環路等待的條件。那樣雖然讀的比較慢,但死鎖是不存了。
將讀寫分離成不同執行緒時,需要特別注意:不要在Socket物件上加同步鎖。這樣跟不分離沒有實質上的區別。
結論
Socket程式設計一般都是多程序程式設計(服務端和客戶端),多執行緒程式設計(Socket傳送接收一般都是獨立執行緒)的綜合。所以,多程序、多執行緒的各種程式設計問題層出不窮,最突出的就是飢餓和死鎖,需要足夠多的程式碼來沉澱。