1. 程式人生 > >Java Socket「飢餓死鎖」問題

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傳送接收一般都是獨立執行緒)的綜合。所以,多程序、多執行緒的各種程式設計問題層出不窮,最突出的就是飢餓和死鎖,需要足夠多的程式碼來沉澱。