Java NIO之學習綜述
目錄
簡介
為提升java程式的執行速度,我們經常將精力放在程式碼優化上面,很少關注影響較大的I/O流方面.多數情況下,並非作業系統不能做到快速的傳輸,而是受到JVM在IO方面限制,導致了作業系統與Java的IO流模型不匹配。作業系統通常在直接儲存器(DMA)協助下完成大塊資料(緩衝區)移動,但JVM中IO流操作的小塊資料(單個位元組,幾行文字).結果是作業系統送來整個緩衝區,而java中IO只是一小塊的讀取,但是每一塊的讀取都要往返幾層物件,使得效率很低.NIO就可以做到將整塊資料直接放到緩衝區Buffer中.
NIO是在JDK1.4版本的java.nio.*引入的內容,滿足了java程式高密度的IO傳輸,與傳統的IO流相比較,讀取和寫入速度有極大的提升.兩者的區別有如下幾點:
IO流 | NIO |
---|---|
面向流 | 面向緩衝 |
阻塞IO | 非阻塞IO |
無 | 選擇器 |
1. IO流是面向流的。
一次性從流中讀取一個位元組或者多個位元組,直到讀取所有的位元組.沒有直接提供緩衝區,所以沒有辦法移動流的中資料,除非將資料緩衝區起來(比如RandomAcessFile就可以實現)。而NIO則是面向塊的(Buffer緩衝),不管是讀取還是寫入,都是將資料先放到緩衝區,由於緩衝區本身就是一個數組,所以可以通過操作索引靈活地讀取資料.
2. IO流是阻塞的。
呼叫read()讀取資料或者write()寫入資料時,在讀取到資料或者完全寫入資料之前,執行緒一直處於阻塞狀態,不能幹別的事情,java NIO是非阻塞模式的,一個執行緒從某通道傳送請求讀取資料,它僅能得到目前能夠獲取的資料,如果沒有資料的時候,不會獲取任何的資料,而不是在阻塞狀態,所以直至資料變的可以讀取之前,可以繼續幹別的事情.非阻塞寫也是如此,一個執行緒請求通道寫入資料,在資料完全寫入之前,執行緒可以去做別的事情.執行緒將非阻塞的時間用於處理其他通道上執行IO執行,所以一個執行緒可以管理多個通道.
3. java NIO中的選擇器允許一個執行緒監視多個通道,
可以將建立的一個或者多個可選擇的通道註冊到選擇器物件中.在通道準備就緒之前,執行緒可以休眠或者輪詢選擇器,檢視上次檢查之後,是否有通道處於就緒狀態.需要注意的是一個通道可以註冊到多個選擇器上面,但是在每個選擇器上只能註冊一次.
java NIO最重要的三個核心API:緩衝區(Buffer),通道(Channel),選擇器(Selector),這也是NIO的創新點.引入"Java程式設計思想"中比較好的例子,從煤礦裡面運出礦藏,通道就是包含的煤層的礦藏,而緩衝區就是裝滿礦藏的卡車,卡車載滿礦藏(資料)而回.我們從而得到了對應的礦藏,從整個過程來看,我們並沒有直接和通道進行接觸,只是和緩衝區進行了互動,將緩衝區派送到通道,而通道將對應資料放到緩衝區裡面,或者從緩衝區獲取資料.而通道可能並不僅僅只存在一個,我們將建立的通道註冊到選擇器上面,通過單個執行緒就可以管理多個通道.
緩衝區(Buffer)
Buffer物件是固定數量的資料容器,原始是一個位元組陣列,所以儲存到緩衝區的資料可以用於檢索. java.nio包下面提供了java基本資料型別除boolean型別以外的對應緩衝區,從通道獲取的原始資料是位元組資料放到位元組緩衝區(ByteBuffer),非位元組緩衝區可以執行從位元組或者到位元組轉換.緩衝區的工作與通道緊密聯絡,通道時IO傳輸發生時通過的入口,而緩衝區是這些資料傳輸的來源或者目標.
Buffer作為頂層的抽象類,提供了操作緩衝區的基本方法,子類有ByteBuffer,ShortBuffer,IntBuffer,LongBuffer,CharBuffer,
FloatBuffer,DoubleBuffer對應的java基本資料型別是byte,short,int,long,char,float,double.其中不包含boolean型別(布林型別無法對應到一個或者多個位元組),此外還有MappedByteBuffer,HeapByteBuffer,DirectByteBuffer。
基本資料型別 | 緩衝區 |
---|---|
byte | ByteBuffer |
short | ShortBuffer |
int | IntBuffer |
long | LongBuffer |
boolean | - |
char | CharBuffer |
float | FloatBuffer |
double | DoubleBuffer |
- | MappedByteBuffer |
- | HeapByteBuffer(底層實現,無法直接訪問) |
- | DirectByteBuffer(底層實現,無法直接訪問) |
通道(Channel)
通道主要是實現了位元組緩衝區和通道另一側的實體(檔案或者套接字)之間有效的傳輸。也就是使用java NIO讀寫網路資料或者檔案,經通道,放到緩衝區。channel類主要在java.nio.channels包中,部分channels將會依賴到java.nio.channels.spi子包中定義的類,標準的IO流是單向的,比如FileInputStream只能用於讀取資料,而FileOutputStream只能用於寫入資料.通道也可以是單向,也可以雙向的.一個channel類實現了定義read()方法的ReadableByteChannel介面,另外一個channel類實現了定義write()方法的WritableByteChannel介面,單一實現了ReadableByteChannel介面或者WritableByteChannel介面都只是單向的,但一個類實現了ReadableByteChannel介面,又實現了WriteableByteChannel介面,就是雙向的(既可以向通道中寫入資料,也可以讀取資料).
通道分為兩種型別:檔案(file)通道和套接字(socket)通道,檔案通道有FileChannel.而套接字(socket)通道包含了SocketChannel,ServerSocketChannel,DatagramChannel,每個Socket通道都關聯一個java.net.socket物件.通道有多種建立方式,其中socket通道有直接建立的工廠方法,但是FileChannel只能在開啟的FileInputStream,FileOutputStream以及RandomAccessFile物件上呼叫getChannel()獲得,沒有直接建立FileChannel物件的方法.關於通道的說明:
a.檔案通道(FileChannel):
- FileChannel---從檔案中讀取資料或者寫入資料到檔案中
b.套接字通道(SocketChannel,DatagramChannel,ServerSocketChannel):
- SocketChannel---模擬連線導向的流協議(TCP/IP),點對點,有序的網路連線,連線成功,只能從連線的地址中接收資料或者傳送資料到連線的指定地址。
- DatagramChannel---模擬無連線導向的流協議(UDP/IP),可以接收任何目的的資料,也可以傳送資料給不同目的地址。
- ServerSocketChannel---本身不傳輸資料,負責監聽傳入的連線和建立新的SocketChannel物件.
選擇器(Selector)
一個通道或者多個通道可以註冊到選擇器物件,選擇器物件管理就是這些被註冊通道集合以及他們的就緒狀態.當一個通道註冊到選擇器上面就會有一個對應的選擇鍵SelectionKey物件返回,SelectionKey物件中包含選擇器和對應註冊的通道.呼叫選擇鍵中對應的方法可以關閉註冊到選擇器上的通道或者通過關閉選擇器使選擇鍵失效.
在檢測到註冊到選擇器上的通道就緒之前,執行緒可以休眠,或者通過週期性的輪詢選擇器,檢視上次檢查後是否有通道處於就緒狀態,所以單個執行緒就可以做到管理多個通道,即管理多個連線。
簡單案例
1.傳統IO流與檔案通道FileChannel比較
public class FileChannelDemo {
public static void main(String[] args) throws IOException {
testStandardIO();
testFileChannel();
}
private static void testFileChannel() throws IOException {
FileInputStream fis = new FileInputStream(new File("D:\\java.txt"));
FileChannel channel = fis.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
channel.read(buffer);
buffer.flip();
System.out.println(buffer.toString());
channel.close();
fis.close();
}
private static void testStandardIO() throws FileNotFoundException, IOException {
FileInputStream fis = new FileInputStream(new File("D:\\java.txt"));
byte[] buffer =new byte[1024];
int len;
while((len=fis.read(buffer))!=-1) {
System.out.println(new String(buffer));
}
fis.close();
}
}
2.SocketChannel和ServerSocketChannel的配合使用.
//傳送端
public class SocketChannelDemo {
public static void main(String[] args) throws IOException {
sendData();
}
private static void sendData() throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
socketChannel.connect(new InetSocketAddress(1234));
if(socketChannel.finishConnect()) {
int i = 0;
while(true) {
String info = "this is info from SocketChannel"+i++;
buffer.clear();
buffer.put(info.getBytes());
buffer.flip();
while(buffer.hasRemaining()) {
socketChannel.write(buffer);
}
}
}
socketChannel.close();
}
}
//接收端
public class ServerSocketChannelDemo {
public static void main(String[] args) throws IOException, InterruptedException {
receiveData();
}
private static void receiveData() throws IOException, InterruptedException {
ServerSocketChannel ssc = ServerSocketChannel.open();
ssc.bind(new InetSocketAddress(1234));
ssc.configureBlocking(false);
ByteBuffer buffer = ByteBuffer.allocate(1024);
String decoding = System.getProperty("file.encoding");
while(true) {
SocketChannel socketChannel = ssc.accept();
if(socketChannel!=null) {
InetSocketAddress removeAddress=(InetSocketAddress)socketChannel.getRemoteAddress();
System.out.println(removeAddress.getPort()+"---"+removeAddress.getAddress());
socketChannel.read(buffer);
buffer.flip();
while(buffer.hasRemaining()) {
System.out.println(Charset.forName(decoding).decode(buffer));
}
}else{
Thread.sleep(1000);
}
}
}
}
3.DatagramChannel
//傳送端
public class DatagramSendDemo {
public static void main(String[] args) throws IOException {
sendData();
}
private static void sendData() throws IOException {
DatagramChannel channel = DatagramChannel.open();
ByteBuffer buffer = ByteBuffer.allocate(1024);
buffer.clear();
buffer.put("helloworld".getBytes());
buffer.flip();
channel.send(buffer,new InetSocketAddress("127.0.0.1",1234));
}
}
//接收端
public class DatagramReceiveDemo {
public static void main(String[] args) throws IOException {
receiveData();
}
private static void receiveData() throws IOException {
DatagramChannel channel = DatagramChannel.open();
channel.bind(new InetSocketAddress("127.0.0.1",1234));
ByteBuffer buffer = ByteBuffer.allocate(1024);
while(true) {
buffer.clear();
channel.receive(buffer);
buffer.flip();
String encoding = System.getProperty("file.encoding");
while(buffer.hasRemaining()) {
System.out.println(Charset.forName(encoding).decode(buffer));
}
}
}
}
總結
傳統的IO流主要是單向傳輸(如FileInputStream只能讀取資料,FileOutputStream只能寫入資料),讀取/寫入單個位元組或者文字行,直到處理完所有的資料.Java NIO是全新的內容,一個類同時實現了ReadableByteChannel介面和WritableByteChannel介面,就可以做到雙向傳輸(一個類中即有讀取方法,也有寫入方法,類似於IO流中RandomAccessFile),通過通道(Channel)將資料放到緩衝區(Buffer)中,實現了讀取/寫入大塊資料。相比較傳統IO流,做到高效率傳輸。