1. 程式人生 > >第2章 NIO入門

第2章 NIO入門

復用 失去 lock span output jstack string run cat

2.1 傳統的BIO編程

  以服務器為例,在傳統BIO模型下的服務器,每當一個新的請求到來的時候回分配一個線程去處理該請求,並且該線程在執行IO操作的時候會一直阻塞,知道IO操作完成或拋出異常才會返回。當網絡情況不佳時,網絡IO可能會耗費大量時間,那麽就會同時有大量線程在服務器上阻塞著,很容易造成內存溢出。

  這種模型被稱為同步阻塞模型,同步指的是只有等待線程IO操作完成該線程才會返回,阻塞指的是IO沒有完成的時候一直等待。

  技術分享圖片

2.1.1 服務端代碼

  Server類,監聽8080端口,在while循環裏Server端阻塞在server.accept上,即等待請求傳到8080端口上,從accept方法返回。後續new Thread是新建一個線程去處理請求。

public class TimeServer {
public static void main(String[] args) throws IOException {
int port = 8080;
ServerSocket server = null;

try {
server = new ServerSocket(port);
System.out.println("The server start in port "+port);
Socket socket = null;
while (true){
socket = server.accept();
Thread thread = new Thread(new TimeServerHandler(socket));
thread.start();
}


} catch (IOException e) {
e.printStackTrace();
} finally {
if (server!=null){
System.out.println("Time server close");
server.close();
server=null;
}
}
}
}

  執行代碼用jstack打印線程狀態,看到server線程在17行“停止”,即阻塞在17行等待請求傳過來。這種阻塞就是BIO裏的B,block,一個IO沒有完成就一直卡在那裏。

技術分享圖片

  有新的客戶端接入新建線程執行處理方法,通過檢查傳過來的字符串是否是要求的“QUERY TIME ORDER”,如果是就返回當前服務器的時間,否則返回錯誤信息。

public class TimeServerHandler implements Runnable {
    private Socket socket;

    public TimeServerHandler(Socket socket) {
        this.socket = socket;
    }

    public void run() {
        BufferedReader in = null;
        PrintWriter out = null;

        try {
            in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
            out = new PrintWriter(socket.getOutputStream(),true);
            String currentTime = null;
            String body = null;
            while (true){
                body = in.readLine();
                if (body == null){
                    break;
                }
                System.out.println("The time server receive order: "+body);
                currentTime = "QUERY TIME ORDER".equalsIgnoreCase(body)?new Date(System.currentTimeMillis()).toString():"BAD ORDER";
                out.println(currentTime);
            }
        } catch (IOException e) {
            e.printStackTrace();
            if (in!=null){
                try {
                    in.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }
            }
            if (out!=null){
                out.close();
                out = null;
            }

            if (socket!=null){
                try {
                    socket.close();
                } catch (IOException ex) {
                    ex.printStackTrace();
                }finally {
                    socket=null;
                }
            }
        }
    }
}

  同時開啟服務端和客戶端後可以在控制臺看到輸出。

2.1.2 缺點

  • 每個請求都需要一個新建線程去處理,扛不住太大的並發,因為沒有給線程數目設置瓶頸。
  • BIO會在網絡不佳情況導致大量線程。

2.2 偽異步IO編程

2.2.1 代碼

  用線程池代替不停的新建線程,好處是線程池是有界的,避免在極端情況下不停新建線程。

public class TimeServer_ThreadPool {
    public static void main(String[] args) {
        int port = 8080;
        ServerSocket server = null;
        try {
            server = new ServerSocket(port);
            System.out.println("server start at port "+ port);
            Socket socket = null;
            ExecutorService pool = Executors.newFixedThreadPool(10);
            while (true){
                socket = server.accept();
                pool.execute(new TimeServerHandler(socket));
            }
            
        } catch (IOException e) {
            e.printStackTrace();
        }
    }
}

2.2.2 弊端

  • 使用BIO讀取數據時,線程會一直阻塞直到 1、有數據讀 2、數據讀取完畢 3、拋出異常。當客戶端請求發送較慢或者網絡時延較時,讀取數據的線程會一直阻塞。
  • 使用BIO輸出數據時,線程會一直阻塞直到 1、有數據寫 2、數據寫完 3、拋出異常。當服務端寫數據時,如果網絡情況不佳,客戶端不能及時讀取數據,大量數據留在TCP緩沖區,當發送端即服務端的Window size為0的時候寫線程就會無法繼續寫從而阻塞。
  • 無論讀還是寫,都是阻塞的,阻塞與否以及阻塞是否嚴重依賴於網絡傳輸的質量。
  • 當線程池的隊列使用阻塞隊列時,前臺線程負責把請求封裝成對象加入線程池的阻塞隊列,如果網絡狀況十分的差,阻塞隊列也滿了,那麽復制把請求對象加入阻塞隊列的前臺線程也會阻塞,整個系統失去異步性,所有的請求都會超時。

  

2.3 NIO 編程

  NIO與BIO的區別在兩點

  • 面向的對象不同。BIO面向Stream,該Stream是單向通信,只能是讀Stream或者寫Stream。NIO面向Buffer,NIO面向buffer和channel,channel是鐵路,buffer是鐵路上運輸的數據。
  • 阻塞性。BIO是阻塞的,NIO通過Selector實現多路復用。

2.3.1 Buffer與Channel

  

第2章 NIO入門