Java多線程:Linux多路複用,Java NIO與Netty簡述

JVM的多路複用器實現原理

  • Linux 2.5之前:select/poll
  • Linux 2.6之後: epoll
  • Windows: Winsock的select模型(感謝評論指正,僅Java NIO.2使用了Windows IOCP,因爲Netty沒有采用NIO.2此處不展開)
  • Free BSD, OS X: kqueue

下面僅講解Linux的多路複用。html

Linux中的IO

Linux的IO將全部外部設備都看做文件來操做,與外部設備的操做均可以看作文件操做,其讀寫都使用內核提供的系統調用,內核會返回一個文件描述符(fd, file descriptor),例如socket讀寫使用socketfd。描述符是一個索引,指向內核中一個結構體,應用程序對文件的讀寫經過描述符完成。java

一個基本的IO,涉及兩個系統對象:調用這個IO進程的對象,系統內核,read操做發生時流程以下:linux

  1. 經過read系統調用向內核發起讀請求。
  2. 內核向硬件發送讀指令,並等待讀就緒。
  3. 內核把將要讀取的數據複製到描述符所指向的內核緩存區中。
  4. 將數據從內核緩存區拷貝到用戶進程空間中。

Linux I/O模型簡介

  1. 阻塞I/O模型:最經常使用,全部文件操做都是阻塞的。
  2. 非阻塞I/O模型:緩衝區無數據則返回,通常採用輪詢的方式作狀態檢查。
  3. I/O複用模型:詳細見下
  4. 信號驅動I/O:使用信號回調應用,內核通知用戶什麼時候開啓一個I/O操做。
  5. 異步I/O:內核操做完成後進行通知,內核通知用戶什麼時候完成一個I/O操做。

Linux IO 多路複用

使用場景

  • 客戶處理多個描述符(交互輸入,網絡套接口)
  • 客戶處理多個套接口(少見)
  • TCP服務器既要處理監聽套接口,又要處理已鏈接套接口。
  • 一個服務器既要處理TCP,又要處理UDP
  • 一個服務器處理多個服務/多個協議

與多進程/多線程對比

I/O多路複用系統開銷小,系統沒必要建立進程/線程,也不須要維護這些進程/線程。git

系統調用

目前支持I/O多路複用的系統調用包括select,pselect,poll,epoll,I/O多路複用即經過一種機制,一個進程能夠監視多個描述符,一旦某個描述符準備就緒,就可以通知程序進行相應的讀寫操做。github

select/poll

select目前在全部平臺支持,select函數監視文件操做符(將fd加入fdset),循環遍歷fdset內的fd獲取是否有資源的信息,若遍歷完全部fdset內的fd後無資源可用,則select讓該進程睡眠,直到有資源可用或超時則喚醒select進程,以後select繼續循環遍歷,找到就緒的fd後返回。select單個進程打開的fd有必定限制,由FD_SETSIZE設置,默認爲1024(32位)和2048(64位)。編程

poll與select的主要區別是不使用fdset,而是使用pollfd結構(本質鏈表結構),於是沒有fd數目限制。windows

poll和select共有的問題:數組

  • 每次select/poll找到就緒的fd,都須要把fdset/pollfd進行內存複製。
  • select/poll,都要在內核中遍歷全部傳遞來的fd來尋找就緒的fd,隨着監視的fd數量增長,效率也會降低。

epoll

Linux 2.6內核中提出了epoll,epoll包括epoll_create,epoll_etl,epoll_wait三個函數分別負責建立epoll,註冊監聽的事件和等待事件產生。promise

  • epoll每次註冊新的事件到epoll中時,都會把全部fd拷貝進內核,而不是在epoll_wait時重複拷貝,保證每一個fd在整個過程當中僅拷貝一次。此外,epoll將內核與用戶進程空間mmap到同一塊內存,將fd消息存於該內存避免了沒必要要的拷貝。
  • epoll使用事件的就緒通知方式,經過epoll_ctl註冊fd,一旦該fd就緒,內核就經過回調函數把就緒的fd加入一個就緒鏈表,喚醒epoll_wait進入睡眠的進程,epoll_wait通知消息給應用程序後再次睡眠。所以epoll不隨着fd數目增長效率降低,只有活躍fd纔會調用回調函數,效率與鏈接總數無關。
  • epoll沒有最大併發鏈接的限制,1G內存約能監聽10萬個端口。

epoll有LT模式和ET模式:緩存

  • LT模式:epoll_wait檢測到fd並通知後,應用程序能夠不馬上處理,下次調用epoll_wait,會再次通知;
  • ET模式:應用程序必須馬上處理,下次調用,不會再通知此事件。ET模式效率更高,epoll工做在ET模式下必須使用非阻塞套接字。

性能對比

  • 若是有大量的idle-connection或dead-connection,epoll效率比select/poll高不少。
  • 鏈接少鏈接十分活躍的狀況下,select/poll的性能可能比epoll好。

Java的IO模式

  1. BIO:即傳統Socket編程,線程數:客戶端訪問數爲1:1,因爲線程數膨脹後系統性能會急劇降低,致使了BIO的低效。
  2. 僞異步I/O:爲了解決一個鏈路一個線程的問題,引入線程池處理多個客戶端接入請求,能夠靈活調配線程資源,能夠限制線程數量防止膨脹,但底層還是阻塞模型,高客戶端訪問時,會有通訊阻塞的問題。
  3. NIO:Java NIO的核心爲Channels, Buffers, Selectors。Channel有點像流,數據能夠從Channel讀到Buffer中,也能夠從Buffer寫到Channel內。而Selector則被用於多路複用,Java NIO能夠把Channel註冊到Selector上,以後,Selector會獲取進入就緒狀態的Channel(Selector進行循環的select/poll/epoll/IOCP操做),並進行後續操做。Selector是NIO實現的關鍵。Java NIO編程較爲複雜。
  4. AIO:NIO.2引入的異步通道概念,不須要多路複用器對註冊的通道輪詢便可異步讀寫,簡化了NIO的編程。(可是Netty做者稱AIO的性能並不比NIO和epoll好)

Netty

使用Netty而非直接使用Java NIO出於如下緣由:

  1. Java NIO的API過於繁雜。
  2. Java NIO開發須要瞭解Reactor模型,Java多線程等。
  3. Java NIO低可靠性。
  4. Java NIO有不少臭名昭著的BUG,如NIO的epoll空輪詢bug

下面簡單介紹下Netty的部分功能。

ByteBuf

Netty的ByteBuf依然是Byte數組緩衝區,提供對基礎類型,byte[]數組,ByteBuffer,ByteBuf的讀寫,緩衝區自身的copy和slice,操做指針,字節序,構造實例等功能。相對於ByteBuffer,ByteBuf的讀寫採用兩個指針而非flip方案,增長了可靠性,並提供了自動擴展方案。

ByteBuf的內存池實現比較複雜,可是否使用內存池,有較大的性能差別。隨着JVM和JIT的發展,對象的分配和回收是個輕量級的工做,可是對於緩衝區Buffer,特別是堆外直接內存的分配和回收則仍很耗時。Netty提供了基於內存池的緩衝區重用機制,帶來了性能提升。UnpooledByteBufAllocator在Netty4仍然是默認的allocator,但在大多狀況下,PooledByteBufAllocator將帶來更高性能。更改默認方式僅需在初始化時加以設置:

客戶端

b.group(group)
.channel(NioSocketChannel.class)
 .option(ChannelOption.TCP_NODELAY, true)
 .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)

服務端

.childHandler(new ChannelInitializer<SocketChannel>() {
  @Override
  public void initChannel(SocketChannel ch) throws Exception {
  ch.config().setAllocator(PooledByteBufAllocator.DEFAULT);

Channel和Unsafe

Netty的Channel和NIO的Channel相似,但有本身的子類和實現。Unsafe則封裝了Netty不但願用戶調用的API,做爲Channel的輔助類。

Channel包括而不限於網絡的讀,寫,客戶端發起鏈接,主動關閉鏈接,鏈路關閉,獲取雙方通訊地址等功能。Channel也包括了Netty框架的相關功能,如獲取該Channel的EventLoop,獲取緩衝區分配器ByteBufAllocator和Pipeline等。Channel封裝了Java NIO不統一的SocketChannel和ServerSockerChannel,其接口定義大而全。

Unsafe是Channel的輔助接口,實際的I/O讀寫操做都是由Unsafe完成的。包括register,bind,disconnect,close,write,flush幾個接口,能夠看到它更接近於本來的Java NIO Channel。

ChannelPipeline和ChannelHandler

Netty的pipeline和handler機制相似於Servlet和Filter,爲了方便攔截和業務邏輯定製。Netty將Channel的管道抽象爲ChannelPipeline,讓消息在其中流動,ChannelPipeline持有消息攔截器ChannelHandler列表,能夠經過增長和刪除handler來改變業務邏輯,而不是對已有的handler進行修改。

ChannelHandler的種類繁多,且用戶能夠自定義,自定義時,一般只須要繼承ChannelHandlerAdapter並重寫爲了實現業務邏輯的必要方法便可。

此外,ChannelPipeline支持運行時動態添加或刪除ChannelHandler,某些場景下這個特性很實用。

ChannelPipeline是線程安全的,但ChannelHandler不是線程安全的,須要用戶本身進行保障。

EventLoop和EventLoopGroup

Netty的線程模型得以無鎖化依賴於其NioEventLoop。所以,此處詳細展開。

Netty的線程模型

  • Reactor模型:全部I/O都在NIO線程完成,NIO線程做爲服務端,接收全部客戶端TCP鏈接,並處理鏈路。
  • Reactor多線程模型:由專門的一個Acceptor線程監聽服務端,接收客戶端的TCP請求,並調度一個subReactor線程池,該線程池維護多個處理線程,一個NIO線程能夠處理N條鏈路。
  • 主從Reactor多線程模型:在上述Reactor多線程模型基礎上,服務端接收客戶端TCP請求的再也不是一個NIO線程,而是一個獨立的NIO線程池,Acceptor線程池僅用於客戶端的登陸和認證,鏈路創建成功就交給subReactor線程池作後續操做。

Netty線程池:服務端啓動時,建立bossGroup, workerGroup兩個NioEventLoopGroup,其實是兩個Reactor線程池,一個用於接收客戶端TCP請求,一個用於處理I/O讀寫或執行業務。

  • 接收線程池(bossGroup)職責:接收客戶端TCP鏈接,初始化Channel參數,將鏈路狀態變動通知給ChannelPipeline。
  • I/O處理線程池(workerGroup)職責:異步讀取通訊對端數據,發送讀事件給ChannelPipeline;異步發送消息到通訊對端,調用ChannelPipeline的消息發送接口;執行業務或系統調用/定時任務等工做。

經過調整bossGroup和workerGroup的線程個數,group()函數參數數量,是否共享線程池等,Netty的Reactor模型能夠在單線程,多線程,主從多線程等模式中切換。

NioEventLoop

Netty的NioEventLoop讀取到消息以後,直接調用ChannelPipeline的fireChannelRead方法,只要用戶不切換線程,一直都由NioEventLoop調用用戶的Handler,期間不切換線程,而是串行化運行handler,避免了多線程操做的鎖的競爭,達到性能最優。

NioEventLoop不純粹是一個IO線程,它既能夠處理系統Task又能夠處理定時任務。

Future和Promise

Future起源於JDK的Future,Netty的Future命名爲ChannelFuture,與Channel操做有關。Netty中全部操做都是異步的,所以,獲取異步操做結果,就要交給ChannelFuture。ChannelFuture有completed何uncompleted兩種狀態,建立後處於uncompleted狀態,一旦I/O操做完成,則被設置成completed狀態,此時可能操做失敗,操做成功或操做被取消。和JDK的Future相似,ChannelFuture有不少方便的API,包括獲取操做結果,添加事件監聽器,取消I/O操做,同步等待等。

Promise是可寫的Future,用於設置I/O額結果。Netty發起I/O操做時,會建立一個新的Promise對象。

參考文獻

聊聊IO多路複用之select、poll、epoll詳解
關於同步,異步,阻塞,非阻塞,IOCP/epoll,select/poll,AIO ,NIO ,BIO的總結
【Java】從BIO、NIO到Linux下的IO多路複用
OSX/iOS中多路I/O複用總結
java nio及操做系統底層原理
Select函數實現原理分析
Netty4底層用對象池和不用對象池實踐優化
設置Netty接收Buff爲堆內存模式
關於java nio在windows下實現

相關閱讀:NIO.2

NIO.2 uses IOCP
在 Java 7 中體會 NIO.2 異步執行的快樂
Java IO & NIO & NIO2
5種調優Java NIO和NIO.2的方式

相關文章
相關標籤/搜索