Selector(選擇器)是Java NIO中能夠同時(shí)監(jiān)測(cè)多個(gè)Channel通道误辑,并且還能知道Channel上讀寫事件是否準(zhǔn)備好。這樣一個(gè)Selector線程就可以管理多個(gè)Channel沦泌,而不像Blocking IO那樣一個(gè)線程對(duì)應(yīng)一個(gè)監(jiān)管一個(gè)IO事件磷蜀。
Selector特點(diǎn)
一個(gè)Selector對(duì)應(yīng)多個(gè)Channel:僅用單個(gè)線程來(lái)處理多個(gè)Channels的好處是朝捆,只需要更少的線程來(lái)處理通道。事實(shí)上国章,可以只用一個(gè)線程處理所有的通道具钥。對(duì)于操作系統(tǒng)來(lái)說(shuō),線程之間上下文切換的開銷很大液兽,而且每個(gè)線程都要占用系統(tǒng)的一些資源(如內(nèi)存)骂删。因此,使用的線程越少越好四啰。
select宁玫、poll、epoll模式
select柑晒,poll欧瘪,epoll都是IO多路復(fù)用的機(jī)制。I/O多路復(fù)用就通過(guò)一種機(jī)制匙赞,可以監(jiān)視多個(gè)描述符佛掖,一旦某個(gè)描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進(jìn)行相應(yīng)的讀寫操作涌庭。
- select模式
特點(diǎn):忙輪詢芥被,文件描述符有大小限制,會(huì)將整個(gè)文件描述符數(shù)組頻繁在用戶內(nèi)存空間和內(nèi)核的內(nèi)存地址空間的拷貝復(fù)制脾猛,造成較大的性能消耗撕彤。
過(guò)程:不停地從頭到尾遍歷整個(gè)文件描述符,根據(jù)每一個(gè)描述符的狀態(tài)進(jìn)行通知處理猛拴,被通知的線程還需要遍歷整個(gè)文件描述符來(lái)判斷是哪個(gè)事件準(zhǔn)備好了羹铅。
- poll模式
特點(diǎn):輪詢,沒(méi)有文件描述符大小限制(有系統(tǒng)內(nèi)存大小相關(guān))愉昆,但是同樣會(huì)將整個(gè)文件描述符數(shù)組頻繁在用戶內(nèi)存空間和內(nèi)核的內(nèi)存地址空間的拷貝復(fù)制职员,造成較大的性能消耗
過(guò)程:為了避免CPU空轉(zhuǎn),可以同時(shí)觀察許多流的I/O事件跛溉,在空閑的時(shí)候焊切,會(huì)把當(dāng)前線程阻塞掉,當(dāng)有一個(gè)或多個(gè)流有I/O事件時(shí)芳室,就從阻塞態(tài)中醒來(lái)专肪,被喚醒的程序就會(huì)輪詢一遍所有的文件描述符,來(lái)判斷哪個(gè)IO的事件就緒堪侯,并進(jìn)行相應(yīng)的處理嚎尤。
- epoll模式
特點(diǎn):基于IO事件響應(yīng),高性能伍宦,沒(méi)有文件描述符大小限制芽死,只會(huì)將準(zhǔn)備就緒的IO描述符進(jìn)行復(fù)制。
過(guò)程:epoll同時(shí)觀察許多流的I/O事件次洼,在沒(méi)有IO事件發(fā)生時(shí)关贵,會(huì)把當(dāng)前線程阻塞掉,當(dāng)有一個(gè)或多個(gè)流有I/O事件時(shí)卖毁,就從阻塞態(tài)中醒來(lái)揖曾,被喚醒的程序就能獲取所有已經(jīng)準(zhǔn)備就緒的文件描述符,并進(jìn)行相應(yīng)的處理亥啦。
- epoll模式的其他特性
- 在Linux2.6(包括)之后使用epoll模式實(shí)現(xiàn)Java NIO炭剪,在用戶程序使用上是沒(méi)有差別的,只是遍歷Selector.selectedKeys()方法返回集合的時(shí)間復(fù)雜度為O(n)或O(1)禁悠,其中n為整個(gè)文件描述符的數(shù)量念祭。
- epoll模式的高效性:epoll在被內(nèi)核初始化時(shí)(操作系統(tǒng)啟動(dòng)),同時(shí)會(huì)開辟出epoll自己的內(nèi)核高速cache區(qū)碍侦,用于安置每一個(gè)我們想監(jiān)控的socket粱坤,這些socket會(huì)以紅黑樹的形式保存在內(nèi)核cache里,以支持快速的查找瓷产、插入站玄、刪除。這個(gè)內(nèi)核高速cache區(qū)濒旦,就是建立連續(xù)的物理內(nèi)存頁(yè)株旷,然后在之上建立slab層,簡(jiǎn)單的說(shuō),就是物理上分配好你想要的size的內(nèi)存對(duì)象晾剖,每次使用時(shí)都是使用空閑的已分配好的對(duì)象锉矢,避免了內(nèi)核內(nèi)存空間和用戶內(nèi)存空間的復(fù)制消耗。
Selector的使用
Selector總是和Channel成對(duì)出現(xiàn)的齿尽,與Selector一起使用時(shí)沽损,Channel必須處于非阻塞模式下。這意味著不能將FileChannel與Selector一起使用循头,因?yàn)镕ileChannel不能切換到非阻塞模式绵估。而套接字通道都可以。
從一個(gè)完整的程序示例開始
需要留意的步驟:
步驟3
使用Selector選擇器需要將Channel設(shè)置為非阻塞狀態(tài)卡骂,F(xiàn)ileChannel不可設(shè)置為非阻塞狀態(tài)国裳,套接字可以。
步驟5
注意通常不會(huì)注冊(cè)寫就緒事件全跨,因?yàn)樵诎l(fā)送緩沖區(qū)未滿的情況下始終是可寫的缝左,而且
注冊(cè)寫事件,而又不用寫數(shù)據(jù)螟蒸,則緩沖區(qū)未滿總會(huì)響應(yīng)寫事件就緒盒使,很容易造成CPU空轉(zhuǎn),出現(xiàn)消耗CPU100%的情況七嫌。
注冊(cè)的事件可以為:
- SelectionKey.OP_CONNECT(某個(gè)channel成功連接到另一個(gè)服務(wù)器稱為“連接就緒”)
- SelectionKey.OP_ACCEPT(channel準(zhǔn)備好接收新進(jìn)入的連接稱為“接收就緒”少办,對(duì)應(yīng)于ServerSocket.accept方法)
- SelectionKey.OP_READ(一個(gè)有數(shù)據(jù)可讀的通道可以說(shuō)是“讀就緒”)
- SelectionKey.OP_WRITE(等待寫數(shù)據(jù)的通道可以說(shuō)是“寫就緒”)
register()方法會(huì)返回一個(gè)SelectionKey對(duì)象
- interest集合(所選擇的感興趣的事件集合)
- ready集合(通道已經(jīng)準(zhǔn)備就緒的操作的集合)
這是一個(gè)復(fù)合int類型字段,通過(guò)如下方法可以進(jìn)行判斷
selectionKey.isAcceptable();
selectionKey.isConnectable();
selectionKey.isReadable();
selectionKey.isWritable()
- Channel诵原、Selector
通過(guò)如下方法獲取觸發(fā)此IO事件的Channel英妓,以及被哪個(gè)Selector監(jiān)聽到的
- 附加的對(duì)象(可選,是在注冊(cè)Channel時(shí)一同注冊(cè)的一個(gè)對(duì)象)
步驟6.1
通過(guò)select方法獲取通道
select()方法返回的int值表示有多少通道已經(jīng)就緒绍赛。亦即蔓纠,自上次調(diào)用select()方法后有多少通道變成就緒狀態(tài)。如果調(diào)用select()方法吗蚌,因?yàn)橛幸粋€(gè)通道變成就緒狀態(tài)腿倚,返回了1,若再次調(diào)用select()方法蚯妇,如果另一個(gè)通道就緒了敷燎,它會(huì)再次返回1。如果對(duì)第一個(gè)就緒的channel沒(méi)有做任何操作箩言,現(xiàn)在就有兩個(gè)就緒的通道硬贯,但在每次select()方法調(diào)用之間,只有一個(gè)通道就緒了陨收。
- int select()
select()阻塞到至少有一個(gè)通道在你注冊(cè)的事件上就緒了
- int select(long timeout)
select(long timeout)和select()一樣饭豹,除了最長(zhǎng)會(huì)阻塞timeout毫秒(參數(shù))
- int selectNow()
selectNow()不會(huì)阻塞,不管什么通道就緒都立刻返回
如何喚醒阻塞在select方法的線程
- Selector.wakeup()
將使得選擇器上的第一個(gè)有就緒Channel但是還沒(méi)有返回的選擇操作立即返回。如果當(dāng)前沒(méi)有就緒Channel被選擇拄衰,那么下一次對(duì) select( )方法的一種形式的調(diào)用將立即返回它褪,后續(xù)的選擇操作將正常進(jìn)行。在選擇操作之間多次調(diào)用 wakeup( )方法與調(diào)用它一次沒(méi)有什么不同肾砂。有時(shí)這種延遲的喚醒行為并不是您想要的列赎,您可以通過(guò)在調(diào)用 wakeup( )方法后調(diào)用 selectNow( )方法來(lái)繞過(guò)這個(gè)問(wèn)題宏悦,此時(shí)需要將代碼合理地關(guān)注于返回值和執(zhí)行選擇集合镐确。
- Selector.close( )
如果選擇器的 close( )方法被調(diào)用,那么任何一個(gè)在選擇操作中阻塞的線程都將被喚醒,與選擇器相關(guān)的通道將被注銷,而鍵將被取消饼煞。通道本身并不會(huì)關(guān)閉源葫。
- Thread.interrupt( )
拋出中斷異常,程序異常返回砖瞧,如果捕獲異常則進(jìn)行清理操作息堂。
步驟6.3
每次迭代末尾的keyIterator.remove()調(diào)用。
Selector不會(huì)自己從已選擇鍵集中移除SelectionKey實(shí)例块促。必須在處理完通道時(shí)自己移除荣堰。下次該通道變成就緒時(shí),Selector會(huì)再次將其放入已選擇鍵集中竭翠。實(shí)際上在兩次調(diào)用select( )方法之間振坚,都必須手動(dòng)將其清空,換句話說(shuō)斋扰,select( )方法只會(huì)在已有的所選鍵集上添加鍵渡八,它們不會(huì)創(chuàng)建新的建集。
步驟7
先使Selector的注冊(cè)信息失效传货,然后在關(guān)閉Channel
先關(guān)閉Selector屎鳍,然后在關(guān)閉Channel,平滑的關(guān)閉整個(gè)系統(tǒng)问裕。