java之NIO基礎(chǔ)

本文大綱如下:

  • 1、什么是NIO
  • 2、為什么使用NIO
  • 3藏斩、NIO的基本使用
  • 4嫂便、BIO、NIO丢烘、AIO區(qū)別以及總結(jié)
NIO大綱

一、什么是NIO

NIO是JDK1.4新加入的,為了解決BIO(原始IO的不足)的非阻塞IO模型导披。原始的IO流是單向的,如InputStream只能讀取不能寫入埃唯,而NIO是面向通道(Channel)和緩沖區(qū)(Buffer)撩匕,NIO可以輸入也可以輸出。

二墨叛、為什么使用NIO

探討NIO之前止毕,肯定先說明BIO的不足模蜡,只有在BIO滿足不了需求的時候,才會出現(xiàn)新的技術(shù)代替扁凛。如下圖所示忍疾,因為BIO是一個阻塞的IO模型,所以需要為每個IO配備一個線程去監(jiān)聽調(diào)度谨朝,如果在高并發(fā)的情況下卤妒,開辟新的線程以及切換線程會帶來巨大的損耗。但是對于比較簡單的文件讀寫字币,還是使用BIO则披。


BIO模型

NIO的出現(xiàn)解決了BIO的低效率不能高并發(fā)的問題,所以NIO不是阻塞的IO模型洗出。他只需要調(diào)用一個線程去輪詢Selector士复,是否有新的事件出現(xiàn)即可。再通過事件找到具體的Channel翩活,這樣就可以將N的線程降低為一個線程了阱洪。如下圖所示,只需要單個線程監(jiān)聽Selector隅茎,當對應(yīng)的Channel有消息到達時候澄峰,再調(diào)用工作線程去處理,這樣可以減少了數(shù)據(jù)未到達之前的等待時間辟犀。


NIO模型

無論是輸入還是輸出俏竞,NIO中Channel和Buffer是配對使用的。在輸入(Input)時堂竟,Channel的數(shù)據(jù)讀取到Buffer中魂毁,使用者再從Buffer中讀取數(shù)據(jù)。輸出反之出嘹。調(diào)度者直接調(diào)度的只能是Buffer席楚。


數(shù)據(jù)流流程

三、NIO的基本用法

1)税稼、Channel烦秩、Buffer、Selector組件的介紹

① 郎仆、Channel通道

Channel通道是 I/O 傳輸發(fā)生時通過的入口只祠。當Buffer數(shù)據(jù)寫入到Channel時,他就開始傳輸了扰肌。反之當Channel接收到數(shù)據(jù)時抛寝,Buffer就可以讀取了。Channel是真正開始數(shù)據(jù)傳輸?shù)摹?/p>

對應(yīng)的Channel主要有:

  • FileChannel 讀寫文件
  • DatagramChannel 讀寫UDP數(shù)據(jù)
  • SocketChannel 讀寫TCP數(shù)據(jù)
  • ServerSocketChannel 監(jiān)聽TCP鏈接,并為服務(wù)端創(chuàng)建一個SocketChannel

②盗舰、Buffer緩沖區(qū)

Buffer即緩沖區(qū)晶府。在NIO模式下,數(shù)據(jù)并非立刻到達的钻趋,在數(shù)據(jù)沒到達之前都是阻塞的川陆,所以在NIO中才引入緩沖區(qū)概念,數(shù)據(jù)到達時先寫入到Buffer中爷绘,再交給調(diào)用者處理緩沖區(qū)书劝。這樣處理者線程就不需要阻塞了进倍。

對應(yīng)的Buffer有:

  • ByteBuffer 以字節(jié)為單位
  • CharBuffer 以Char為單位
  • DoubleBuffer 以Double為單位
  • FloatBuffer 以Float為單位
  • IntBuffer 以Int為單位
  • LongBuffer 以Long為單位
  • ShortBuffer 以Short為單位
Buffer的屬性:
  • private int mark = -1;
  • private int position = 0;
  • private int limit;
  • private int capacity;

Buffer中共有mask土至、position、limit猾昆、capacity屬性陶因,其中主要的是position、limit垂蜗、capacity楷扬。

capacity: capacity是容器的大小,即緩沖區(qū)的大小贴见。

limit : capacity意味著緩沖區(qū)的大小烘苹,但并不是所有的緩沖區(qū),使用者都能用的上片部,使用者可以根據(jù)需要镣衡,劃分出自己所需要的大小,但不能大于了capacity档悠,這就是limit的意義廊鸥。寫的模式下,limit代表了辖所,最多可以寫多少惰说。讀模式下,意味著最多可以讀到多少缘回。當從寫模式切換至讀模式吆视,即調(diào)用flip()函數(shù)后,limit=position酥宴,position=0啦吧;這樣就可以開始讀buffer了。

position:在寫的模式下幅虑,position為0丰滑,position代表下一個寫入的位置,所以position最多不能大于limit-1;在讀模式下褒墨,position代表的是下一個讀取的位置炫刷,大小同樣不能大于limit-1。在初始狀態(tài)下或者調(diào)用flip函數(shù)時郁妈,position會重新賦值等于0浑玛;

mark: mark從字面意思就是標記的意思,它就是當前position的一個快照噩咪。通過reset函數(shù)會讓position重新賦值為mark顾彰。

Buffer中核心API:

Buffer既然是個內(nèi)存,它的API就是對該內(nèi)存位置的管理標記胃碾,即對屬性管理涨享。下面只介紹部分方法

  • clear() 當該buffer中的數(shù)據(jù)處理完的時候,就可以調(diào)用該方法仆百,他會使buffer變成初始狀態(tài)厕隧。
  • compact() 于clear一樣,compact()也是清除的作用俄周,但是他只清除讀取完的數(shù)據(jù)吁讨,把未讀取的數(shù)據(jù)copy到首部,移動position和limit
  • hasRemaining() 代表是否有下一個位置可以處理峦朗。讀時是不是讀完了建丧,寫時為是不是寫完了。通常是在這操作之前判斷波势。

③翎朱、Selector選擇器

當應(yīng)用中有大量的Channel的時候,即IO連接時艰亮。還得有大量的線程去監(jiān)聽數(shù)據(jù)狀態(tài)闭翩。為此NIO加入了Selector,由Channel向Selector注冊迄埃,并支持一個Selector可以注冊多個Channel疗韵。將監(jiān)聽 Channel換成監(jiān)聽Selector,從而減少線程使用侄非。

由于Channel可以進行讀或者寫蕉汪,所以向Selector注冊時,需要表明是監(jiān)聽哪一種事件逞怨。最終輪詢Selector即可知道哪種事件到達了者疤。

Selector事件如下:

  • OP_ACCEPT TCP服務(wù)端接收到客戶端連接事件
  • OP_CONNECT 該連接是否已經(jīng)斷開事件
  • OP_READ 可以讀取該數(shù)據(jù)事件
  • OP_WRITE 可以寫入數(shù)據(jù)事件

2)、NIO基本用法

① 叠赦、FileChannel

FileChannel是讀取文件時的Channel

讀取文件例子如下:

    public static void main(String[] args) throws IOException {
        RandomAccessFile file = new RandomAccessFile("D:\\proguard-rules.pro", "rw");
        FileChannel channel = file.getChannel();

        // 分配緩存空間
        ByteBuffer buf = ByteBuffer.allocate(48);
        // buf讀取通道的數(shù)據(jù) length為讀到的數(shù)據(jù)長度
        int length = channel.read(buf);
        byte[] bytes = new byte[48];
        while (length != -1) {
            // buf由寫的狀態(tài) 切換至讀狀態(tài)
            buf.flip();
            while(buf.hasRemaining()){
                buf.get(bytes,0,length);
                System.out.print(new String(bytes));
            }
            // 清空buf數(shù)據(jù)
            buf.clear();
            length = channel.read(buf);
        }
        file.close();
    }

上訴代碼只是簡單的讀取文件驹马,雖然簡單,但包含了NIO的基本用法流程。生產(chǎn)Buffer對象有兩種方法:

  • ByteBuffer buf = ByteBuffer.allocate(48);
  • ByteBuffer buf = ByteBuffer.wrap(bytes,0,length);
transferTo(long position, long count,
                                    WritableByteChannel target)

現(xiàn)代的操作系統(tǒng)分為內(nèi)核態(tài)和用戶態(tài)糯累,文件的copy是從內(nèi)核態(tài) ->用戶態(tài) ->內(nèi)核態(tài)算利,它需要經(jīng)過用戶態(tài)才能copy。FileChannel中的拷貝transferTo(transferTo類似)泳姐,直接從內(nèi)核態(tài)到內(nèi)核態(tài)效拭,效率比較高。

② 胖秒、DatagramChannel

DatagramChannel是接收發(fā)送UDP的缎患,而UDP是面向數(shù)據(jù)包,而非數(shù)據(jù)流的阎肝。

發(fā)送數(shù)據(jù)包:

        DatagramChannel channel = DatagramChannel.open();

        // 發(fā)送數(shù)據(jù)
        String data = "DatagramChannel...";
        ByteBuffer writeBuffer = ByteBuffer.allocate(48);
        writeBuffer.clear();
        writeBuffer.put(data.getBytes());
        writeBuffer.flip();
        int len = channel.send(writeBuffer, new InetSocketAddress("127.0.0.1", 5520));
        System.out.println(len);

接收數(shù)據(jù)包:

        DatagramChannel channel = DatagramChannel.open();
        // 接收綁定端口
        channel.socket().bind(new InetSocketAddress(5521));
        ByteBuffer readBuffer = ByteBuffer.allocate(48);

        channel.receive(readBuffer);
        readBuffer.flip();
        byte[] bytes = new byte[48];
        while (readBuffer.hasRemaining())  {
            readBuffer.get(bytes,0,readBuffer.limit());
            System.out.print(new String(bytes));
        }

③ 挤渔、ServerSocketChannel

利用ServerSocketChannel監(jiān)聽TCP鏈接

       Selector selector = Selector.open();

        ServerSocketChannel server = ServerSocketChannel.open();
        // 設(shè)置為非阻塞,沒有連接時也會返回null
        server.configureBlocking(false);
        // 綁定本地端口
        server.socket().bind(new InetSocketAddress(5510));
        // 注冊客戶端連接到達監(jiān)聽
        server.register(selector, SelectionKey.OP_ACCEPT);

        while (true) {
            try {
                if (selector.select() == 0) {
                    continue;
                }

                Iterator<SelectionKey> iterator = selector.selectedKeys().iterator();
                while (iterator.hasNext()) {

                    SelectionKey key = iterator.next();
                    iterator.remove();
                    // 客戶端到達狀態(tài)
                    if (key.isAcceptable()) {
                        ServerSocketChannel serverSocketChannel = (ServerSocketChannel) key.channel();
                        // 非阻塞狀態(tài)拿到客戶端連接
                        SocketChannel socketChannel = serverSocketChannel.accept();
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

首先盗痒,生成ServerSocketChannel對象蚂蕴,并綁定本地端口低散。再用ServerSocketChannel向Selector注冊SelectionKey.OP_ACCEPT俯邓。輪詢Selector返回的事件,當有TCP連接時熔号,則生成SocketChannel進行網(wǎng)絡(luò)數(shù)據(jù)傳輸稽鞭。

④ 、SocketChannel

SocketChannel專門就是接收發(fā)送TCP數(shù)據(jù)的引镊,打開連接朦蕴,將buffer中的數(shù)據(jù)寫入到channel或者從channel讀取數(shù)據(jù)到buffer中。舉個簡單的例子弟头。

    // 客戶端打開SocketChannel
    SocketChannel socketChannel = SocketChannel.open();
    socketChannel.connect(new InetSocketAddress("127.0.0.1", 5510));

    // 開啟非阻塞模式 異步模式下調(diào)用connect(), read() 和write() 可以立即返回無阻塞
    socketChannel.configureBlocking(false);
    ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
    // 連接還在
    while (!socketChannel.finishConnect()) {
        // 從channel中讀取數(shù)據(jù)
        socketChannel.read(byteBuffer);
        // buffer切換到讀模式
        byteBuffer.flip();
        // channel發(fā)送buffer中數(shù)據(jù)
        socketChannel.write(byteBuffer);
    }

在高并發(fā)的情況下吩抓,需要借助Selector,利用輪詢Selector方式赴恨,找到SocketChannel疹娶,再根據(jù)需求做特定的處理。

Selector selector = Selector.open();
        // 客戶端打開SocketChannel
        SocketChannel socketChannel = SocketChannel.open();
        socketChannel.connect(new InetSocketAddress("127.0.0.1", 5510));

        // 開啟非阻塞模式 異步模式下調(diào)用connect(), read() 和write() 可以立即返回無阻塞
        socketChannel.configureBlocking(false);
        socketChannel.register(selector,SelectionKey.OP_READ);
        ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
        // 連接還在
        while (true) {
            if (selector.select() == 0) {
                continue;
            }
            Set<SelectionKey> selectionKeys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = selectionKeys.iterator();
            while (iterator.hasNext()) {
                SelectionKey selectionKey = iterator.next();
                if (selectionKey.isValid()) {
                    // 取消對讀事件的繼續(xù)監(jiān)聽
                    selectionKey.interestOps(selectionKey.readyOps() & ~SelectionKey.OP_READ);
                    // 從channel中讀取數(shù)據(jù)
                    socketChannel.read(byteBuffer);
                }
                iterator.remove();
            }
        }
    }

channel向Selector注冊是SelectionKey key = channel.register(selector, SelectionKey.OP_READ);結(jié)合上述的介紹可以看出SelectionKey與Channel是一對一的關(guān)系伦连,而Selector和Channel是一對多關(guān)系雨饺。

將SelectionKey作為key,value是處理數(shù)據(jù)的Runnable,注冊的時候惑淳,存入Runnable额港。事件到達時,根據(jù)key取出Runnable歧焦,這樣就避免為每個連接創(chuàng)建線程執(zhí)行了移斩。


Input和Output流程

四、BIO、NIO向瓷、AIO區(qū)別

  • BIO忍宋,同步阻塞的IO模型。當用InputStream去讀取數(shù)據(jù)的時候风罩,必須等到數(shù)據(jù)操作完成才能進行下一步操作糠排,否則會阻塞當前的線程。這種方式比較簡單超升,但短流量高并發(fā)的情況下入宦,會造成效率低下∈易粒基本用于本地文件操作等簡單業(yè)務(wù)乾闰。

  • NIO,同步非阻塞IO模型盈滴。NIO還不是異步涯肩,它將一個線程操作一次請求,只是有效的減少了線程巢钓。即每一個連接注冊到多路復用器Selector中病苗,輪詢Selector中是否有事件到達,有事件就代表一次請求到達症汹,再用一個線程處理該請求硫朦。相比較BIO,它只是將多個線程去等待數(shù)據(jù)到達減低為一個線程去等待背镇。多用于低流量多連接的場景咬展,如聊天服務(wù)器。

  • AIO瞒斩,異步非阻塞IO模型破婆。當進行讀寫操作時,只須直接調(diào)用API的read或write方法胸囱。該方法都是異步的祷舀,方法執(zhí)行完成會回調(diào)回來。從方法簽名中可以看出旺矾,read時候會注入一個CompletionHandler回調(diào)方法蔑鹦,通知該次操作的狀態(tài)。調(diào)用者發(fā)送一個read或者write操作請求箕宙,操作完成后會得到一個通知嚎朽,真正的IO操作由操作系統(tǒng)內(nèi)核進行處理。AIO多用于長連接業(yè)務(wù)柬帕。

    public abstract <A> void read(ByteBuffer dst,
                                  long timeout,
                                  TimeUnit unit,
                                  A attachment,
                                  CompletionHandler<Integer,? super A> handler);
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末哟忍,一起剝皮案震驚了整個濱河市狡门,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌锅很,老刑警劉巖其馏,帶你破解...
    沈念sama閱讀 222,464評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異爆安,居然都是意外死亡叛复,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,033評論 3 399
  • 文/潘曉璐 我一進店門扔仓,熙熙樓的掌柜王于貴愁眉苦臉地迎上來褐奥,“玉大人,你說我怎么就攤上這事翘簇∏寺耄” “怎么了?”我有些...
    開封第一講書人閱讀 169,078評論 0 362
  • 文/不壞的土叔 我叫張陵版保,是天一觀的道長呜笑。 經(jīng)常有香客問我,道長彻犁,這世上最難降的妖魔是什么叫胁? 我笑而不...
    開封第一講書人閱讀 59,979評論 1 299
  • 正文 為了忘掉前任,我火速辦了婚禮袖裕,結(jié)果婚禮上曹抬,老公的妹妹穿的比我還像新娘。我一直安慰自己急鳄,他們只是感情好,可當我...
    茶點故事閱讀 69,001評論 6 398
  • 文/花漫 我一把揭開白布堰酿。 她就那樣靜靜地躺著疾宏,像睡著了一般。 火紅的嫁衣襯著肌膚如雪触创。 梳的紋絲不亂的頭發(fā)上坎藐,一...
    開封第一講書人閱讀 52,584評論 1 312
  • 那天,我揣著相機與錄音哼绑,去河邊找鬼岩馍。 笑死,一個胖子當著我的面吹牛抖韩,可吹牛的內(nèi)容都是我干的蛀恩。 我是一名探鬼主播,決...
    沈念sama閱讀 41,085評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼茂浮,長吁一口氣:“原來是場噩夢啊……” “哼双谆!你這毒婦竟也來了壳咕?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,023評論 0 277
  • 序言:老撾萬榮一對情侶失蹤顽馋,失蹤者是張志新(化名)和其女友劉穎谓厘,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體寸谜,經(jīng)...
    沈念sama閱讀 46,555評論 1 319
  • 正文 獨居荒郊野嶺守林人離奇死亡竟稳,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,626評論 3 342
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了熊痴。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片住练。...
    茶點故事閱讀 40,769評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖愁拭,靈堂內(nèi)的尸體忽然破棺而出讲逛,到底是詐尸還是另有隱情,我是刑警寧澤岭埠,帶...
    沈念sama閱讀 36,439評論 5 351
  • 正文 年R本政府宣布盏混,位于F島的核電站,受9級特大地震影響惜论,放射性物質(zhì)發(fā)生泄漏许赃。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 42,115評論 3 335
  • 文/蒙蒙 一馆类、第九天 我趴在偏房一處隱蔽的房頂上張望混聊。 院中可真熱鬧,春花似錦乾巧、人聲如沸句喜。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,601評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽咳胃。三九已至,卻和暖如春旷太,著一層夾襖步出監(jiān)牢的瞬間展懈,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,702評論 1 274
  • 我被黑心中介騙來泰國打工供璧, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留存崖,地道東北人。 一個月前我還...
    沈念sama閱讀 49,191評論 3 378
  • 正文 我出身青樓睡毒,卻偏偏與公主長得像来惧,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子吕嘀,可洞房花燭夜當晚...
    茶點故事閱讀 45,781評論 2 361

推薦閱讀更多精彩內(nèi)容