NIO的三個主要組成部分:Buffer(緩衝區)、Channel(通道)、Selector(選擇器)
NIO與IO的區別
Java NIO是從java1.4版本開始引入的一個新的IO API,可以替代標準的Java IOAPI。NIO和IO有同樣的作用和目的,但是使用的方式完全不同。
普通的IO是面向流的,系統一次一個字節地處理數據,一個輸入流產生一個字節的數據,一個輸出流消費一個字節的數據,面向流的I/O速度非常慢
NIO支持面向緩衝區的,基於通道的IO操作。NIO將以更加高效的方式進行文件的讀寫。
IO | NIO |
---|---|
面向流 | 面向緩衝區 |
阻塞IO | 非阻塞IO |
無 | 選擇器 |
阻塞和非阻塞,同步和異步的概念
舉個例子,比如我們去照相館拍照,拍完照片之後,商家說需要30分鐘左右才能洗出來照片
-
同步+阻塞(BIO)
這個時候如果我們一直在店裏面啥都不幹,一直等待商家面前等待它洗完照片,這個過程就叫同步阻塞。
-
同步+非阻塞(NIO)
當然大部分人很少這麼幹,更多的是大家拿起手機開始看電視,看一會就會問老闆洗完沒,老闆說沒洗完,然後我們接着看,再過一會接着問(輪詢),直到照片洗完,這個過程就叫同步非阻塞。
-
異步+非阻塞(AIO)
當然實際情況是,大家可能會直接先去逛街或者喫飯做其他的活動,同時等待老闆打電話,這樣以來兩不耽誤,這個過程就叫異步非阻塞。
總結
從上面的描述中我們其實能夠看到阻塞和非阻塞通常是指客戶端在發出請求後,在服務端處理這個請求的過程中,客戶端本身是否直接掛起等待結果(阻塞),還是繼續做其他的任務(非阻塞)。
而異步和同步,則是對於請求結果的獲取是客戶端主動等待獲取(同步),還是由服務端來通知消息結果(異步)。
從這一點來看同步和阻塞其實描述的兩個不同角度的事情,阻塞和非阻塞指的一個是客戶端等待消息處理時的本身的狀態,是掛起還是繼續幹別的。同步和異步指的對於消息結果的獲取是客戶端主動獲取,還是由服務端間接推送。
阻塞:等待結果
非阻塞:可以做別的事情
同步:主動獲取結果
異步:等待服務器通知結果
Buffer(緩衝區)
緩衝區概念
Java NIO中的Buffer主要用於與NIO通道(Channel)進行交互,數據是從通道讀入緩衝區,從緩衝區寫入通道中的。
Buffer就像一個數組,可以保存多個相同類型的數據。根據數據類型的不同,有以下Buffer常用子類:
- ByteBuffer(常用)
- ByteBuffer
- CharBuffer
- DoubleBuffer
- FloatBuffer
- IntBuffer
- LongBuffer
- ShortBuffer
創建ByteBuffer對象
方式一:在堆中創建緩衝區:allocate(int capacity)
- 在堆中創建緩衝區稱爲:間接緩衝區
public static void main(String[] args) {
//創建堆緩衝區
ByteBuffer byteBuffer = ByteBuffer.allocate(10);
}
方式二:在系統內存創建緩衝區:allocatDirect(int capacity)
- 在系統內存創建緩衝區稱爲:直接緩衝區
public static void main(String[] args) {
//創建直接緩衝區
ByteBuffer byteBuffer = ByteBuffer.allocateDirect(10);
}
方式三:通過數組創建緩衝區:wrap(byte[] arr)
- 在系統內存創建緩衝區稱爲:直接緩衝區
public static void main(String[] args) {
byte[] byteArray = new byte[10];
ByteBuffer byteBuffer = ByteBuffer.wrap(byteArray);
}
- 間接緩衝區的創建和銷燬效率要高於直接緩衝區
- 間接緩衝區的創建和銷燬效率要高於直接緩衝區
緩衝區的基本屬性
-
容量(capacity):Buffer所能夠包含的元素的最大數量。定義了Buffer後,容量是不可變的。
-
限制(limit):第一個不應該讀取或寫入元素的index索引。就是limit後的數據補課讀寫。緩衝區的限制(limit)不能爲負,並且不能大於容量.
-
位置(position):當前可寫入的索引。位置不能小於0,並且不能大於"限制".
-
標記(mark)與重置(reset):標記是一個索引,通過Buffer中的mark()方法指定Buffer中一個特定的position,之後可以通過reset()方法恢復到這個position。
Channel(通道)
一、概念
通道(Channel):用於源節點和目標節點的連接。在Java nio中負責緩衝區中數據的傳輸。
Channel本身不存儲數據,因此需要配合緩衝區進行傳輸。
二、通道的主要實現類
java.nio.channels.Channel 接口
- FileChannel:從文件讀取數據的
- DatagramChannel:讀寫UDP網絡協議數據
- SocketChannel:讀寫TCP網絡協議數據
- ServerSocketChannel:可以監聽TCP連接
三、獲取通道
1.Java針對支持通道的類提供了getChannel()方法
本地IO:
FileInputStream、FileOutputStream
RandomAccessFile
網絡IO:
Socket
ServerSocket
DatagramSocket
2、在JDK1.7中的NIO.2針對各個通道提供了靜態方法open()
3、在JDK1.7中的NIO.2的File工具類的newByteChannel()
Selector(選擇器)
多路複用的概念
選擇器Selector是NIO中的重要技術之一。它與SelectableChannel聯合使用實現了非阻塞的多路複用。使用它可以節省CPU資源,提高程序的運行效率。
"多路"是指:服務器端同時監聽多個“端口”的情況。每個端口都要監聽多個客戶端的連接。服務器端的非多路複用效果
如果不使用“多路複用”,服務器端需要開很多線程處理每個端口的請求。如果在高併發環境下,造成系統性能下降。
服務器端的多路複用效果使用了多路複用,只需要一個線程就可以處理多個通道,降低內存佔用率,減少CPU切換時間,在高併發、高頻段業務環境下有非常重要的優勢
選擇器Selector
Selector被稱爲:選擇器,也被稱爲:多路複用器,它可以註冊到很多個Channel上,監聽各Channel上發生的事件,並且能夠根據事件情況決定Channel讀寫。這樣,通過一個線程管理多個Channel,就可以處理大量網絡連接了。
有了Selector,我們就可以利用一個線程來處理所有的Channels。線程之間的切換對操作系統來說代價是很高的,並且每個線程也會佔用一定的系統資源。所以,對系統來說使用的線程越少越好。
如何創建一個Selector
register()方法的第二個參數:是一個int值,意思是在通過Selector監聽Channel時對什麼事件感興趣。可以監聽
四種不同類型的事件,而且可以使用SelectionKey的四個常量表示:
- 連接就緒–常量:SelectionKey.OP_CONNECT
- 接收就緒–常量:SelectionKey.OP_ACCEPT (ServerSocketChannel在註冊時只能使用此項)
- 讀就緒–常量:SelectionKey.OP_READ
- 寫就緒–常量:SelectionKey.OP_WRITE
注意:對於ServerSocketChannel在註冊時,只能使用OP_ACCEPT,否則拋出異常。
示例:下面的例子,服務器創建3個通道,同時監聽3個端口,並將3個通道註冊到一個選擇器中
import java.net.InetSocketAddress;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
public class Server {
public static void main(String[] args) throws Exception {
//創建3個通道,同時監聽3個端口
ServerSocketChannel channelA = ServerSocketChannel.open();
channelA.configureBlocking(false);
channelA.bind(new InetSocketAddress(7777));
ServerSocketChannel channelB = ServerSocketChannel.open();
channelB.configureBlocking(false);
channelB.bind(new InetSocketAddress(8888));
ServerSocketChannel channelC = ServerSocketChannel.open();
channelC.configureBlocking(false);
channelC.bind(new InetSocketAddress(9999));
//獲取選擇器
Selector selector = Selector.open();
//註冊三個通道
channelA.register(selector, SelectionKey.OP_ACCEPT);
channelB.register(selector, SelectionKey.OP_ACCEPT);
channelC.register(selector, SelectionKey.OP_ACCEPT);
}
}
接下來,就可以通過選擇器selector操作三個通道了。