從Java IO到Java NIO:如何理解阻塞和非阻塞I/O的區別?
Java NIO實現非阻塞I/O
在Java中,阻塞I/O(Blocking I/O)和非阻塞I/O(Non-blocking I/O)是兩種不同的I/O模式。
阻塞I/O模式下,當應用程序進行輸入/輸出操作時,線程會一直阻塞,直到數據傳輸完成或者發生異常。在此期間,線程無法執行其他任務,因此阻塞I/O模式具有較低的效率和響應性能。
非阻塞I/O模式下,當應用程序進行輸入/輸出操作時,線程會立即返回,并且不會等待數據傳輸完成。在此期間,線程可以執行其他任務,因此非阻塞I/O模式具有較高的效率和響應性能。
Java NIO中的非阻塞I/O是基于選擇器(Selector)和通道(Channel)的。選擇器可以監聽多個通道上的I/O事件,并在有事件發生時通知應用程序,從而實現非阻塞I/O操作。通道則是用于輸入/輸出操作的對象,可以是文件通道或網絡通道。
Java NIO是非阻塞的,因為它基于選擇器和通道實現了非阻塞I/O,支持同時處理多個通道的I/O事件,從而提高了I/O操作的效率和響應性能。相比之下,傳統的Java IO(也稱為IO流)是阻塞的,因為它只能同時處理一個輸入/輸出流,當進行輸入/輸出操作時,線程會一直阻塞,直到數據傳輸完成或者發生異常。
1、創建通道
通道是Java NIO中用于輸入/輸出操作的對象,可以通過SocketChannel、ServerSocketChannel、DatagramChannel等創建網絡通道,或者通過FileChannel創建文件通道。在這里,我們以SocketChannel為例創建網絡通道。
SocketChannel channel = SocketChannel.open();
2、將通道設置為非阻塞模式
通過調用通道的configureBlocking(false)方法,將通道設置為非阻塞模式。在非阻塞模式下,通道的讀取和寫入操作不會阻塞線程,而是立即返回。
channel.configureBlocking(false);
3、創建選擇器
選擇器是Java NIO中用于監聽多個通道的I/O事件的對象,用于實現非阻塞I/O。可以通過Selector.open()方法創建選擇器。
Selector selector = Selector.open();
4、將通道注冊到選擇器上
通過調用通道的register()方法,將通道注冊到選擇器上,并指定要監聽的事件類型,例如讀取事件、寫入事件、連接事件、接受事件等。在這里,我們注冊了讀取事件。
channel.register(selector, SelectionKey.OP_READ);
5、輪詢選擇器
通過調用選擇器的select()方法,輪詢選擇器上注冊的通道,當有通道上的I/O事件就緒時,select()方法會返回就緒的通道數量。
while (true) {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
// 處理就緒的通道
keyIterator.remove();
}
}
6、處理就緒的通道
通過調用選擇器的selectedKeys()方法,獲取所有就緒的通道,并進行相應的讀取或寫入操作。在這里,我們實現了從通道讀取數據的操作。
while (true) {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isReadable()) {
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
while (bytesRead > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
bytesRead = channel.read(buffer);
}
if (bytesRead == -1) {
channel.close();
}
}
keyIterator.remove();
}
}
需要注意的是,在非阻塞I/O模式下,讀取和寫入操作通常需要多次調用,直到完整的數據傳輸完成。在讀取操作中,需要將數據從通道讀取到緩沖區,并判斷緩沖區中是否已經讀取完畢。
此外,在非阻塞I/O模式下,發生異常的可能性比較高,因此需要進行異常處理。可以通過選擇器的selectedKeys()方法和SelectionKey的readyOps()方法,判斷通道是否出現異常,并進行相應的處理。
以下是完整的示例代碼。在這個例子中,我們使用了一個簡單的Echo服務器,將客戶端發送的消息原樣返回。
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.util.Iterator;
import java.util.Set;
public class NonBlockingServer {
public static void main(String[] args) throws IOException {
// 創建服務器套接字通道
ServerSocketChannel serverChannel = ServerSocketChannel.open();
serverChannel.socket().bind(new InetSocketAddress(9999));
serverChannel.configureBlocking(false);
// 創建選擇器
Selector selector = Selector.open();
serverChannel.register(selector, SelectionKey.OP_ACCEPT);
System.out.println("Server started on port 9999");
while (true) {
selector.select();
Set<SelectionKey> selectedKeys = selector.selectedKeys();
Iterator<SelectionKey> keyIterator = selectedKeys.iterator();
while (keyIterator.hasNext()) {
SelectionKey key = keyIterator.next();
if (key.isAcceptable()) {
// 處理連接事件
ServerSocketChannel channel = (ServerSocketChannel) key.channel();
SocketChannel clientChannel = channel.accept();
clientChannel.configureBlocking(false);
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("Client connected: " + clientChannel.getRemoteAddress());
} else if (key.isReadable()) {
// 處理讀取事件
SocketChannel channel = (SocketChannel) key.channel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer);
while (bytesRead > 0) {
buffer.flip();
while (buffer.hasRemaining()) {
channel.write(buffer);
}
buffer.clear();
bytesRead = channel.read(buffer);
}
if (bytesRead == -1) {
channel.close();
}
}
keyIterator.remove();
}
}
}
}
問題:selector.select()是阻塞,為什么還說NIO是非阻塞的呢?
selector.select()方法確實會阻塞,直到有至少一個通道準備好進行I/O操作或者等待超時或中斷。但是,需要注意的是,這種阻塞只會影響當前的線程,不會影響應用程序的其他線程。
在服務端線程調用選擇器的select()方法時,只有當前服務端線程會被阻塞,而不是客戶端線程。
客戶端的阻塞和非阻塞I/O操作取決于具體的實現。對于阻塞I/O模式,客戶端線程在進行輸入/輸出操作時,會一直阻塞,直到數據傳輸完成或者發生異常。對于非阻塞I/O模式,客戶端線程在進行輸入/輸出操作時,會立即返回,并且不會等待數據傳輸完成。在此期間,客戶端線程可以執行其他任務。
因此,Java NIO仍然可以稱為非阻塞I/O。
Java NIO提供了一種基于事件驅動的I/O模型,應用程序使用選擇器(Selector)來注冊通道(Channel)上的I/O事件,并在有事件發生時進行相應的處理。在選擇器上調用select()方法會阻塞當前線程,直到至少有一個通道上注冊的事件發生,此時select()方法會返回,應用程序可以通過selectedKeys()方法獲取就緒的事件。由于選擇器可以同時監聽多個通道,因此Java NIO可以同時處理多個通道上的I/O事件,從而提高了I/O操作的效率和響應性能。
需要注意的是,雖然選擇器的select()方法會阻塞當前線程,但是可以通過調用選擇器的wakeup()方法中斷阻塞,使得select()方法立即返回。此外,可以在選擇器上設置超時時間,使得select()方法在指定時間內返回,避免長時間的無限阻塞。
實戰Java NIO中實現文件I/O(File I/O)和網絡I/O(Network I/O)
文件I/O(File I/O)
Java NIO中的文件I/O是通過FileChannel來實現的。FileChannel類提供了讀取和寫入文件的方法,而ByteBuffer類則用于存儲讀取和寫入的數據。
以下是實現文件I/O的詳細步驟:
步驟1:獲取FileChannel實例
在進行文件I/O之前,需要先獲取FileChannel實例。可以通過FileInputStream或FileOutputStream來獲取FileChannel實例,例如:
FileInputStream fileInputStream = new FileInputStream("file.txt");
FileChannel fileChannel = fileInputStream.getChannel();
步驟2:創建ByteBuffer
在進行文件I/O之前,需要先創建ByteBuffer實例,用于存儲讀取和寫入的數據。可以通過ByteBuffer的allocate方法創建ByteBuffer實例,例如:
ByteBuffer buffer = ByteBuffer.allocate(1024);
步驟3:讀取文件數據
(1)從FileChannel中讀取數據
可以通過FileChannel的read方法從文件中讀取數據,并將數據存儲到ByteBuffer中。read方法有兩個重載版本:
int read(ByteBuffer dst) throws IOException;
long read(ByteBuffer[] dsts, int offset, int length) throws IOException;
第一個版本的read方法將數據讀取到單個ByteBuffer中,返回值為讀取的字節數。如果返回值為-1,表示已經讀取到了文件的末尾。
第二個版本的read方法將數據讀取到多個ByteBuffer中,返回值為讀取的字節數。如果返回值為-1,表示已經讀取到了文件的末尾。
以下是使用第一個版本read方法的示例代碼:
int bytesRead = fileChannel.read(buffer);
while (bytesRead != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
bytesRead = fileChannel.read(buffer);
}
上述代碼首先通過FileChannel的read方法將數據讀取到ByteBuffer中,并返回讀取的字節數。隨后,通過flip方法將ByteBuffer從寫模式切換為讀模式,并通過get方法讀取ByteBuffer中的數據。當ByteBuffer中的數據被讀取完畢后,通過clear方法將ByteBuffer從讀模式切換為寫模式,并再次調用FileChannel的read方法讀取文件中的數據,直到文件中的所有數據被讀取完畢。
(2)向FileChannel中寫入數據
可以通過FileChannel的write方法向文件中寫入數據,例如:
byte[] data = "Hello, World!".getBytes();
ByteBuffer buffer = ByteBuffer.wrap(data);
int bytesWritten = fileChannel.write(buffer);
上述代碼首先將數據存儲到ByteBuffer中,隨后調用FileChannel的write方法將數據寫入到文件中。
步驟4:關閉FileChannel
在使用完FileChannel后,需要調用其close方法關閉FileChannel,例如:
fileChannel.close();
完整的代碼示例:
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
public class FileIODemo {
public static void main(String[] args) throws IOException {
FileInputStream fileInputStream = new FileInputStream("file.txt");
FileChannel fileChannel = fileInputStream.getChannel();
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = fileChannel.read(buffer);
while (bytesRead != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
bytesRead = fileChannel.read(buffer);
}
fileChannel.close();
}
}
網絡I/O(Network I/O)
Java NIO中的網絡I/O是通過SocketChannel和ServerSocketChannel來實現的,它們分別用于客戶端和服務端的網絡通信。
以下是實現網絡I/O的詳細步驟:
步驟1:獲取SocketChannel或ServerSocketChannel實例
在進行網絡I/O之前,需要先獲取SocketChannel或ServerSocketChannel實例。可以通過SocketChannel或ServerSocketChannel的open方法獲取相應的實例,例如:
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("www.example.com", 80));
或:
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.bind(new InetSocketAddress(8080));
步驟2:創建ByteBuffer
在進行網絡I/O之前,需要先創建ByteBuffer實例,用于存儲讀取和寫入的數據。可以通過ByteBuffer的allocate方法創建ByteBuffer實例,例如:
ByteBuffer buffer = ByteBuffer.allocate(1024);
步驟3:讀取網絡數據
(1)從SocketChannel中讀取數據
可以通過SocketChannel的read方法從網絡中讀取數據,并將數據存儲到ByteBuffer中。read方法的用法與文件I/O中的read方法相同,這里不再贅述。
以下是使用SocketChannel的read方法的示例代碼:
int bytesRead = socketChannel.read(buffer);
while (bytesRead != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
bytesRead = socketChannel.read(buffer);
}
上述代碼首先通過SocketChannel的read方法將數據讀取到ByteBuffer中,并返回讀取的字節數。隨后,通過flip方法將ByteBuffer從寫模式切換為讀模式,并通過get方法讀取ByteBuffer中的數據。當ByteBuffer中的數據被讀取完畢后,通過clear方法將ByteBuffer從讀模式切換為寫模式,并再次調用SocketChannel的read方法讀取網絡中的數據,直到網絡中的所有數據被讀取完畢。
(2)向SocketChannel中寫入數據
可以通過SocketChannel的write方法向網絡中寫入數據,例如:
byte[] data = "Hello, World!".getBytes();
ByteBuffer buffer = ByteBuffer.wrap(data);
int bytesWritten = socketChannel.write(buffer);
上述代碼首先將數據存儲到ByteBuffer中,隨后調用SocketChannel的write方法將數據寫入到網絡中。
步驟4:關閉SocketChannel或ServerSocketChannel
在使用完SocketChannel或ServerSocketChannel后,需要調用其close方法關閉SocketChannel或ServerSocketChannel,例如:
socketChannel.close();
或:
serverSocketChannel.close();
:完整的代碼示例:
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.channels.SocketChannel;
public class NetworkIODemo {
public static void main(String[] args) throws IOException {
SocketChannel socketChannel = SocketChannel.open();
socketChannel.connect(new InetSocketAddress("www.example.com", 80));
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = socketChannel.read(buffer);
while (bytesRead != -1) {
buffer.flip();
while (buffer.hasRemaining()) {
System.out.print((char) buffer.get());
}
buffer.clear();
bytesRead = socketChannel.read(buffer);
}
socketChannel.close();
}
}