NIO芥颈,Non-Block IO 放案,從 Java 1.4 版本開始引入的非阻塞 IO ,是一種非阻塞 + 同步的通信模式止状,也是 I/O 多路復用的基礎(chǔ)烹棉,基于緩沖區(qū)(Buffer)的模式。
一怯疤、NIO主要有三大核心
通過下圖簡單展示三大核心之間的關(guān)系:
1.1 Channel簡介
讀寫數(shù)據(jù)的雙向通道浆洗,可以從 channel 將數(shù)據(jù)讀入 buffer,也可以將 buffer 的數(shù)據(jù)寫入 channel集峦。
jdk中定義了一個java.nio.channels.channel接口伏社,主要提供了開啟連接和關(guān)閉連接的方法。
package java.nio.channels;
import java.io.IOException;
import java.io.Closeable;
/**
* A nexus for I/O operations.
*
* <p> A channel represents an open connection to an entity such as a hardware
* device, a file, a network socket, or a program component that is capable of
* performing one or more distinct I/O operations, for example reading or
* writing.
*
* <p> A channel is either open or closed. A channel is open upon creation,
* and once closed it remains closed. Once a channel is closed, any attempt to
* invoke an I/O operation upon it will cause a {@link ClosedChannelException}
* to be thrown. Whether or not a channel is open may be tested by invoking
* its {@link #isOpen isOpen} method.
*
* <p> Channels are, in general, intended to be safe for multithreaded access
* as described in the specifications of the interfaces and classes that extend
* and implement this interface.
*
*
* @author Mark Reinhold
* @author JSR-51 Expert Group
* @since 1.4
*/
public interface Channel extends Closeable {
/**
* Tells whether or not this channel is open.
*
* @return <tt>true</tt> if, and only if, this channel is open
*/
public boolean isOpen();
/**
* Closes this channel.
*
* <p> After a channel is closed, any further attempt to invoke I/O
* operations upon it will cause a {@link ClosedChannelException} to be
* thrown.
*
* <p> If this channel is already closed then invoking this method has no
* effect.
*
* <p> This method may be invoked at any time. If some other thread has
* already invoked it, however, then another invocation will block until
* the first invocation is complete, after which it will return without
* effect. </p>
*
* @throws IOException If an I/O error occurs
*/
public void close() throws IOException;
在jdk中對于channel接口提供了很多實現(xiàn)類塔淤,最主要的是以下四個:
1)SocketChannel :一個客戶端用來發(fā)起 TCP 的 Channel 摘昌。
2)ServerSocketChannel :一個服務(wù)端用來監(jiān)聽新進來的連接的 TCP 的 Channel 。對于每一個新進來的連接高蜂,都會創(chuàng)建一個對應(yīng)的 SocketChannel 聪黎。
3)DatagramChannel :通過 UDP 讀寫數(shù)據(jù)。
4)FileChannel :從文件中妨马,讀寫數(shù)據(jù)挺举。
1.2 Buffer簡介
buffer 則用來緩沖讀寫數(shù)據(jù)杀赢,jdk提供java.nio.buffer抽象類烘跺,其屬性和構(gòu)造如下代碼所示:
package java.nio;
import java.util.Spliterator;
public abstract class Buffer {
/**
* The characteristics of Spliterators that traverse and split elements
* maintained in Buffers.
*/
static final int SPLITERATOR_CHARACTERISTICS =
Spliterator.SIZED | Spliterator.SUBSIZED | Spliterator.ORDERED;
// Invariants: mark <= position <= limit <= capacity
private int mark = -1;
private int position = 0;
private int limit;
private int capacity;
// Used only by direct buffers
// NOTE: hoisted here for speed in JNI GetDirectBufferAddress
long address;
// Creates a new buffer with the given mark, position, limit, and capacity,
// after checking invariants.
//
Buffer(int mark, int pos, int lim, int cap) { // package-private
if (cap < 0)
throw new IllegalArgumentException("Negative capacity: " + cap);
this.capacity = cap;
limit(lim);
position(pos);
if (mark >= 0) {
if (mark > pos)
throw new IllegalArgumentException("mark > position: ("
+ mark + " > " + pos + ")");
this.mark = mark;
}
}
}
如上代碼所示,Buffer有四個屬性脂崔,他們在Buffer讀和寫的情況下具有不同的含義:
mark:記錄當前讀或?qū)懙奈恢?br>
position:下一個位置
limit:范圍
capacity:Buffer的容量滤淳,創(chuàng)建時候指定,不能修改砌左。
主要模式切換如下所示:
主要方法:
每個buffer的實現(xiàn)類都實現(xiàn)了以下主要方法脖咐,下面以ByteBuffer的源碼舉例:
1)創(chuàng)建 Buffer:
/**
* 創(chuàng)建并指定大小
**/
public static ByteBuffer allocate(int capacity) {
if (capacity < 0)
throw new IllegalArgumentException();
return new HeapByteBuffer(capacity, capacity);
}
/**
* 數(shù)組轉(zhuǎn)成Buffer對象
**/
public static ByteBuffer wrap(byte[] array, int offset, int length)
{
try {
return new HeapByteBuffer(array, offset, length);
} catch (IllegalArgumentException x) {
throw new IndexOutOfBoundsException();
}
}
2)寫入:
// 寫入 byte
public abstract ByteBuffer put(byte b);
public abstract ByteBuffer put(int index, byte b);
// 寫入 byte 數(shù)組
public final ByteBuffer put(byte[] src) { ... }
public ByteBuffer put(byte[] src, int offset, int length) {...}
從channel將數(shù)據(jù)寫入buffer,該方法返回數(shù)據(jù)大谢愦酢:
int num = channel.read(buffer);
3)讀绕ㄉ谩:
// 讀取 byte
public abstract byte get();
public abstract byte get(int index);
// 讀取 byte 數(shù)組
public ByteBuffer get(byte[] dst, int offset, int length) {...}
public ByteBuffer get(byte[] dst) {...}
從buffer中的數(shù)據(jù)寫入到channel中,該方法返回數(shù)據(jù)大胁:
int num = channel.write(buffer);
4)讀寫模式切換
ByteBuffer是繼承buffer的派歌,其讀寫切換要基于Buffer的方法:
切換成讀模式:
public final Buffer flip() {
limit = position;
position = 0;
mark = -1;
return this;
}
切換成寫模式,clear()方法:
這個方法會將buffer的起始位置設(shè)為0痰哨,其實表面是將buffer清空了胶果,實際是不在記錄buffer的讀寫位置,此時寫入數(shù)據(jù)斤斧,原數(shù)據(jù)將會被覆蓋早抠。
public final Buffer clear() {
position = 0;
limit = capacity;
mark = -1;
return this;
}
如果想咋之前的數(shù)據(jù)后面繼續(xù)寫可以使用compact()方法:
public abstract ByteBuffer compact();
它有兩種實現(xiàn),我們應(yīng)該知道了解撬讽,分別是基于堆內(nèi)存和基于直接內(nèi)存的:
以下兩種buffer需要記兹锪:
/**
* @description: byteBuffer讀寫
* @author:weirx
* @date:2021/5/18 10:22
* @version:3.0
*/
public class ByteBufferReadWrite {
public static void main(String[] args) {
ByteBuffer buffer1 = ByteBuffer.allocate(16);
ByteBuffer buffer2 = ByteBuffer.allocateDirect(16);
//java.nio.HeapByteBuffer[pos=0 lim=16 cap=16]
//java 堆內(nèi)存悬垃,讀寫效率低,受gc影響
System.out.println(buffer1);
//java.nio.DirectByteBuffer[pos=0 lim=16 cap=16]
//直接內(nèi)存 讀寫效率高(少一次拷貝)咪奖,不收gc影響盗忱,分配的效率低,注意關(guān)閉羊赵,否則會造成內(nèi)存泄漏
System.out.println(buffer2);
}
}
執(zhí)行結(jié)果:
java.nio.HeapByteBuffer[pos=0 lim=0 cap=16]
java.nio.DirectByteBuffer[pos=0 lim=16 cap=16]
常見buffer有:
- ByteBuffer
- MappedByteBuffer
- DirectByteBuffer
- HeapByteBuffer
- ShortBuffer
- IntBuffer
- LongBuffer
- FloatBuffer
- DoubleBuffer
- CharBuffer
1.3 Selector簡介
Selector被稱為多路復用器趟佃,用于輪詢NIO的channel是否處于可讀或者可寫的狀態(tài),其位于jdk的java.nio.channels.Selector昧捷。
selector 的作用就是配合一個線程來管理多個 channel闲昭,獲取這些 channel 上發(fā)生的事件,這些 channel 工作在非阻塞模式下靡挥,不會讓線程吊死在一個 channel 上序矩。適合連接數(shù)特別多,但流量低的場景跋破。
調(diào)用 selector 的 select() 會阻塞直到 channel 發(fā)生了讀寫就緒事件簸淀,這些事件發(fā)生,select 方法就會返回這些事件交給 thread 來處理毒返。
輪詢步驟:
1)每個channel需要注冊到selector上租幕。
2)selector輪詢每個channel,當有channel發(fā)生讀寫操作拧簸,這個channel處于就緒狀態(tài)劲绪,會被輪詢到,等到就緒狀態(tài)的channel集合盆赤,進行后續(xù)的IO操作贾富。
代碼舉例:
//創(chuàng)建selector
Selector selector = Selector.open();
//注冊channel
channel.configureBlocking(false); // 必須是非阻塞
/*
第二個參數(shù)有以下四種類型:
Connect :連接完成事件( TCP 連接 ),僅適用于客戶端牺六,對應(yīng) SelectionKey.OP_CONNECT
Accept:接受新連接事件颤枪,僅適用于服務(wù)端,對應(yīng) SelectionKey.OP_ACCEPT 淑际。
Read :讀事件畏纲,適用于兩端,對應(yīng)SelectionKey.OP_READ 庸追,表示 Buffer 可讀霍骄。
Write :寫時間,適用于兩端淡溯,對應(yīng)SelectionKey.OP_WRITE 读整,表示 Buffer 可寫。
*/
SelectionKey key = channel.register(selector, SelectionKey.OP_READ);