文章目錄
- 1. BIO
- 2. NIO
- 2.1 NIO與多路復用器
- 2.2. NIO與redis
- 3. AIO
- 4. BIO、NIO、AIO的對比
IO模型就是說用什么樣的通道進行資料的發送和接收,首先要明確一點:IO是作業系統與其他網路進行資料互動,JDK底層并沒有實作IO,而是對作業系統內核函式做的一個封裝,IO代碼進入底層其實都是native形式的,Java共支持3種網路編程IO模式:BIO,NIO,AIO,下文進行介紹
1. BIO
BIO(Blocking IO) 又稱同步阻塞IO,一個客戶端由一個執行緒來進行處理,執行緒模型如下所示

BIO代碼示例
public class SocketServer {
public static void main(String[] args) throws IOException {
//創建socket連接,埠為9000
ServerSocket serverSocket = new ServerSocket(9000);
while (true) {
System.out.println("等待連接,,");
//阻塞方法
Socket clientSocket = serverSocket.accept();
System.out.println("有客戶端連接了,,");
//單執行緒連接,性能不好,下面開啟多執行緒
//handler(clientSocket);
//開啟多執行緒
new Thread(new Runnable() {
@Override
public void run() {
try {
handler(clientSocket);
} catch (IOException e) {
e.printStackTrace();
}
}
}).start();
}
}
private static void handler(Socket clientSocket) throws IOException {
byte[] bytes = new byte[1024];
System.out.println("準備read,,");
//接收客戶端的資料,阻塞方法,沒有資料可讀時就阻塞
int read = clientSocket.getInputStream().read(bytes);
System.out.println("read完畢,,");
if (read != -1) {
System.out.println("接收到客戶端的資料:" + new String(bytes, 0, read));
}
clientSocket.getOutputStream().write("HelloClient".getBytes());
clientSocket.getOutputStream().flush();
}
}
上邊是BIO連接的示例代碼,啟動后可以通過telnet與 localhost 9000 建立連接,并發送字串資訊123,測驗結果如下:


測驗成功,但BIO現在已經用的不多了,因為它在大并發下有幾個致命的缺點:
- 如果BIO使用單執行緒接受連接,則會阻塞其他連接,效率較低,
- 如果使用多執行緒雖然減弱了單執行緒帶來的影響,但當有大并發進來時,會導致服務器執行緒太多,壓力太大而崩潰,
- 就算使用執行緒池,也只能同時允許有限個數的執行緒進行連接,如果并發量遠大于執行緒池設定的數量,還是與單執行緒無異
- IO代碼里read操作是阻塞操作,如果連接不做資料讀寫操作會導致執行緒阻塞,就是說只占用連接,不發送資料,則會浪費資源,比如執行緒池中500個連接,只有100個是頻繁讀寫的連接,其他占著茅坑不拉屎,浪費資源!
- 另外多執行緒也會有執行緒切換帶來的消耗
綜上所述,BIO方式已經不適用于如下的大并發場景,僅適用于連接數目比較小且固定的架構,這種方式對服務器資源要求比較高,但BIO程式簡單易理解,
2. NIO
為了解決BIO在大并發下存在的問題,誕生了NIO,NIO(Non Blocking IO)又稱同步非阻塞IO,服務器實作模式為一個執行緒可以處理多個請求(連接),也就是多路復用,JDK1.4開始引入,
應用場景:
NIO方式適用于連接數目多且連接比較短(輕操作) 的架構, 比如聊天服務器, 彈幕系統, 服務器間通訊,編程比較復雜
先來看一個NIO的簡單版本(未加入Selector ):
public class NioServer {
// 保存客戶端連接
static List<SocketChannel> channelList = new ArrayList<>();
public static void main(String[] args) throws IOException, InterruptedException {
// 創建NIO ServerSocketChannel,與BIO的serverSocket類似
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(9000));
// 設定ServerSocketChannel為非阻塞, 配置為true,則和BIO類似
serverSocket.configureBlocking(false);
System.out.println("服務啟動成功");
while (true) {
// 非阻塞模式accept方法不會阻塞,否則會阻塞
// NIO的非阻塞是由作業系統內部實作的,底層呼叫了linux內核的accept函式
SocketChannel socketChannel = serverSocket.accept();
if (socketChannel != null) { // 如果有客戶端進行連接
System.out.println("連接成功");
// 設定SocketChannel為非阻塞
socketChannel.configureBlocking(false);
// 保存客戶端連接在List中
channelList.add(socketChannel);
}
// 遍歷連接進行資料讀取
Iterator<SocketChannel> iterator = channelList.iterator();
while (iterator.hasNext()) {
SocketChannel sc = iterator.next();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
// 非阻塞模式read方法不會阻塞,否則會阻塞
int len = sc.read(byteBuffer);
// 如果有資料,把資料列印出來
if (len > 0) {
System.out.println("接收到訊息:" + new String(byteBuffer.array()));
} else if (len == -1) { // 如果客戶端斷開,把socket從集合中去掉
iterator.remove();
System.out.println("客戶端斷開連接");
}
}
}
}
}
從上述代碼可以看到,NIO使用一個mian執行緒 + 一個陣列 解決了BIO的痛點,具體解決方式如下
- 首先設定服務端連接的阻塞方式為false,代表非阻塞方式
- 非阻塞式接受客戶端連接,意味著這段代碼一直在輪詢的跑,不會阻塞,
- 如果有客戶端連接進來,就把這個連接放入list集合中
- 后續遍歷list集合,使用非阻塞式讀取資料
- 讀取完成再次輪詢跑代碼
測驗如下:

這樣就算NIO的全部嗎?顯然不會這么簡單,這種方式雖然解決了BIO的部分痛點,但并不是很完美,假如連接數太多,有10000個連接,其中只有1000個連接有寫資料,但是由于其他9000個連接并沒有斷開,我們還是要每次輪詢遍歷一萬次,其中有十分之九的遍歷都是無效的,這顯然不是一個讓人很滿意的狀態,為了處理無效遍歷的問題,NIO引入了多路復用器
2.1 NIO與多路復用器
NIO 有三大核心組件:
- Buffer(緩沖區):buffer 底層就是個陣列
- Channel(通道):channel 類似于流,每個 channel 對應一個 buffer緩沖區
- Selector(多路復用器):channel 會注冊到 selector 上,由 selector 根據 channel 讀寫事件的發生將其交由某個空閑的執行緒處理
注意:NIO 的 Buffer 和 channel 都是既可以讀也可以寫,NIO的多路復用示意圖如下:

引入多路復用器selector后的代碼示例
public class NioSelectorServer {
public static void main(String[] args) throws IOException, InterruptedException {
// 創建NIO ServerSocketChannel
ServerSocketChannel serverSocket = ServerSocketChannel.open();
serverSocket.socket().bind(new InetSocketAddress(9000));
// 設定ServerSocketChannel為非阻塞
serverSocket.configureBlocking(false);
// 打開Selector處理Channel,即創建epoll
Selector selector = Selector.open();
// 把ServerSocketChannel注冊到selector上,并且selector對客戶端accept連接操作感興趣
serverSocket.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("服務啟動成功");
while (true) {
// 阻塞等待需要處理的事件發生
selector.select();
// 獲取selector中注冊的全部事件的 SelectionKey 實體
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
// 遍歷SelectionKey對事件進行處理
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 如果是OP_ACCEPT事件,則進行連接獲取和事件注冊
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
SocketChannel socketChannel = server.accept();
socketChannel.configureBlocking(false);
// 這里只注冊了讀事件,如果需要給客戶端發送資料可以注冊寫事件
socketChannel.register(selector, SelectionKey.OP_READ);
System.out.println("客戶端連接成功");
} else if (key.isReadable()) { // 如果是OP_READ事件,則進行讀取和列印
SocketChannel socketChannel = (SocketChannel) key.channel();
ByteBuffer byteBuffer = ByteBuffer.allocate(128);
int len = socketChannel.read(byteBuffer);
// 如果有資料,把資料列印出來
if (len > 0) {
System.out.println("接收到訊息:" + new String(byteBuffer.array()));
} else if (len == -1) { // 如果客戶端斷開連接,關閉Socket
System.out.println("客戶端斷開連接");
socketChannel.close();
}
}
//從事件集合里洗掉本次處理的key,防止下次select重復處理
iterator.remove();
}
}
}
}
2.2. NIO與redis
為什么 redis 不建議用 bigkey?
bigkey的big體現在單個value值很大,一般認為超過10KB就是bigkey,由于redis底層用的是NIO,多路復用一個執行緒,如果存在bigkey的話,這個bigkey就會占用這個執行緒較大的時間,導致其他連接的資料互動阻塞,所以不建議使用bigkey,注意:這里說的阻塞并不是 異步非阻塞的阻塞,
3. AIO
AIO自JDK1.7以后才開始支持,是異步非阻塞的,客戶端與服務端的連接(accept)、資料讀寫(read、write)不再由main執行緒去執行,而是開辟一個回呼函式,當客戶端與服務端建立連接時,把這個客戶端的連接傳入回呼函式中,由服務端啟動一個子執行緒去處理,這就完成了異步操作!適用于連接數較多且連接時間較長的應用,
AIO與BIO、NIO的不同之處在于:
- AIO是 異步非阻塞模型
- NIO是 同步非阻塞模型
- BIO是 同步阻塞模型
AIO代碼示例
public class AIOServer {
public static void main(String[] args) throws Exception {
//創建服務端
final AsynchronousServerSocketChannel serverChannel =
AsynchronousServerSocketChannel.open().bind(new InetSocketAddress(9000));
//使用CompletionHandler異步處理客戶端連接
serverChannel.accept(null, new CompletionHandler<AsynchronousSocketChannel, Object>() {
@Override
public void completed(AsynchronousSocketChannel socketChannel, Object attachment) {
try {
System.out.println("2--"+Thread.currentThread().getName());
// 在此接收客戶端連接,如果不寫這行代碼后面的客戶端連接連不上服務端
serverChannel.accept(attachment, this);
System.out.println(socketChannel.getRemoteAddress());
ByteBuffer buffer = ByteBuffer.allocate(1024);
//使用CompletionHandler異步讀取資料
socketChannel.read(buffer, buffer, new CompletionHandler<Integer, ByteBuffer>() {
@Override
public void completed(Integer result, ByteBuffer buffer) {
System.out.println("3--"+Thread.currentThread().getName());
buffer.flip();
System.out.println(new String(buffer.array(), 0, result));
socketChannel.write(ByteBuffer.wrap("HelloClient".getBytes()));
}
@Override
public void failed(Throwable exc, ByteBuffer buffer) {
exc.printStackTrace();
}
});
} catch (IOException e) {
e.printStackTrace();
}
}
@Override
public void failed(Throwable exc, Object attachment) {
exc.printStackTrace();
}
});
System.out.println("1--"+Thread.currentThread().getName());
Thread.sleep(Integer.MAX_VALUE);
}
}
AIO作為異步非阻塞模型,理論上來說應該被廣泛使用,但大多數公司并沒有使用AIO,而是使用了netty,為什么?
- 首先AIO得底層實作仍使用Epoll,并沒有很好的實作異步,在性能上對比NIO沒有太大優勢
- 其次AIO的代碼邏輯比較復雜,且Linux上AIO還不夠成熟
- Netty在NIO上做了很多異步的封裝,是異步非阻塞框架
4. BIO、NIO、AIO的對比

一個關于同步異步與阻塞非阻塞的段子:
老張愛喝茶,廢話不說,煮開水,
出場人物:老張,水壺兩把(普通水壺,簡稱水壺;會響的水壺,簡稱響水壺),
- 老張把水壺放到火上,立等水開,(同步阻塞) 老張覺得自己有點傻
- 老張把水壺放到火上,去客廳看電視,時不時去廚房看看水開沒有,(同步非阻塞) 老張還是覺得自己有點傻,于是變高端了,買了把會響笛的那種水壺,水開之后,能大聲發出嘀~~~~的噪音,
- 老張把響水壺放到火上,立等水開,(異步阻塞)老張覺得這樣傻等意義不大
- 老張把響水壺放到火上,去客廳看電視,水壺響之前不再去看它了,響了再去拿壺,(異步非阻塞)老張覺得自己聰明了,
所謂同步異步,只是對于水壺而言,
- 普通水壺,同步
- 響水壺,異步,
- 雖然都能干活,但響水壺可以在自己完工之后,提示老張水開了,這是普通水壺所不能及的,
同步只能讓呼叫者去輪詢自己(情況2中),造成老張效率的低下,
所謂阻塞非阻塞,僅僅對于老張而言,
- 立等的老張,阻塞
- 看電視的老張,非阻塞,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/250200.html
標籤:其他
