從JDK1.5開始,java引入了java.nio包,nio的含義爲非阻塞型IO。這一篇就從使用到源碼來簡單瞭解一下NIO吧。
一、基本用法
RandomAccessFile aFile = null;
try {
aFile = new RandomAccessFile(NIOTest.class.getClassLoader().getResource("nio.txt").getPath(), "rw");
FileChannel fileChannel = aFile.getChannel();
ByteBuffer buf = ByteBuffer.allocate(1024);
int bytesRead = fileChannel.read(buf);
System.out.println(bytesRead);
while (bytesRead != -1) {
buf.flip();
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
buf.compact();
bytesRead = fileChannel.read(buf);
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (aFile != null) {
aFile.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
如上例程,讀取一個resource下的nio.txt文件,獲取其對應Channel,定義一個ByteBuffer,然後調用read方法。
這裏的Channel與傳統阻塞型IO中的Stream類似,不同在於,Stream的讀寫是流的形式的,也就是一個個字節讀的,而Channel卻是直接傳入一個Buffer塊,然後就然後那樣嗯。
所以我們就可以得出NIO和傳統IO的兩大區別:1.面向塊而非面向流;2.顧名思義,可以非阻塞。
二、非阻塞的好處
既然NIO是非阻塞的,那麼非阻塞IO到底有什麼好處呢?
既然要思考非阻塞的好處,我們自然要看看阻塞通常被用在哪。最典型的例子就是Socket,而我們這裏就使用非阻塞的Socket:SocketChannel來展示下非阻塞IO的作用:
Selector selector = null;
ServerSocketChannel ssc = null;
try {
selector = Selector.open();
ssc = ServerSocketChannel.open();
ssc.socket().bind(new InetSocketAddress(PORT));
ssc.configureBlocking(false);
ssc.register(selector, SelectionKey.OP_ACCEPT);
while (true) {
if (selector.select(TIMEOUT) == 0) {
System.out.println("==");
continue;
}
Iterator<SelectionKey> iter = selector.selectedKeys().iterator();
while (iter.hasNext()) {
SelectionKey key = iter.next();
if (key.isAcceptable()) {
handleAccept(key);
}
if (key.isReadable()) {
handleRead(key);
}
if (key.isConnectable()) {
System.out.println("isConnectable = true");
}
iter.remove();
}
}
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (selector != null) {
selector.close();
}
if (ssc != null) {
ssc.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
這是一個Socket服務端的實現,利用非阻塞服務端ServerSocketChannel和選擇器Selector完美展示如何在單一的線程中監聽多個客戶端的請求。
傳統的IO型ServerSocket通常只在一個線程中完成accept,此後的read則是爲Socket單獨開啓一個線程,通過阻塞的方式去監聽。而這裏,accept、read是平級的,他們都在主線程中完成,而在read操作中,可以從key中獲取具體的SocketChannel:
public static void handleRead(SelectionKey key) throws IOException {
SocketChannel sc = (SocketChannel) key.channel();
ByteBuffer buf = (ByteBuffer) key.attachment();
long bytesRead = sc.read(buf);
while (bytesRead > 0) {
buf.flip();
while (buf.hasRemaining()) {
System.out.print((char) buf.get());
}
System.out.println();
buf.clear();
bytesRead = sc.read(buf);
}
if (bytesRead == -1) {
sc.close();
}
}
一個線程,可以完成多個客戶端的IO讀取,究其原因自然是因爲其非阻塞的特性,他雖然也會不斷地輪詢IO,但是並不因爲沒有讀到數據而阻塞IO端口,而是返回一個null,因此他可以在一個線程中同時監聽多個IO端,這給降低併發壓力帶來可能。而許多框架(如Netty)就是這麼去實現的。
三、實現源碼
粗略閱讀發現,NIO的源碼實現相當複雜,方便起見,這裏只粗略看看部分的源碼,只說明一下其中注意到的細節,暫時不整個流程地去解讀源碼了。
3.1 ServerSocketChannel的啓動
ServerSocket socket;
ServerSocketChannelImpl(SelectorProvider var1) throws IOException {
super(var1);
this.fd = Net.serverSocket(true);
this.fdVal = IOUtil.fdVal(this.fd);
this.state = 0;
}
public ServerSocket socket() {
Object var1 = this.stateLock;
synchronized(this.stateLock) {
if (this.socket == null) {
this.socket = ServerSocketAdaptor.create(this);
}
return this.socket;
}
}
從源碼不難發現,ServerSocketChannel的啓動最根本的還是使用了ServerSocket。
3.2 ServerSocketChannel和Selector的綁定
public final SelectionKey register(Selector sel, int ops,
Object att)
throws ClosedChannelException
{
synchronized (regLock) {
if (!isOpen())
throw new ClosedChannelException();
if ((ops & ~validOps()) != 0)
throw new IllegalArgumentException();
if (blocking)
throw new IllegalBlockingModeException();
SelectionKey k = findKey(sel);
if (k != null) {
k.interestOps(ops);
k.attach(att);
}
if (k == null) {
// New registration
synchronized (keyLock) {
if (!isOpen())
throw new ClosedChannelException();
k = ((AbstractSelector)sel).register(this, ops, att);
addKey(k);
}
}
return k;
}
}
從代碼中可以看出,register的過程其實是一個雙向綁定的過程。
protected final SelectionKey register(AbstractSelectableChannel var1, int var2, Object var3) {
if (!(var1 instanceof SelChImpl)) {
throw new IllegalSelectorException();
} else {
SelectionKeyImpl var4 = new SelectionKeyImpl((SelChImpl)var1, this);
var4.attach(var3);
Set var5 = this.publicKeys;
synchronized(this.publicKeys) {
this.implRegister(var4);
}
var4.interestOps(var2);
return var4;
}
}
selector的register將ServerSocketChannel傳給了SelectionKey的實現類,然後調用implRegister,這個方法因操作系統而異,在windows中:
protected void implRegister(SelectionKeyImpl var1) {
Object var2 = this.closeLock;
synchronized(this.closeLock) {
if (this.pollWrapper == null) {
throw new ClosedSelectorException();
} else {
this.growIfNeeded();
this.channelArray[this.totalChannels] = var1;
var1.setIndex(this.totalChannels);
this.fdMap.put(var1);
this.keys.add(var1);
this.pollWrapper.addEntry(this.totalChannels, var1);
++this.totalChannels;
}
}
}
可以看到這裏把var1傳給了一個Wrapper。最終這玩意調用了一個native的方法,具體就不多說了。
3.3 Selector的select
依然看windows下的實現。
protected int doSelect(long var1) throws IOException {
if (this.channelArray == null) {
throw new ClosedSelectorException();
} else {
this.timeout = var1;
this.processDeregisterQueue();
if (this.interruptTriggered) {
this.resetWakeupSocket();
return 0;
} else {
this.adjustThreadsCount();
this.finishLock.reset();
this.startLock.startThreads();
try {
this.begin();
try {
this.subSelector.poll();
} catch (IOException var7) {
this.finishLock.setException(var7);
}
if (this.threads.size() > 0) {
this.finishLock.waitForHelperThreads();
}
} finally {
this.end();
}
this.finishLock.checkForException();
this.processDeregisterQueue();
int var3 = this.updateSelectedKeys();
this.resetWakeupSocket();
return var3;
}
}
}
private int updateSelectedKeys() {
++this.updateCount;
byte var1 = 0;
int var4 = var1 + this.subSelector.processSelectedKeys(this.updateCount);
WindowsSelectorImpl.SelectThread var3;
for(Iterator var2 = this.threads.iterator(); var2.hasNext(); var4 += var3.subSelector.processSelectedKeys(this.updateCount)) {
var3 = (WindowsSelectorImpl.SelectThread)var2.next();
}
return var4;
}
從這裏可以看出,它通過遍歷選擇器擁有的線程,然後將其轉變爲SelectThread,調用其subSelector.processSelectedKeys()方法,最終完成了select的操作。
總結
對於NIO的使用,網上的教程非常多,而且也有Netty這樣的框架使用它,原本想要從源碼角度去對NIO進行解讀,但是博客寫的並不如意。事實上NIO本身就是操作系統支持的東西,並非單純JAVA層面的東西,java.nio包中大部分都是接口,具體的實現類都在sun.nio包中,其中的實現代碼也是非常的晦澀難懂,變量的命名幾乎和代碼混淆了一般全是var1var2的命名,但是其基本原理也是不難看出來。所謂Selector,其實無非是把ServerSocketChannel綁定在內部,內部又擁有用於輪訓的Thread,可以切換式地將IO放入Thread中讀取,其最終調用的也是底層的操作系統開放的接口。
這篇博客寫的不如意,NIO的源碼比先前想象的複雜多,以後可能會繼續寫NIO的博客來完善,這篇姑且做個伏筆。
此外最近要找工作,源碼的解讀先告一段落,接下來可能會根據面試複習的內容去寫一寫讀書筆記(JVM、操作系統、數據庫、網絡等等)。