摘要:阻塞請求結果返回之前,當前線程被掛起。也就是說在異步中,不會對用戶線程產生任何阻塞。當前線程在拿到此次請求結果的過程中,可以做其它事情。事實上,可以只用一個線程處理所有的通道。
準備知識 同步、異步、阻塞、非阻塞
同步和異步說的是服務端消息的通知機制,阻塞和非阻塞說的是客戶端線程的狀態。
已客戶端一次網絡請求為例做簡單說明:
同步
同步是指一次請求沒有得到結果之前就不返回。
異步
請求不會立刻得到最終結果,服務器處理完成再異步通知客戶端。
阻塞
請求結果返回之前,當前線程被掛起。在此期間不能做任何其他的事情。
非阻塞
請求立即返回,后續由客戶端時不時的詢問服務器結果或者服務器異步回調。
通常來說,IO操作包括:對硬盤的讀寫、對socket的讀寫以及外設的讀寫。
已一個IO讀取過程為例做簡要說明(如圖):
DMA把數據讀取到內核空間的緩沖區(讀就緒)
內核將數據拷貝到用戶空間。
內核空間是用戶代碼無法控制的,所以用戶空間在讀取之前,首先會判斷是否已經讀就緒。
同步IO
當用戶發出IO請求操作之后,內核會去查看要讀取的數據是否就緒,如果數據沒有就緒,就一直等待。需要通過用戶線程或者內核不斷地去輪詢數據是否就緒,當數據就緒時,再將數據從內核拷貝到用戶空間。
異步IO
只有IO請求操作的發出是由用戶線程來進行的,IO操作的兩個階段都是由內核自動完成,然后發送通知告知用戶線程IO操作已經完成。也就是說在異步IO中,不會對用戶線程產生任何阻塞。
阻塞IO
當用戶線程發起一個IO請求操作(以讀請求操作為例),內核查看要讀取的數據還沒就緒,當前線程被掛起,阻塞等待結果返回。
非阻塞IO
如果數據沒有就緒,則會返回一個標志信息告知用戶線程當前要讀的數據沒有就緒。當前線程在拿到此次請求結果的過程中,可以做其它事情。
BIO
同步阻塞,傳統io方式。
適用于連接數目比較小且固定的架構,這種方式對服務器資源要求比較高,并發局限于應用中。
NIO
同步非阻塞,jdk4開始支持。
適用于連接數目多且連接比較短(輕操作)的架構,比如聊天服務器。
AIO
異步非阻塞,jdk7開始支持。
適用于連接數目多且連接比較長(重操作)的架構。
形象的理解NIO和AIO:
如果把內核比作快遞,NIO就是你要自己時不時到官網查下快遞是否已經到了你所在城市,然后自己去取快遞;AIO就是快遞員送貨上門了。
阻塞I/O(blocking I/O)
非阻塞I/O (nonblocking I/O)
I/O復用(select 和poll) (I/O multiplexing)
信號驅動I/O (signal driven I/O (SIGIO))
異步I/O (asynchronous I/O (the POSIX aio_functions))
IO復用模型(IO多路復用)簡言之,就是通過單個線程(進程)來管理多IO流。如圖:
IO多路復用避免阻塞在IO上,原本為多進程或多線程來接收多個連接的消息變為單進程或單線程保存多個socket的狀態后輪詢處理。只有當某個socket讀寫就緒后,才真正調用實際的IO讀寫操作。這樣可以避免線程切換帶來的開銷。
實現IO多路復用需要函數來支持,就是你說的linux下的select、poll、epoll以及win下 iocp和BSD的kqueue。這幾個函數也會使進程阻塞,但是和阻塞I/O所不同的是,它可以同時阻塞多個I/O操作。而且可以同時對多個讀操作,多個寫操作的I/O準備狀態進行檢測。
IO多路復用為何比非阻塞IO模型的效率高是因為在非阻塞IO中,不斷地詢問socket狀態是通過用戶線程去進行的,而在IO多路復用中,輪詢每個socket狀態是內核在進行的,這個效率要比用戶線程要高的多。
五種IO模型以及select、poll、epoll的詳細介紹推薦大家看這篇文章 →
socket阻塞與非阻塞,同步與異步、I/O模型
在Reactor模式中,會先對每個client注冊感興趣的事件,然后有一個線程專門去輪詢每個client是否有事件發生,當有事件發生時(讀寫就緒),便順序處理每個事件,當所有事件處理完之后,便再轉去繼續輪詢,如圖所示:
從這里可以看出,多路復用IO就是采用Reactor模式。注意,上面的圖中展示的是順序處理每個事件,當然為了提高事件處理速度,可以通過多線程或者線程池的方式來處理事件。
在Proactor模式中,當檢測到有事件發生時,會新起一個異步操作,然后交由內核線程去處理,當內核線程完成IO操作之后,發送一個通知告知操作已完成,可以得知,異步IO模型采用的就是Proactor模式。
這部分摘選自:Java NIO:淺析I/O模型
Java NIO介紹Channels and Buffers(通道和緩沖區)
標準的IO基于字節流和字符流進行操作的,而NIO是基于通道(Channel)和緩沖區(Buffer)進行操作,數據總是從通道讀取到緩沖區中,或者從緩沖區寫入到通道中。
Non-blocking IO(非阻塞IO)
Java NIO可以讓你非阻塞的使用IO,例如:當線程從通道讀取數據到緩沖區時,線程還是可以進行其他事情。當數據被寫入到緩沖區時,線程可以繼續處理它。從緩沖區寫入通道也類似。
Selectors(選擇器)
選擇器用于監聽多個通道的事件(比如:連接打開,數據到達)。因此,單個的線程可以監聽多個數據通道。
? IO ? ? ? ? ? ? ? ? NIO
面向流? ? ?? ? 面向緩沖
阻塞IO ? ? ?? ? 非阻塞IO
? 無? ? ? ? ? ? ? ? 選擇器
Java NIO的通道類似流,但又有些不同:
既可以從通道中讀取數據,又可以寫數據到通道。但流的讀寫通常是單向的。
通道可以異步地讀寫。
通道中的數據總是要先讀到一個Buffer,或者總是要從一個Buffer中寫入。
Channel的實現FileChannel (從文件中讀寫數據)
DatagramChannel (通過UDP讀寫網絡中的數據)
SocketChannel (通過TCP讀寫網絡中的數據)
ServerSocketChannel (可以監聽新進來的TCP連接,像Web服務器那樣)
BufferJava NIO中的Buffer用于和NIO通道進行交互。如你所知,數據是從通道讀入緩沖區,從緩沖區寫入到通道中的。
Buffer的基本用法使用Buffer讀寫數據一般遵循以下四個步驟:
分配指定大小的buffer空間
寫入數據到Buffer
調用flip()方法
從Buffer中讀取數據
調用clear()方法或者compact()方法
當向buffer寫入數據時,buffer會記錄下寫了多少數據。一旦要讀取數據,需要通過flip()方法將Buffer從寫模式切換到讀模式。在讀模式下,可以讀取之前寫入到buffer的所有數據。
一旦讀完了所有的數據,就需要清空緩沖區,讓它可以再次被寫入。有兩種方式能清空緩沖區:調用clear()或compact()方法。clear()方法會清空整個緩沖區。compact()方法只會清除已經讀過的數據。任何未讀的數據都被移到緩沖區的起始處,新寫入的數據將放到緩沖區未讀數據的后面。
注:Buffer中的數據并未清除,只是這些標記告訴我們可以從哪里開始往Buffer里寫數據。
ByteBuffer
MappedByteBuffer
CharBuffer
DoubleBuffer
FloatBuffer
IntBuffer
LongBuffer
ShortBuffer
SelectorSelector(選擇器)是Java NIO中能夠檢測一到多個通道,并能夠知曉通道是否為諸如讀寫事件做好準備的組件。這樣,一個多帶帶的線程可以管理多個channel,從而管理多個網絡連接。
為什么使用Selector?僅用單個線程來處理多個Channels的好處是,只需要更少的線程來處理通道。事實上,可以只用一個線程處理所有的通道。對于操作系統來說,線程之間上下文切換的開銷很大,而且每個線程都要占用系統的一些資源(如內存)。因此,使用的線程越少越好。
但是,需要記住,現代的操作系統和CPU在多任務方面表現的越來越好,所以多線程的開銷隨著時間的推移,變得越來越小了。實際上,如果一個CPU有多個內核,不使用多任務可能是在浪費CPU能力。不管怎么說,關于那種設計的討論應該放在另一篇不同的文章中。在這里,只要知道使用Selector能夠處理多個通道就足夠了。
NIO如何實現非阻塞?服務器上所有Channel需要向Selector注冊,而Selector則負責監視這些Socket的IO狀態(觀察者),當其中任意一個或者多個Channel具有可用的IO操作時,該Selector的select()方法將會返回大于0的整數,該整數值就表示該Selector上有多少個Channel具有可用的IO操作,并提供了selectedKeys()方法來返回這些Channel對應的SelectionKey集合(一個SelectionKey對應一個就緒的通道)。正是通過Selector,使得服務器端只需要不斷地調用Selector實例的select()方法即可知道當前所有Channel是否有需要處理的IO操作。
注:java NIO就是多路復用IO,jdk7之后底層是epoll模型。
/** * NioServer * Date: 6/27/2016 * Time: 8:06 PM * * @author xiaodong.fan */ public class NioServer { public static void main(String[] args) throws Exception { // 1、初始化一個ServerSocketChannel ServerSocketChannel serverSocketChannel = ServerSocketChannel.open(); InetSocketAddress inetSocketAddress = new InetSocketAddress("127.0.0.1", 9999); serverSocketChannel.configureBlocking(false);// 設置為非阻塞模式,后續的accept()方法會立刻返回 serverSocketChannel.socket().bind(inetSocketAddress, 1024);// 監聽本地9999端口的請求,第二個參數限制可以建立的最大連接數 Selector selector = Selector.open(); /** * 將通道注冊到一個選擇器上(非阻塞模式與選擇器搭配會工作的更好) * 注意register()方法的第二個參數。這是一個“interest集合”,意思是在通過Selector監聽Channel時對什么事件感興趣。 * 可以監聽四種不同類型的事件:OP_CONNECT,OP_ACCEPT,OP_READ,OP_WRITE * 如果你對不止一種事件感興趣,那么可以用“位或”操作符將常量連接起來:SelectionKey.OP_READ | SelectionKey.OP_WRITE */ serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); // 2、監聽連接請求并處理 while (true) { int connects = selector.select(2000);// 每次最多阻塞2秒 if (connects == 0) { System.out.println("沒有請求..."); continue; } else { System.out.println("請求來了..."); } // 獲取監聽到有連接請求的channel對應的selectionKey Set參考文章selectedKeys = selector.selectedKeys(); // 遍歷selectionKey來訪問就緒的通道 Iterator selectedKeyIterator = selectedKeys.iterator(); while (selectedKeyIterator.hasNext()) { SelectionKey selectionKey = selectedKeyIterator.next(); if (selectionKey.isValid()) { if (selectionKey.isAcceptable()) {// 接收就緒 ServerSocketChannel channel = (ServerSocketChannel) selectionKey.channel(); // 返回一個包含新進來的連接SocketChannel,因為前面設置的非阻塞模式,這里會立即返回。 SocketChannel socketChannel = channel.accept(); if (socketChannel == null) { return; } socketChannel.configureBlocking(false); socketChannel.register(selector, SelectionKey.OP_READ); System.out.println("連接建立完成"); doWrite(socketChannel, "connection is established");// 連接建立完成,給客戶端發消息 } else if (selectionKey.isReadable()) {// 讀就緒 SocketChannel socketChannel = (SocketChannel) selectionKey.channel(); ByteBuffer readBuffer = ByteBuffer.allocate(10); while ((socketChannel.read(readBuffer)) > 0) {// // 讀取客戶端發送來的消息 readBuffer.flip(); byte[] bytes = new byte[readBuffer.remaining()]; readBuffer.get(bytes); String body = new String(bytes, "utf-8"); doWrite(socketChannel, body);// 將客戶端發送的內容原封不動的發回去 readBuffer.clear(); } socketChannel.close();//讀取數據完畢后關閉連接,如果不關閉一直處于連接狀態。 } } selectedKeyIterator.remove(); // 注意每次必須手動remove(),下次該通道變成就緒時,Selector會再次將其放入已選擇鍵集中 } } } private static void doWrite(SocketChannel socketChannel, String response) throws IOException { if (StringUtils.isNotBlank(response)) { byte[] bytes = response.getBytes(); ByteBuffer writeBuffer = ByteBuffer.allocate(1024); writeBuffer.put(bytes); writeBuffer.flip(); // 發送消息到客戶端 socketChannel.write(writeBuffer); writeBuffer.clear(); } } }
Java NIO:淺析I/O模型
socket阻塞與非阻塞,同步與異步、I/O模型
Java BIO、NIO、AIO 學習
Java NIO:NIO概述
Java NIO 系列教程
Java網絡編程——使用NIO實現非阻塞Socket通信
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/64851.html
摘要:相信你們也注意到了,但是應該大多數人都不是很熟悉,它也是一個基于,使用編寫的,完全異步的數據庫驅動,同時支持和,其項目地址。 之前的Akka系列博客接下去可能并不會經常更新了,但是后續看到一些好的點或者大家對哪些還是比較感興趣還會繼續寫幾篇,這里先跟大家說明一下。 背景 寫一個純函數式的Mysql異步驅動這個構思是公司的一個大佬提的,這將會是一個開源項目,我也很有幸能夠參與其中,嘗試寫...
摘要:從通道進行數據寫入創建一個緩沖區,填充數據,并要求通道寫入數據。三之通道主要內容通道介紹通常來說中的所有都是從通道開始的。從中選擇選擇器維護注冊過的通道的集合,并且這種注冊關系都被封裝在當中停止選擇的方法方法和方法。 由于內容比較多,我下面放的一部分是我更新在我的微信公眾號上的鏈接,微信排版比較好看,更加利于閱讀。每一篇文章下面我都把文章的主要內容給列出來了,便于大家學習與回顧。 Ja...
摘要:從使用到原理學習線程池關于線程池的使用,及原理分析分析角度新穎面向切面編程的基本用法基于注解的實現在軟件開發中,分散于應用中多出的功能被稱為橫切關注點如事務安全緩存等。 Java 程序媛手把手教你設計模式中的撩妹神技 -- 上篇 遇一人白首,擇一城終老,是多么美好的人生境界,她和他歷經風雨慢慢變老,回首走過的點點滴滴,依然清楚的記得當初愛情萌芽的模樣…… Java 進階面試問題列表 -...
閱讀 1459·2021-09-02 13:57
閱讀 1874·2019-08-30 15:55
閱讀 2413·2019-08-30 15:54
閱讀 2250·2019-08-30 15:44
閱讀 2737·2019-08-30 13:18
閱讀 486·2019-08-30 13:02
閱讀 643·2019-08-29 18:46
閱讀 1668·2019-08-29 11:25