Java 中的io模型詳解
1. BIO
我們先看一個(gè) Java 例子:
package cn.bridgeli.demo; import java.io.BufferedReader; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.net.ServerSocket; import java.net.Socket; /** * @author bridgeli */ public class SocketBIO { public static void main(String[] args) throws Exception { ServerSocket server = new ServerSocket(9090, 20); System.out.println("step1: new ServerSocket(9090) "); while (true) { Socket client = server.accept(); System.out.println("step2:client: " + client.getPort()); new Thread(new Runnable() { @Override public void run() { InputStream inputStream = null; BufferedReader reader = null; try { inputStream = client.getInputStream(); reader = new BufferedReader(new InputStreamReader(inputStream)); while (true) { String dataLine = reader.readLine(); //阻塞2 if (null != dataLine) { System.out.println(dataLine); } else { client.close(); break; } } System.out.println("客戶端斷開"); } catch (IOException e) { e.printStackTrace(); } finally { if (null != reader) { try { reader.close(); } catch (IOException e) { e.printStackTrace(); } } if (null!= inputStream) { try { inputStream.close(); } catch (IOException e) { e.printStackTrace(); } } } } }).start(); } } }
BIO 是最初始的 IO 模型,該模型有兩個(gè)大問(wèn)題:1. accept 是阻塞的;2. read 也是阻塞的,也就是說(shuō)我們的服務(wù)器起來(lái)之后,首先會(huì)在 accept 處阻塞,等待客戶端連接,但有一個(gè)客戶端連接的時(shí)候,我們可以從客戶端處讀取數(shù)據(jù),這個(gè)時(shí)候也是阻塞的,所以我們的系統(tǒng)只能是單連接的,當(dāng)有多個(gè)客戶端連接的時(shí)候,只能一個(gè)一個(gè)的排著隊(duì)連接,然后從客戶端中讀取數(shù)據(jù),為了實(shí)現(xiàn)多連接,這就要求我們必須啟用線程來(lái)解決,最開始等待客戶端連接,然后有一個(gè)客戶端連上了之后,啟動(dòng)一個(gè)線程讀取客戶端的數(shù)據(jù),然后主線程繼續(xù)等待客戶端連接。
該模型最大的問(wèn)題就是缺乏彈性伸縮能力,當(dāng)客戶端并發(fā)訪問(wèn)量增加后,服務(wù)端的線程個(gè)數(shù)和客戶端并發(fā)訪問(wèn)數(shù)呈1:1的正比關(guān)系,Java 中的線程也是比較寶貴的系統(tǒng)資源,線程數(shù)量快速膨脹后,系統(tǒng)的性能將急劇下降,隨著訪問(wèn)量的繼續(xù)增大,系統(tǒng)最終就死掉了。當(dāng)然不僅僅是 Java,我們直接設(shè)想假設(shè)有一萬(wàn)個(gè)客戶端連接到服務(wù)端,服務(wù)端要開一萬(wàn)個(gè)線程,那么這個(gè)時(shí)候服務(wù)端光開線程要占用多少資源?需要多大內(nèi)存?操作系統(tǒng)為了調(diào)度這些線程 CPU 是不是也要被占用完了?
為了解決此問(wèn)題,有人對(duì)服務(wù)器的線程模型進(jìn)行優(yōu)化,服務(wù)端采用線程池來(lái)處理多個(gè)客戶端請(qǐng)求。但是同樣是有問(wèn)題的,
1. 線程總數(shù)有限,又要等待;
2. 多余的連接會(huì)堆積在任務(wù)隊(duì)列中,當(dāng)任務(wù)隊(duì)列滿了,那么此時(shí)就開始啟用拒絕策略了,所以還是沒有從根本上解決問(wèn)題。
2. NIO
BIO 最大的問(wèn)題,在于 B,block,阻塞,所以只要解決了這個(gè)問(wèn)題就可以,那么此時(shí) NIO 應(yīng)運(yùn)而生,N 就是 non-block 的意思(Java 中是 new 的意思),同樣先看一個(gè)例子:
package cn.bridgeli.demo; import java.net.InetSocketAddress; import java.nio.ByteBuffer; import java.nio.channels.ServerSocketChannel; import java.nio.channels.SocketChannel; import java.util.LinkedList; /** * @author bridgeli */ public class SocketNIO { public static void main(String[] args) throws Exception { LinkedList<SocketChannel> clients = new LinkedList<>(); ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.bind(new InetSocketAddress(9090)); serverSocketChannel.configureBlocking(false); while (true) { SocketChannel client = serverSocketChannel.accept(); if (null != client) { client.configureBlocking(false); System.out.println("client port: " + client.socket().getPort()); clients.add(client); } ByteBuffer buffer = ByteBuffer.allocateDirect(4096); for (SocketChannel c : clients) { int num = c.read(buffer); if (num > 0) { buffer.flip(); byte[] aaa = new byte[buffer.limit()]; buffer.get(aaa); String b = new String(aaa); System.out.println(c.socket().getPort() + " : " + b); buffer.clear(); } } } } }
這個(gè)時(shí)候我們會(huì)發(fā)現(xiàn)連接和讀取都是非阻塞的了,由于都是非阻塞的,所以這就要求我們需要有一個(gè)集合,用來(lái)存儲(chǔ)所有的連接,然后從連接中讀取數(shù)據(jù)。這個(gè)模型解決了我們需要開線程的問(wèn)題,沒循環(huán)一次,如果有新連接過(guò)來(lái),我們就把連接放到集合中,然后挨個(gè)讀取連接中的數(shù)據(jù),此時(shí)就不需要我們每連接每線程了,但是還是有一個(gè)問(wèn)題,隨著連接的增加,我們的隊(duì)列會(huì)越來(lái)越大,而且我們每次都要遍歷所有的連接讀取數(shù)據(jù),我們還假設(shè)有一萬(wàn)個(gè)連接,但是前 9999 個(gè)連接都沒有數(shù)據(jù),只有最后一個(gè)連接有數(shù)據(jù),那前 9999 次讀取都是浪費(fèi)。
3. 多路復(fù)用
為了解決 NIO 中無(wú)效讀取的問(wèn)題,這個(gè)時(shí)候我們可以根據(jù)事件監(jiān)聽,告訴操作系統(tǒng)說(shuō),我們監(jiān)聽那些事件,然后當(dāng)這些事件有數(shù)據(jù)到達(dá)時(shí)通知我們?nèi)プx取,例子如下:
package cn.bridgeli.demo; 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; /** * @author bridgeli */ public class SocketMultiplexingIO { private ServerSocketChannel serverSocketChannel = null; private Selector selector = null; public void initServer() { try { serverSocketChannel = ServerSocketChannel.open(); serverSocketChannel.configureBlocking(false); serverSocketChannel.bind(new InetSocketAddress(9090)); selector = Selector.open(); serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); } catch (IOException e) { e.printStackTrace(); } } public void start() { initServer(); System.out.println("服務(wù)器啟動(dòng)了..."); try { while (true) { while (selector.select() > 0) { Set<SelectionKey> selectionKeys = selector.selectedKeys(); Iterator<SelectionKey> iterator = selectionKeys.iterator(); while (iterator.hasNext()) { SelectionKey key = iterator.next(); iterator.remove(); if (key.isAcceptable()) { acceptHandler(key); } else if (key.isReadable()) { readHandler(key); } } } } } catch (IOException e) { e.printStackTrace(); } } public void acceptHandler(SelectionKey key) { try { ServerSocketChannel ssc = (ServerSocketChannel) key.channel(); SocketChannel client = ssc.accept(); client.configureBlocking(false); ByteBuffer buffer = ByteBuffer.allocate(8192); client.register(selector, SelectionKey.OP_READ, buffer); System.out.println("新客戶端:" + client.getRemoteAddress()); } catch (IOException e) { e.printStackTrace(); } } public void readHandler(SelectionKey key) { SocketChannel client = (SocketChannel) key.channel(); ByteBuffer buffer = (ByteBuffer) key.attachment(); buffer.clear(); int read = 0; try { while (true) { read = client.read(buffer); if (read > 0) { buffer.flip(); while (buffer.hasRemaining()) { client.write(buffer); } buffer.clear(); } else if (read == 0) { break; } else { client.close(); break; } } } catch (IOException e) { e.printStackTrace(); } } public static void main(String[] args) { SocketMultiplexingIO service = new SocketMultiplexingIO(); service.start(); } }
再多路復(fù)用中,有 poll、epoll、Selector 等實(shí)現(xiàn)方式,其中他們的區(qū)別是,poll 需要我們每次告訴操作系統(tǒng)說(shuō),我們都要關(guān)注哪些事件,而 epoll 是操作系統(tǒng)會(huì)開辟一塊內(nèi)存區(qū)域,存儲(chǔ)下我們要關(guān)注的事件,不用每次都告訴操作系統(tǒng)我們關(guān)注哪些事件。
關(guān)于 BIO、NIO、多路復(fù)用,馬士兵教育的周志磊老師有一個(gè)很形象的例子。BIO 是阻塞的,所以需要我們每連接每線程,就相當(dāng)于我們?yōu)槊恳惠v車在收費(fèi)站修建一條路,每來(lái)一輛車就要修一條路,我們我們自己從車上卸下裝的貨;NIO 是非阻塞的,我們就需要我們每次都跑到收費(fèi)站,然后看我們修好的路上面車來(lái)了沒有,沒有來(lái)的話,等下次在看,來(lái)的話,我們卸下貨,再等下次看有沒有新貨;多路復(fù)用中的 poll,就是我們?cè)谑召M(fèi)站安裝一個(gè)電話機(jī),然后我們每次打電話,我關(guān)注的哪些路是否有車來(lái)了,需要我卸貨,而 epoll 是我們不僅在收費(fèi)站安裝了一個(gè)電話機(jī),我們還留下了一個(gè)本子,我們每次打電話的時(shí)候,會(huì)把我們新關(guān)注的路告訴收費(fèi)站,收費(fèi)站在本子上記下我們關(guān)注的那些路,假設(shè)我們關(guān)注一萬(wàn)條路,這樣就不需要我們每次在電話中每次把這一萬(wàn)條路說(shuō)一邊,問(wèn)這些路是否有車來(lái)了,需要我們卸貨。
最后再說(shuō)幾個(gè)小問(wèn)題
1. 我們學(xué)習(xí) IO 模型,IO 模型是操作系統(tǒng)提供給我們的接口,屬于系統(tǒng)調(diào)用,所以我們可以通過(guò) strace 追蹤到每一個(gè)程序所執(zhí)行的系統(tǒng)調(diào)用。命令如下:
strace -ff -o out + 要追蹤的進(jìn)程
2. 當(dāng)我們追蹤 BIO 的時(shí)候,因?yàn)?JDK 的優(yōu)化,所以如果使用高版本的 JDK,也不會(huì)看到阻塞,這個(gè)時(shí)候你可以通過(guò) JDK1.4 編譯運(yùn)行(這也是為什么我們使用 lambda 表達(dá)式和 try-with-resource 的原因)
3. IO 調(diào)用屬于系統(tǒng)調(diào)用,所以從 BIO -> NIO -> 多路復(fù)用,是操作系統(tǒng)的進(jìn)步,而我們各種變成語(yǔ)言寫的屬于應(yīng)用,所以有沒有 異步非阻塞IO 模型,這樣看操作系統(tǒng)底層有沒有這樣的模型,需要操作系統(tǒng)給我們提供 異步非阻塞IO 相關(guān)的接口,我們的應(yīng)用才能進(jìn)一步優(yōu)化
4. 我們通過(guò) strace 追蹤到的每一個(gè)系統(tǒng)調(diào)用,都可以通過(guò) man 命令查看文檔(僅限 linux 系統(tǒng),非 Windows 系統(tǒng)),如果沒有 man 命令,安裝一下就可以了。
以上就是Java 中的io模型詳解的詳細(xì)內(nèi)容,更多關(guān)于Java io模型的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Java實(shí)現(xiàn)企業(yè)員工管理系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)企業(yè)員工管理系統(tǒng),文中示例代碼介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2022-02-02Java設(shè)計(jì)模式之動(dòng)態(tài)代理模式實(shí)例分析
這篇文章主要介紹了Java設(shè)計(jì)模式之動(dòng)態(tài)代理模式,結(jié)合實(shí)例形式分析了動(dòng)態(tài)代理模式的概念、功能、組成、定義與使用方法,需要的朋友可以參考下2018-04-04基于SpringBoot + Redis實(shí)現(xiàn)密碼暴力破解防護(hù)
在現(xiàn)代應(yīng)用程序中,保護(hù)用戶密碼的安全性是至關(guān)重要的,密碼暴力破解是指通過(guò)嘗試多個(gè)密碼組合來(lái)非法獲取用戶賬戶的密碼,為了保護(hù)用戶密碼不被暴力破解,我們可以使用Spring Boot和Redis來(lái)實(shí)現(xiàn)一些防護(hù)措施,本文將介紹如何利用這些技術(shù)來(lái)防止密碼暴力破解攻擊2023-06-06Mybatis3中方法返回生成的主鍵:XML,@SelectKey,@Options詳解
這篇文章主要介紹了Mybatis3中方法返回生成的主鍵:XML,@SelectKey,@Options,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-01-01java反射機(jī)制的一些學(xué)習(xí)心得小結(jié)
這篇文章主要給大家介紹了關(guān)于java反射機(jī)制的一些學(xué)習(xí)心得,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2021-02-02關(guān)于Java中properties文件編碼問(wèn)題
這篇文章主要介紹了關(guān)于Java中properties文件編碼問(wèn)題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2021-11-11