摘自:http://www.cnblogs.com/zhuYears/archive/2012/09/28/2690194.html
看着感覺有點意思,挺有道理,就粘過來了;
前提
首先先強調上下文:下面提到了同步與異步、阻塞與非阻塞的概念都是在IO的場合下。它們在其它場合下有着不同的含義,比如作業系統中,通信技術上。
然後借鑒下《Unix網絡程式設計卷》中的理論:
IO操作中涉及的2個主要對象為程式程序、系統核心。以讀操作為例,當一個IO讀操作發生時,通常經曆兩個步驟:
1,等待資料準備
2,将資料從系統核心拷貝到操作程序中
例如,在socket上的讀操作,步驟1會等到網絡資料包到達,到達後會拷貝到系統核心的緩沖區;步驟2會将資料包從核心緩沖區拷貝到程式程序的緩沖區中。
阻塞(blocking)與非阻塞(non-blocking)IO
IO的阻塞、非阻塞主要表現在一個IO操作過程中,如果有些操作很慢,比如讀操作時需要準備資料,那麼目前IO程序是否等待操作完成,還是得知暫時不能操作後先去做别的事情?一直等待下去,什麼事也不做直到完成,這就是阻塞。抽空做些别的事情,這是非阻塞。
非阻塞IO會在發出IO請求後立即得到回應,即使資料包沒有準備好,也會傳回一個錯誤辨別,使得操作程序不會阻塞在那裡。操作程序會通過多次請求的方式直到資料準備好,傳回成功的辨別。
想象一下下面兩種場景:
A 小明和小剛兩個人都很耿直内向,一天小明來找小剛借書:“小剛啊,你那本XXX借我看看”。 于是小剛就去找書,小明就等着,找了半天找到了,把書給了小明。
B 小明和小剛兩個人都很活潑外向,一天小明來找小剛借書:“嘿小剛,你那本XXX借我看看”。 小剛說:“我得找一會”,小明就去打球去了。過會又來,這次書找到了,把書給了小明。
結論:A是阻塞的,B是非阻塞的。
從CPU角度可以看出非阻塞明顯提高了CPU的使用率,程序不會一直在那等待。但是同樣也帶來了線程切換的增加。增加的 CPU 使用時間能不能補償系統的切換成本需要好好評估。
同步(synchronous)與異步(asynchronous)IO
先來看看正式點的定義,POSIX标準将IO模型分為了兩種:同步IO和異步IO,Richard Stevens在《Unix網絡程式設計卷》中也總結道:
A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
An asynchronous I/O operation does not cause the requesting process to be blocked;
可以看出,判斷同步和異步的标準在于:一個IO操作直到完成,是否導緻程式程序的阻塞。如果阻塞就是同步的,沒有阻塞就是異步的。這裡的IO操作指的是真實的IO操作,也就是資料從核心拷貝到系統程序(讀)的過程。
繼續前面借書的例子,異步借書是這樣的:
C 小明很懶,一天小明來找小剛借書:“嘿小剛,你那本XXX借我看看”。 小剛說:“我得找一會”,小明就出去打球了并且讓小剛如果找到了就把書拿給他。小剛是個負責任的人,找到了書送到了小明手上。
A和B的借書方式都是同步的,有人要問了B不是非阻塞嘛,怎麼還是同步?
前面說了IO操作的2個步驟:準備資料和把資料從核心中拷貝到程式程序。映射到這個例子,書即是準備的資料,小剛是核心,小明是程式程序,小剛把書給小明這是拷貝資料。在B方式中,小剛找書這段時間小明的确是沒閑着,該幹嘛幹嘛,但是小剛找到書把書給小明的這個過程也就是拷貝資料這個步驟,小明還是得乖乖的回來候着小剛把書遞手上。是以這裡就阻塞了,根據上面的定義,是以是同步。
在涉及到 IO 處理時通常都會遇到一個是同步還是異步的處理方式的選擇問題。同步能夠保證程式的可靠性,而異步可以提升程式的性能。小明自己去取書不管等着不等着遲早拿到書,指望小剛找到了送來,萬一小剛忘了或者有急事忙别的了,那書就沒了。
讨論
說實話,網上關于同步與異步、阻塞與非阻塞的文章多之又多,大部分是拷貝的,也有些寫的非常好的。參考了許多,也借鑒了許多,也經過自己的思考。
同步與異步、阻塞與非阻塞之間确實有很多相似的地方,很容易混淆。wiki更是把異步與非阻塞畫上了等号,更多的人還是認為他們是不同的。原因可能有很多,每個人的知識背景不同,設定的上下文也不同。
我的看法是:在IO中,根據上面同步異步的概念,也可以看出來同步與異步往往是通過阻塞非阻塞的形式來表達的,并且是通過一種中間處理機制來達到異步的效果。同步與異步往往是IO操作請求者和回應者之間在IO實際操作階段的協作方式,而阻塞非阻塞更确切的說是一種自身狀态,目前程序或者線程的狀态。
在發出IO讀請求後,阻塞IO會一直等待有資料可讀,當有資料可讀時,會等待資料從核心拷貝至系統程序;而非阻塞IO都會立即傳回。至于資料怎麼處理是程式程序自己的事情,無關同步和異步。
兩種方式的組合
組合的方式當然有四種,分别是:同步阻塞、同步非阻塞、異步阻塞、異步非阻塞。
Java網絡IO實作和IO模型
不同的作業系統上有不同的IO模型,《Unix網絡程式設計卷》将unix上的IO模型分為5類:blocking I/O、nonblocking I/O、I/O multiplexing (select and poll)、signal driven I/O (SIGIO)以及asynchronous I/O (the POSIX aio_functions)。具體可參考《Unix網絡程式設計卷1》6.2章節。
在windows上IO模型也是有5種:select 、WSAAsyncSelect、WSAEventSelect、Overlapped I/O 事件通知以及IOCP。具體可參考windows五種IO模型。
Java是平台無關的語言,在不同的平台上會調用底層作業系統的不同的IO實作,下面就來說一下Java提供的網絡IO的工具和實作,為了擴大阻塞非阻塞的直覺感受,我都使用了長連接配接。
阻塞IO
同步阻塞最常用的一種用法,使用也是最簡單的,但是 I/O 性能一般很差,CPU 大部分在空閑狀态。下面是一個簡單的基于TCP的同步阻塞的Socket服務端例子:
1 @Test
2 public void testJIoSocket() throws Exception
3 {
4 ServerSocket serverSocket = new ServerSocket(10002);
5 Socket socket = null;
6 try
7 {
8 while (true)
9 {
10 socket = serverSocket.accept();
11 System.out.println("socket連接配接:" + socket.getRemoteSocketAddress().toString());
12 BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
13 while(true)
14 {
15 String readLine = in.readLine();
16 System.out.println("收到消息" + readLine);
17 if("end".equals(readLine))
18 {
19 break;
20 }
21 //用戶端斷開連接配接
22 socket.sendUrgentData(0xFF);
23 }
24 }
25 }
26 catch (SocketException se)
27 {
28 System.out.println("用戶端斷開連接配接");
29 }
30 catch (IOException e)
31 {
32 e.printStackTrace();
33 }
34 finally
35 {
36 System.out.println("socket關閉:" + socket.getRemoteSocketAddress().toString());
37 socket.close();
38 }
39 }
使用SocketTest作為用戶端工具進行測試,同時開啟2個用戶端連接配接Server端并發送消息,如下圖:
再看下背景的列印
socket連接配接:/127.0.0.1:54080
收到消息hello!
收到消息my name is client1
由于伺服器端是單線程的,在第一個連接配接的用戶端阻塞了線程後,第二個用戶端必須等待第一個斷開後才能連接配接。當輸入“end”字元串斷開用戶端1,這時候看到背景繼續列印:
socket連接配接:/127.0.0.1:54080
收到消息hello!
收到消息my name is client1
收到消息end
socket關閉:/127.0.0.1:54080
socket連接配接:/127.0.0.1:54091
收到消息hello!
收到消息my name is client2
所有的用戶端連接配接在請求服務端時都會阻塞住,等待前面的完成。即使是使用短連接配接,資料在寫入 OutputStream 或者從 InputStream 讀取時都有可能會阻塞。這在大規模的通路量或者系統對性能有要求的時候是不能接受的。
阻塞IO + 每個請求建立線程/線程池
通常解決這個問題的方法是使用多線程技術,一個用戶端一個處理線程,出現阻塞時隻是一個線程阻塞而不會影響其它線程工作;為了減少系統線程的開銷,采用線程池的辦法來減少線程建立和回收的成本,模式如下圖:
簡單的實作例子如下,使用一個線程(Accptor)接收用戶端請求,為每個用戶端建立線程進行處理(Processor),線程池的我就不弄了:
public class MultithreadJIoSocketTest
{
@Test
public void testMultithreadJIoSocket() throws Exception
{
ServerSocket serverSocket = new ServerSocket(10002);
Thread thread = new Thread(new Accptor(serverSocket));
thread.start();
Scanner scanner = new Scanner(System.in);
scanner.next();
}
public class Accptor implements Runnable
{
private ServerSocket serverSocket;
public Accptor(ServerSocket serverSocket)
{
this.serverSocket = serverSocket;
}
public void run()
{
while (true)
{
Socket socket = null;
try
{
socket = serverSocket.accept();
if(socket != null)
{
System.out.println("收到了socket:" + socket.getRemoteSocketAddress().toString());
Thread thread = new Thread(new Processor(socket));
thread.start();
}
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
}
public class Processor implements Runnable
{
private Socket socket;
public Processor(Socket socket)
{
this.socket = socket;
}
@Override
public void run()
{
try
{
BufferedReader in = new BufferedReader(new InputStreamReader(socket.getInputStream()));
String readLine;
while(true)
{
readLine = in.readLine();
System.out.println("收到消息" + readLine);
if("end".equals(readLine))
{
break;
}
//用戶端斷開連接配接
socket.sendUrgentData(0xFF);
Thread.sleep(5000);
}
}
catch (InterruptedException e)
{
e.printStackTrace();
}
catch (SocketException se)
{
System.out.println("用戶端斷開連接配接");
}
catch (IOException e)
{
e.printStackTrace();
}
finally {
try
{
socket.close();
}
catch (IOException e)
{
e.printStackTrace();
}
}
}
}
}
使用2個用戶端連接配接,這次沒有阻塞,成功的收到了2個用戶端的消息。
收到了socket:/127.0.0.1:55707
收到了socket:/127.0.0.1:55708
收到消息hello!
收到消息hello!
在單個線程進行中,我人為的使單個線程read後阻塞5秒,就像前面說的,出現阻塞也隻是在單個線程中,沒有影響到另一個用戶端的處理。
這種阻塞IO的解決方案在大部分情況下是适用的,在出現NIO之前是最通常的解決方案,Tomcat裡阻塞IO的實作就是這種方式。但是如果是大量的長連接配接請求呢?不可能建立幾百萬個線程保持連接配接。再退一步,就算線程數不是問題,如果這些線程都需要通路服務端的某些競争資源,勢必需要進行同步操作,這本身就是得不償失的。