關於NIO這部分,除了《Java編程思想》中的介紹還有兩份資料我覺得很好:一是《深入Java Web技術內幕》第2章的部分,二是併發編程網上Jakob JenkovNIO系列教程翻譯,讀完之後受益匪淺。
1. NIO是什麼:
非阻塞IO是NIO的一大特點,那它是怎麼實現的呢,Selector和它的名字所反映的一樣起到選擇和調度的作用,它所調度的就是Channel(通道),Channel包括:FileChannel,SocketChannel,ServerSocketChannel,DatagramChannel,它們負責對指定的資源進行訪問讀寫,其中除了FileChannel都具有非阻塞的功能,ServerSocketChannel相當於一個服務器程序,它有accept方法監聽和接受客戶端的請求,而SocketChannel則對應於一個具體的Socket連接,它可以通過ServerSocketChannel的accept方法來得到,我們可以通過read和write對該連接信道進行讀寫,DatagramChannel是UDP數據報通信方式。在傳統的IO中,accept,read,write方法都是阻塞的方式進行的,也就是說accept方法負責接受一個客戶端請求,在未接受到一個請求之前線程就會阻塞在此處不能進行,因此要同時打開多個ServerSocketChannel必須要有多個線程支持,在非阻塞模式下,accept,read,write可以在直接返回,讓其他任務可以執行,這樣我們可以在一個線程中同時處理多個Channel,那你可能會問,accept直接返回了,請求來的時候如何在去調用accept接受呢,這就需要Selector來調度了,Selector的select()方法是阻塞的,它可以同時監聽對Channel的操作請求,將請求轉發到對應的channel中,從總體上看,把每個Channel中阻塞等待的行爲統一移到了Selector,從而我們可以在單線程中同時處理多個信道的讀寫任務。Buffer緩存則是我們對Channel進行讀寫的工具,它還提供了Char,Int等多種不同的視圖讓我們可以以不同的方式讀寫數據,也提供了Heap和Direct直接內存兩種緩存存儲方式。
下面我們對這3個關鍵的“部件”進行詳細的分析,當然我們應當明白不同的技術有不同的使用場景,這裏爲了突出NIO的特點我們集中於單線程(或少量線程)非阻塞的方式,它使用與高併發數據量處理少而簡單的場景。
2. Selector:
結合上面的論述,可以看到Selector起到了代替多個Channel監聽感興趣事件發生的作用,這讓我很容易想起一個設計模式——觀察者模式,在這裏Selector是Obserable,Channel是Observer,Channel要向Selector註冊自己對哪些事情感興趣,當事件發生時,Selector通知對應的Channel。
這裏註冊有個兩個部分:哪個channel和指定的事件,SelectionKey包含了註冊的要素:
2.1 SelectionKey(註冊感興趣的事,監聽返回準備好的事,它關聯一個Selector和Channel,我爲什麼忍不住想到迪米特法則,中毒太深...):
操作事件:
SelectionKey.OP_CONNECT
SelectionKey.OP_ACCEPT
SelectionKey.OP_READ
SelectionKey.OP_WRITE
因爲SelectionKey包含5個部分:
(1)interest集合和ready集合一樣包含有一些方便判斷的方法,可以看api或源碼;
(2)ready集合;
(3)channel引用;
(4)Selector引用;
(5)attach附加對象(可選);
2.2 Selector的重要方法:
(1)select方法:包括select(),selectNow(非阻塞),select(long timeout)返回int,有多個個ready的Channel;
(2)selectedKeys方法:返回ready的Channel的selectionKey集合,遍歷它們,根據readyOps集合處理對應事件;
(3)wakeUp方法:從select阻塞中喚醒;
(4)close方法:是所用selectionKey無效,也就釋放了對Channel們的引用不影響垃圾回收啦;
2.3 Selector使用示例:
public class SelectorSample {
private List<SelectableChannel> channels;
private boolean isListening = true;
public SelectorSample(List<SelectableChannel> channels) {
this.channels = channels;
}
public void doHandle() {
try(Selector selector = Selector.open()) {
for(SelectableChannel channel : channels) {
channel.configureBlocking(false); //非阻塞
channel.register(selector, SelectionKey.OP_ACCEPT | SelectionKey.OP_CONNECT
| SelectionKey.OP_READ | SelectionKey.OP_WRITE);
}
while(isListening) {
int ready = selector.select();
if(ready == 0) continue;
Set<SelectionKey> selectionKeys = selector.keys();
for(Iterator<SelectionKey> iterator = selectionKeys.iterator(); iterator.hasNext();) {
SelectionKey key = iterator.next();
if(key.isAcceptable()) {
System.out.println("doSomething when be acceptable");
} else if(key.isConnectable()) {
System.out.println("doSomething when be able to connect");
} else if(key.isReadable()) {
System.out.println("doSomething when be readable");
} else if(key.isWritable()) {
System.out.println("doSomething when be writable");
}
iterator.remove(); //注意要從就緒集合中刪除,下次就緒有selector添加
}
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
2. Channel :
Channel的體系中有:SelectableChannel和InterruptiableChannel,前者繼承自後者,之前說過FileChannel是不可以非阻塞的它屬於InterruptiableChannel,而其他3種進一步屬於SelectableChannel。
2.1 如何打開通道:
2.2 ServerSocketChannel:
線程安全;
public abstract class ServerSocketChannel
extends AbstractSelectableChannel
implements NetworkChannel
它的主要功能就是監聽的某個地址和端口上的套接字請求,並打開SocketChannel;2.3 SocketChannel:
public abstract class SocketChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel
ByteChannel實現了WritableChannel和ReadableChannel,因此它是可讀寫的;
Scatter/Gatther分別實現了將一個channel的內容讀到多個buffer(一個Buffer滿了才能讀到下一個)和多個Buffer寫到一個Channel的功能;
NetworkChannel:綁定到地址/端口的能力;
我們可以通過它來進行一個端到端的,有連接的套接字通信;
2.4 DatagramChannel:
線程安全的;
public abstract class DatagramChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, MulticastChannel
與SocketChannel接口上的不同在與MulticastChannel,它是NetworkChannel子類;增加了多播的功能,使得我們可以使用基於UDP套接字的多播功能;
2.5 Pipe:
管道一般可以在兩個線程中進行單向的數據傳輸,它有兩個嵌套類:SinkChannel和SourceChannel,分別負責在一個線程(發送者)寫入和在另一個線程中讀取:
public static abstract class SinkChannel
extends AbstractSelectableChannel
implements WritableByteChannel, GatheringByteChannel
public static abstract class SourceChannel
extends AbstractSelectableChannel
implements ReadableByteChannel, ScatteringByteChannel
基於上面的論述,相信已經可以很清除的明白它們各自有什麼功能了:都可以非阻塞,一個負責寫,並且可以將多個Buffer一起寫入,一個負責讀,可以從Channel中將數據讀入多個Buffer;
2.6 FileChannel:
這個Channel是阻塞的,是操作磁盤文件一種方式,它和之前幾個Channel大不相同,所以我要將它和下面要介紹的DirectByteBuffer(MappedByteBuffer)一起討論。
3. Buffer:
總的來說,具體工作中使用Buffer的次數要遠遠多與Selector和Channel,我們通過它對Channel進行具體的讀寫操作。
之前說過NIO的特點之一就是面向緩存,我們在使用Buffer時都是基於一塊分配指定的大小的固定內存進行操作的,只有兩種分配方式:Heap和Direct,它們的區別下面會詳細說明。無論我們進行視圖轉換(CharBuffer/IntBuffer等等),還是compact壓縮,還是duplicate複製、slice切片,都是最初的allocate分配那一塊內存。
3.1 Buffer的基本屬性和重要方法:
capacity:容量;
limit:可操作的限制位置;
position:下一個操作位置;
mark:標記;
address:使用direct內存時的內存地址;
capacity,limit,position這3個屬性是我們進行操作最關鍵和常用的,結合操作方法我們來看看它們的使用細節:
flip:limit=position,position=0;這個方法通常在從通道中讀取數據後使用,這樣我們可以再從Buffer中讀取數據;或者在Buffer寫入數據後調用,讓通道寫入;
rewind:position=0,mark=-1;
clear:position=0,limit=capacity,mark=-1;通常在重新從
remaining:limit-position;用於檢查是否還有數據;
mark和reset:mark標記,mark=position;reset復位,position=mark(mark>0時),注意它並不會修改mark值;
compact:將原來(limit-position)未處理完的數據複製到開頭,再將position移到數據的下一個位置,limit=capacity,這個方法是進行壓縮,去掉已處理過的數據,主要 爲了接下來將數據寫入Buffer,注意,它只是在原數組上進行復制的,沒有新分配空間;
另一個你可能需要注意的是equals方法和compareTo方法,它們比較的是limit-position之間的大小;
3.2 Buffer的體系結構:
來看看Buffer的體系:
Channel都是基於字節的,我們一般也從ByteBuffer開始;
ByteBuffer具體實現(分配):
ByteBuffer有兩個分配方法(它們返回HeapByteBuffer和DirectByteBuffer都是default包權限,我們無法直接使用它們):
allocate:HeapByteBuffer,從JVM堆中分配,收到JVM垃圾回收處理機制管理,實際上就是爲了一個固定的byte[];
allocateDirect:DirectByteBuffer,使用JNI在native內存中分配,那怎麼回收直接內存呢,DirectBuffer(DirectByteBuffer的接口),可以返回Cleaner,通過它我們可以釋放直接內存,否則你就只能等待Full GC的發生來釋放它了;
Buffer的視圖:
在類圖中我們可以看到有CharBuffer,IntBuffer,另外還有FloatBuffer,DoubleBuffer,ShortBuffer,LongBuffer以及MappedBuffer(特別的,內存映射);
它們實際上都是由ByteBuffer而產生,操作同一塊內存,只是讀取的方式不一樣,以HeapByteBuffer和CharBuffer爲例我們來看看它們是怎麼完成“視圖”的使命的。
轉換方法:
ByteBuffer.asCharBuffer();
HeapByteBuffer中是這樣實現的:
public CharBuffer asCharBuffer() {
int size = this.remaining() >> 1;
int off = offset + position();
return (bigEndian
? (CharBuffer)(new ByteBufferAsCharBufferB(this,
-1,
0,
size,
size,
off))
: (CharBuffer)(new ByteBufferAsCharBufferL(this,
-1,
0,
size,
size,
off)));
}
考慮到字順這裏有兩個實現,新建一個ByteBufferAsCharBufferB還是操作那一塊內存,只是我們換了一組capacity,limit,mark,position來操作;
而在具體的get/put方法中:
public char get() {
return Bits.getCharB(bb, ix(nextGetIndex()));
}
是通過Bits這個工具類來進行不同基本類型的讀取和操作。整個事情就是這樣,基於字節ByteBuffer,考慮字順用不同的方式去讀寫同一塊內存;
PS:對於CharBuffer和ByteBuffer之間的轉換,涉及到編解碼,Charset有ByteBuffer = encode(CharBuffer)和CharBuffer = decode(ByteBuffer);
4. FileChannel
4.1 打開FileChannel:
public final FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, true, rw, this);
}
return channel;
}
}
FileOutputStream:public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, false, true, append, this);
}
return channel;
}
}
FileInputStream:public FileChannel getChannel() {
synchronized (this) {
if (channel == null) {
channel = FileChannelImpl.open(fd, path, true, false, this);
}
return channel;
}
}
正如RandomAccessFile(可以seek方式前後讀寫文件),FileInputStream和FileOutputStream本身的差異一樣,它們也具有不同的特點;4.2 FileChannel的獨特的方法:
4.3 transfer:
4.4 FileChannel中的內存映射:
static MappedByteBuffer newMappedByteBuffer(int var0, long var1, FileDescriptor var3, Runnable var4) {
if(directByteBufferConstructor == null) {
initDBBConstructor();
}
try {
MappedByteBuffer var5 = (MappedByteBuffer)directByteBufferConstructor.newInstance(new Object[]{new Integer(var0), new Long(var1), var3, var4});
return var5;
} catch (IllegalAccessException | InvocationTargetException | InstantiationException var7) {
throw new InternalError(var7);
}
}
這裏可以看到顯然是通過直接內存的方式,但是這段代碼是在FileChannel的map方法中調用的,它和allocateDirect是有區別的;要理解直接內存和內存映射中的原理需要一些重要的基礎知識,才能真正弄清楚HeapByteBuffer,DirectByteBuffer,和map之間的區別。4.5 不同讀取方式的對比:
public class DirectMemoryTest {
private static final String TEST_FILE = "/home/yjh/test.file";
public static void testNormal(String TEST_FILE) {
System.out.print("Normal ");
try(FileInputStream inputStream = new FileInputStream(TEST_FILE);
FileChannel fileChannel = inputStream.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while(fileChannel.read(buffer) != -1) {
buffer.flip();
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void testDirect(String TEST_FILE) {
System.out.print("Direct ");
try(FileInputStream inputStream = new FileInputStream(TEST_FILE);
FileChannel fileChannel = inputStream.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocateDirect(1024);
while(fileChannel.read(buffer) != -1) {
buffer.flip();
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void testMapped(String TEST_FILE) {
System.out.print("Mapped ");
try(FileInputStream inputStream = new FileInputStream(TEST_FILE);
FileChannel fileChannel = inputStream.getChannel()) {
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY, 0, fileChannel.size());
byte[] b = new byte[1024];
while(buffer.get(b).position() < buffer.limit()) {
}
} catch (IOException e) {
e.printStackTrace();
}
}
public static void test(Consumer<String> consumer) {
long startTime = System.currentTimeMillis();
consumer.accept(TEST_FILE);
long endTime = System.currentTimeMillis();
System.out.println("Time consume: " + (endTime - startTime));
}
public static void main(String[] args) {
test(DirectMemoryTest::testNormal);
test(DirectMemoryTest::testDirect);
test(DirectMemoryTest::testMapped);
}
}
我對三種方式分別讀取整個test文件的內容,該文件大小爲500M,運行結果(單位:ms):Direct Time consume: 800
Mapped Time consume: 222