Java中NIO的三大核心組件詳細解析
前言
用戶程序進行IO的讀寫,依賴于底層的IO讀寫,基本上會用到底層的read&write兩大系統(tǒng)調(diào)用。在不同的操作系統(tǒng)中,IO讀寫的系統(tǒng)調(diào)用的名稱可能完全不一樣,但是基本功能是一樣的。
read系統(tǒng)調(diào)用并不是直接從物理設(shè)備把數(shù)據(jù)讀取到內(nèi)存中,write系統(tǒng)調(diào)用也不是直接把數(shù)據(jù)寫入到物理設(shè)備。上層應(yīng)用無論是調(diào)用操作系統(tǒng)的read還是write,都會涉及緩沖區(qū)。**具體來說,調(diào)用操作系統(tǒng)的read,是把數(shù)據(jù)從內(nèi)核緩沖區(qū)復(fù)制到進程緩沖區(qū);而調(diào)用系統(tǒng)調(diào)用的write,是把數(shù)據(jù)從進程緩沖區(qū)復(fù)制到內(nèi)核緩沖區(qū)。**因為外部設(shè)備的讀寫設(shè)計到操作系統(tǒng)的中斷,引入緩沖區(qū)可以減少頻繁地與設(shè)備之間的物理交換,操作系統(tǒng)會對內(nèi)核緩沖區(qū)進行監(jiān)控,等待緩沖區(qū)達到一定的數(shù)量的時候(由內(nèi)核決定,用戶程序無需關(guān)心),再進行IO設(shè)備的中斷處理,集中執(zhí)行物理設(shè)備的實際IO操作。
也就是說上層程序的IO操作,實際上不是物理設(shè)備級別的讀寫,而是緩存的復(fù)制。read&write兩大系統(tǒng)調(diào)用,都不負責(zé)數(shù)據(jù)在內(nèi)核緩沖區(qū)和物理設(shè)備(如磁盤)之間的交換。這項底層的讀寫交換,是由操作系統(tǒng)內(nèi)核來完成的,即使不調(diào)用read&write,當有數(shù)據(jù)到達網(wǎng)卡時軟中斷也會將其拷貝到內(nèi)核緩沖區(qū)。
在1.4版本之前,Java IO類庫是阻塞IO,從1.4版本開始引進了新的IO庫,稱為Java New IO類庫,簡稱為Java NIO。New IO類庫的目標就是讓Java支持非阻塞IO,彌補了原本面向流的OIO(Old IO)同步阻塞的不足,它為標準Java代碼提供了高速的、面向緩沖區(qū)的IO。
Java NIO由以下三個核心組件組成:
Channel(通道)
在OIO中,同一個網(wǎng)絡(luò)連接會關(guān)聯(lián)到兩個流,一個輸入流,一個輸出流,通過這兩個流不斷的進行輸入和輸出的操作在NIO中,同一個網(wǎng)絡(luò)連接使用一個通道表示,所有NIO的IO操作都是從通道開始的,一個通道類似于OIO中的兩個流的接合體,既可以從通道讀取,也可以向通道寫入
Buffer(緩沖區(qū))
通道的讀取就是將數(shù)據(jù)從通道讀取到緩沖區(qū)中;通道的寫入就是將數(shù)據(jù)緩沖區(qū)中寫入到通道中。
Selector(選擇器)
用于實現(xiàn)對多個文件描述符的監(jiān)視,通過選擇器,一個線程可以查詢多個通道的IO事件的就緒狀態(tài)。與OIO相比,使用選擇器的最大優(yōu)勢就是系統(tǒng)開銷小,不需要為每個網(wǎng)絡(luò)連接(文件描述符)創(chuàng)建進程/線程,使用一個線程就可以管理多個通道。
在Java中,NIO和OIO的區(qū)別主要體現(xiàn)在三個方面:
- OIO是面向流的,NIO是面向緩沖區(qū)的
- OIO是面向字節(jié)流或字符流的,在一般的OIO操作中,我們以流式的方式順序地從一個流中讀取一個或多個字節(jié),因此我們不能隨意地改變讀取指針的位置。
- NIO中引入了Channel和Buffer的概念,讀取和寫入只需要從通道中讀取數(shù)據(jù)到緩沖區(qū),或?qū)?shù)據(jù)從緩沖區(qū)中寫入到通道中,可以隨意地讀取Buffer中任意位置的數(shù)據(jù)。
- OIO的操作是阻塞的,而NIO的操作是非阻塞的
- OIO沒有選擇器概念,而NIO有選擇器的概念(IO多路復(fù)用)
一、Buffer
NIO的Buffer類是一個抽象類,位于java.nio包中,提供了一組更加有效的方法,用來進行寫入和讀取的交替訪問,本質(zhì)上是一個內(nèi)存塊(數(shù)組),既可以寫入數(shù)據(jù),也可以從中讀取數(shù)據(jù)。
需要強調(diào)的是Buffer類是一個非線程安全類。
在NIO這種有8種緩沖區(qū)類,分別為ByteBuffer、CharBuffer、ShortBufffer、IntBuffer、LongBuffer、FloatBuffer、DoubleBuffer、MappedByteBuffer。前7種Buffer類型覆蓋了能在IO中傳輸?shù)乃蠮ava基本數(shù)據(jù)類型,第8種數(shù)據(jù)類型MappedByteBuffer是專門用于內(nèi)存映射的一種ByteBuffer類型。實際上使用最多的還是ByteBuffer二進制字節(jié)緩沖區(qū)類型。
1、重要屬性
Buffer類在其內(nèi)部有一個對應(yīng)類型的數(shù)組(如ByteBuffer的byte[]數(shù)組)作為內(nèi)存緩沖區(qū),為了記錄讀寫的狀態(tài)和位置,Buffer類提供了一些重要的屬性,其中有三個重要的成員屬性:
capacity:容量(一旦初始化就不能再改變)
表示內(nèi)部容量的大小,一旦寫入的對象數(shù)量超過了capacity容量,緩沖區(qū)就滿了,不能再寫入了。
position:讀寫位置
- 表示當前的位置。position屬性與緩沖區(qū)的讀寫模式有關(guān),在不同的模式下position屬性的值是不同的,當緩沖區(qū)進行讀寫的模式改變時,position會進行調(diào)整。
- 寫入模式:剛進入寫模式時,position值為0,表示當前寫入的位置從頭開始。每當一個數(shù)據(jù)寫到緩沖區(qū)之后,position會向后移動到下一個可寫的位置。最大可寫值position為limit-1,當position值達到limit時,緩沖區(qū)就已經(jīng)無空間可寫了。
- 讀取模式:剛進入讀模式時,position值被重置為0,表示當前讀取的位置從頭開始。當從緩沖區(qū)讀取時,也是從position位置開始讀,讀取之后position向后移動到下一個可讀的位置。position最大的值為最大刻度上限limit,當position達到limit時表示緩沖區(qū)已經(jīng)無數(shù)據(jù)可讀。
- 當新建一個緩沖區(qū)時,緩沖區(qū)處于寫入模式,這時是可以寫數(shù)據(jù)的。數(shù)據(jù)寫入后,如果要從緩沖區(qū)讀取數(shù)據(jù),這就要進行模式的切換,可以使用flip翻轉(zhuǎn)方法,將緩沖區(qū)變?yōu)樽x取模式。在flip翻轉(zhuǎn)過程中會將position由原來的寫入位置,變?yōu)樾碌目勺x位置,也就是0,表示可以從頭開始讀。此外flip還會調(diào)整limit屬性的值。
limit:讀寫的限制
- 表示讀寫的最大上線,和緩沖區(qū)的讀寫模式有關(guān)。
- 寫入模式:在寫模式下limit屬性值的含義為可以寫入的數(shù)據(jù)最大上限,在剛進入到寫模式時,limit的值會被設(shè)置成緩沖區(qū)的capacity容量值,表示可以將緩沖區(qū)的容量寫滿。
- 讀取模式:在讀模式下limit屬性值的含義為最多能從緩沖區(qū)中讀取到多少數(shù)據(jù)。
- 一般來說是先寫入再讀取,當緩沖區(qū)寫入完成后,就可以開始從Buffer讀取數(shù)據(jù),可以使用flip翻轉(zhuǎn)方法,這時會將寫模式下的position值設(shè)置為讀模式下的limit值。
2、重要方法
1)allocate()創(chuàng)建緩沖區(qū)
在使用Buffer之前,我們首先需要獲取Buffer子類的實例對象,并且分配內(nèi)存空間。獲取一個Buffer實例對象并不是使用子類的構(gòu)造器new來創(chuàng)建一個實例對象,而是調(diào)用子類的allocate()方法,該方法需要傳入一個int類型的參數(shù),表示緩沖區(qū)的容量。
public static void main(String[] args) throws IOException { CharBuffer buffer = CharBuffer.allocate(20); System.out.println("緩沖區(qū)的capacity:" + buffer.capacity()); System.out.println("緩沖區(qū)的position:" + buffer.position()); System.out.println("緩沖區(qū)的limit:" + buffer.limit()); } 緩沖區(qū)的capacity:20 緩沖區(qū)的position:0 緩沖區(qū)的limit:20
2)put()寫入到緩沖區(qū)
在調(diào)用allocate方法分配內(nèi)存、返回了實例對象后,緩沖區(qū)實例對象處于寫模式,可以寫入對象。要寫入緩沖區(qū),需要調(diào)用put方法。
put方法只有一個參數(shù),即為所需要寫入的對象,數(shù)據(jù)類型要求與緩沖區(qū)的類型保持一致。
3)flip()翻轉(zhuǎn)
向緩沖區(qū)寫入數(shù)據(jù)之后是不可以直接從緩沖區(qū)中讀取數(shù)據(jù)的,因為此時緩沖區(qū)還處于寫模式,如果需要讀取數(shù)據(jù),還需要將緩沖區(qū)轉(zhuǎn)換成讀模式。那么此時就需要使用flip()方法進行翻轉(zhuǎn)。flip()方法的作用就是將寫入模式翻轉(zhuǎn)成讀取模式。
對于flip()方法的從寫入到讀取轉(zhuǎn)換的規(guī)則:
- 首先設(shè)置可讀的長度上限limit,將寫模式下緩沖區(qū)中內(nèi)容的最后寫入位置position值作為讀模式下的limit上限值
- 其次把讀的起始位置position的值設(shè)為0,表示從頭開始讀
- 最后清除之前的mark標記(mark保存的是一個臨時位置)
flip()的作用是將寫入模式轉(zhuǎn)換為讀取模式,那么如何將緩沖區(qū)切換成讀取模式呢?
一般來說可以通過調(diào)用clear()清空或者compact()壓縮方法,它們可以將緩沖區(qū)轉(zhuǎn)換為寫模式。
4)get()從緩沖區(qū)讀取
調(diào)用flip方法將緩沖區(qū)切換成讀取模式之后就可以開始從緩沖區(qū)中進行數(shù)據(jù)讀取了。
get方法每次從position的位置讀取一個數(shù)據(jù),并且進行相應(yīng)的緩沖區(qū)屬性的調(diào)整。
讀取操作會改變刻度位置position的值,而limit值不會改變,如果position值和limit的值相等,表示所有數(shù)據(jù)讀取完成,position只想了一個沒有數(shù)據(jù)的元素位置,已經(jīng)不能再讀了,此時再讀會拋出BufferUnderflowException異常。
在讀完之后不可以立即進行寫入操作,必須調(diào)用clear或compact方法清空或者壓縮緩沖區(qū)才能編程寫入模式,讓其重新可寫。
5)rewind()倒帶
已經(jīng)讀完的數(shù)據(jù)如果需要再讀一遍,可以調(diào)用rewin()方法,rewind()也叫倒帶,就像播放磁帶一樣倒回去,再重新播放。
rewind()方法主要是調(diào)整了緩沖區(qū)的position屬性,具體的調(diào)整規(guī)則如下:
- position重置為0,所以可以重讀緩沖區(qū)的所有數(shù)據(jù)
- limit保持不變
- mark標記被清理,之前的臨時位置不能再用了
rewind()方法與flip很像是,區(qū)別在于rewind不會影響limit屬性值,而flip會重設(shè)limit屬性值。
6)mark()和reset()
mark方法的作用是將當前的position的值保存起來,放在mark屬性中,讓mark屬性記住這個臨時位置,之后可以調(diào)用reset方法將mark的值恢復(fù)到position中。
7)clear()清空緩沖區(qū)
在讀取模式下調(diào)用clear方法將緩沖區(qū)切換為寫入模式,此方法會將position清零,limit設(shè)置為capacity最大容量值。
8)使用Buffer類的基本步驟
- 使用創(chuàng)建子類實例對象的allocate()方法創(chuàng)建一個Buffer類的實例對象
- 調(diào)用put方法將數(shù)據(jù)寫入到緩沖區(qū)中
- 寫入完成后在開始讀取數(shù)據(jù)前調(diào)用flip()方法將緩沖區(qū)轉(zhuǎn)換為讀模式
- 調(diào)用get方法從緩沖區(qū)中讀取出數(shù)據(jù)
- 讀取完成后調(diào)用clear()或compact()方法將緩沖區(qū)轉(zhuǎn)換為寫入模式
二、Channel
NIO中一個連接就是用一個Channel(通道)來表示,一個通道可以表示一個底層的文件描述符,例如硬件設(shè)備、文件、網(wǎng)絡(luò)連接等,除此之外Java NIO的通道還可以更加細化,例如對應(yīng)不同的網(wǎng)絡(luò)傳輸協(xié)議類型,在Java中都有不同的NIO Channel實現(xiàn)。
Channel主要有四種重要的類型:
- FileChannel:文件通道,用于文件的數(shù)據(jù)讀寫
- SocketChannel:套接字通道,用于Socket套接字TCP連接的數(shù)據(jù)讀寫
- ServerSocketChannel:服務(wù)器嵌套字通道(或服務(wù)器監(jiān)聽通道),允許我們監(jiān)聽TCP連接請求,為每個監(jiān)聽到的請求創(chuàng)建一個SocketChannel套接字通道
- DatagramChannel:數(shù)據(jù)報通道,用于UDP協(xié)議的數(shù)據(jù)讀寫
1、FileChannel文件通道
FileChannel是專門操作文件的通道,它是阻塞模式的,不能設(shè)置為非阻塞模式。具體的操作如下:
獲取通道
- 通過文件的輸入流、輸出流獲?。簄ew FileInputStream(srcFile).getChannel() / new FileOutputStream(destFile).getChannel()
- 通過RandomAccessFile文件隨機訪問類獲?。簄ew RandomAccessFile(“filename.txt”, “rw”).getChannel()
讀取通道
- 通過調(diào)用通道的int read(ByteBuffer buf)將通道的數(shù)據(jù)讀取到ByteBuffer緩沖區(qū)中,并返回讀取到的數(shù)據(jù)量
- 對于通道來說是讀取數(shù)據(jù),對于ByteBuffer緩沖區(qū)來說是寫入數(shù)據(jù),所以此時ByteBuffer緩沖區(qū)需要處于寫入模式
寫入通道
- 通過調(diào)用通道的int write(ByteBuffer buf)方法將ByteBuffer緩沖區(qū)的數(shù)據(jù)寫入到通道中,并返回寫入成功的字節(jié)數(shù)
- 此時的ByteBuffer緩沖區(qū)要求是可讀的,處于讀模式下
關(guān)閉通道:channel.close()
強制刷新到磁盤
- 在將緩沖區(qū)寫入通道時,由于性能原因,操作系統(tǒng)不可能每次都實時將數(shù)據(jù)寫入磁盤,如果需要保證數(shù)據(jù)真正落盤需要調(diào)用force()方法
- force方法接受一個布爾參數(shù),如果為true,則該方法需要強制更改文件的內(nèi)容和將元數(shù)據(jù)寫入存儲;否則,它只需要強制寫入內(nèi)容更改
2、SocketChannel套接字通道
在NIO中設(shè)計網(wǎng)絡(luò)連接的通道有兩個,一個是SocketChannel負責(zé)連接傳輸,一個是ServerSocketChannel負責(zé)連接的監(jiān)聽。
NIO中SocketChannel傳輸通道與OIO中的Socket類對應(yīng),NIO中的ServerSocketChannel監(jiān)聽通道與OIO中的ServerSocket對應(yīng)。
ServerSocketChannel應(yīng)用于服務(wù)器端,而SocketChannel同時處于服務(wù)器端和客戶端。換句話說,對于一個連接,兩端都有一個負責(zé)傳輸?shù)腟ocketChannel傳輸通道。
這兩種Channel都可以通過configureBlocking()方法設(shè)置是否為阻塞模式。在阻塞模式下,connect連接、read、write操作都是同步阻塞的,效率上和Java舊的OIO的面向流的阻塞式讀寫操作相同。
獲取通道
- 服務(wù)端:ServerSocketChannel.open() / server.accept()
- 客戶端:SocketChannel.open()
讀取通道和寫入通道同樣為read和write
關(guān)閉通道:
在關(guān)閉SocketChannel傳輸通道前,如果傳輸通道用來寫入數(shù)據(jù),則建議調(diào)用shutdownOutput()終止輸出方法,向?qū)Ψ桨l(fā)送一個輸出的結(jié)束標志(-1),然后再調(diào)用close方法關(guān)閉套接字
3、DatagramChannel數(shù)據(jù)報通道
和Socket套接字的TCP傳輸協(xié)議不同,UDP協(xié)議不是面向連接的協(xié)議。
使用UDP協(xié)議時只要知道服務(wù)器的IP和端口就可以直接向?qū)Ψ桨l(fā)送數(shù)據(jù)。
- 獲取通道:Datagram.open()
- 讀取通道:channel.receive(buf),返回值為SocketAddress類型,表示返回發(fā)送端的連接地址
- 寫入通道:channel.send(buffer, new InetSocketAddress(ip, port))
- 關(guān)閉:直接調(diào)用close()即可
三、Selector
Selector選擇器的使命是完成IO的多路復(fù)用。一個通道代表一條連接通路,通過選擇器可以同時監(jiān)控多個通道的IO狀況。選擇器和通道的關(guān)系,是監(jiān)控和被監(jiān)控的關(guān)系。
選擇器提供了獨特的API,能夠選出(select)所監(jiān)控的通道擁有哪些已經(jīng)準備好的、就緒的IO操作事件。
通道和選擇器之間的關(guān)系,通過register(注冊)的方式完成,調(diào)用通道的register(Selector sel, int ops)方法,可以將通道實例注冊到一個選擇器中。register方法有兩個參數(shù):第一個指定通道注冊到的選擇器實例,第二個指定選擇器要監(jiān)控的IO事件類型??晒┻x擇器監(jiān)控的通道IO事件類型包括以下四種:
- 可讀:SelectionKey.OP_READ
- 可寫:SelectionKey.OP_WRITE
- 連接:SelectionKey.OP_CONNECT
- 接收:SelectionKey.OP_ACCEPT
如果選擇器要監(jiān)控通道的多種事件,可以用“按位或”運算符來實現(xiàn)。
并不是所有的通道都是可以被選擇器監(jiān)控或選擇的。比方說FileChannel文件通道選擇器就不能被選擇器復(fù)用。
判斷一個通道能否被選擇器監(jiān)控或選擇有一個前提:判斷它是否繼承了抽象類SelectableChannel(可選擇通道),如果繼承了則可以被選擇,否則不能被選擇。該抽象類中定義了register()、configureBlocking()、isBlocking()等方法。
通道和選擇器的監(jiān)控關(guān)系注冊成功后就可以選擇就緒時間。具體的選擇工作由選擇器的select()方法來完成。通過該方法,選擇器可以不斷地選擇通道中發(fā)生操作的就緒狀態(tài),返回注冊過的感興趣的那些IO事件(函數(shù)放回的是感興趣的IO事件的數(shù)量,)。也就是說一旦通道中發(fā)生了我們在選擇器中注冊過的IO事件,就會被選擇器選中并放入SelectionKeys選擇間的集合中。SelectionKey選擇鍵不僅可以獲得通道的IO事件類型,還可以獲得發(fā)生IO事件所在的通道,此外還可以獲得選出選擇鍵的選擇器實例。使用方式:
public static void main(String[] args) throws IOException { try (Selector selector = Selector.open()) { while (selector.select() > 0) { Set<SelectionKey> selectionKeys = selector.selectedKeys(); for (SelectionKey selectionKey : selectionKeys) { if (selectionKey.isAcceptable()) { } else if (selectionKey.isReadable()) { } else if (selectionKey.isWritable()) { } else if (selectionKey.isConnectable()) { } } } } }
到此這篇關(guān)于Java中NIO的三大核心組件詳細解析的文章就介紹到這了,更多相關(guān)NIO的三大核心組件內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java輸出通過InetAddress獲得的IP地址數(shù)組詳細解析
由于byte被認為是unsigned byte,所以最高位的1將會被解釋為符號位,另外Java中存儲是按照補碼存儲,所以1000 0111會被認為是補碼形式,轉(zhuǎn)換成原碼便是1111 0001,轉(zhuǎn)換成十進制數(shù)便是-1212013-09-09SpringBoot實現(xiàn)返回值數(shù)據(jù)脫敏的步驟詳解
這篇文章主要給大家介紹一下SpringBoot實現(xiàn)返回值數(shù)據(jù)脫敏的步驟,文章通過代碼示例介紹的非常詳細,具有一定的參考價值,需要的朋友可以參考下2023-07-07Spring+Quartz實現(xiàn)動態(tài)任務(wù)調(diào)度詳解
這篇文章主要介紹了Spring+Quartz實現(xiàn)動態(tài)任務(wù)調(diào)度詳解,最近經(jīng)?;趕pring?boot寫定時任務(wù),并且是使用注解的方式進行實現(xiàn),分成的方便將自己的類注入spring容器,需要的朋友可以參考下2024-01-01Spring?Boot?@Autowired?@Resource屬性賦值時機探究
這篇文章主要為大家介紹了Spring?Boot?@Autowired?@Resource屬性賦值時機,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步,早日升職加薪2023-07-07