大廠求職必看!Tomcat線程模型—全網(wǎng)最清晰的講解喂柒!

image

UNIX系統(tǒng)的I/O模型

同步阻塞I/O不瓶、同步非阻塞I/O、I/O多路復(fù)用灾杰、信號(hào)驅(qū)動(dòng)I/O和異步I/O蚊丐。

什么是 I/O

就是計(jì)算機(jī)內(nèi)存與外部設(shè)備之間拷貝數(shù)據(jù)的過程。

為什么需要 I/O

CPU訪問內(nèi)存的速度遠(yuǎn)遠(yuǎn)高于外部設(shè)備艳吠,因此CPU是先把外部設(shè)備的數(shù)據(jù)讀到內(nèi)存里麦备,然后再進(jìn)行處理。
當(dāng)你的程序通過CPU向外部設(shè)備發(fā)出一個(gè)讀指令昭娩,數(shù)據(jù)從外部設(shè)備拷貝到內(nèi)存需要一段時(shí)間凛篙,這時(shí)CPU沒事干,你的程序是:

  • 主動(dòng)把CPU讓給別人
  • 還是讓CPU不停查:數(shù)據(jù)到了嗎栏渺?數(shù)據(jù)到了嗎呛梆?…

這就是I/O模型要解決的問題。

Java I/O模型

對(duì)于一個(gè)網(wǎng)絡(luò)I/O通信過程磕诊,比如網(wǎng)絡(luò)數(shù)據(jù)讀取削彬,會(huì)涉及兩個(gè)對(duì)象:

  • 調(diào)用這個(gè)I/O操作的用戶線程
  • 操作系統(tǒng)內(nèi)核

一個(gè)進(jìn)程的地址空間分為用戶空間和內(nèi)核空間全庸,用戶線程不能直接訪問內(nèi)核空間。
當(dāng)用戶線程發(fā)起I/O操作后(Selector發(fā)出的select調(diào)用就是一個(gè)I/O操作)融痛,網(wǎng)絡(luò)數(shù)據(jù)讀取操作會(huì)經(jīng)歷兩個(gè)步驟:

  1. 用戶線程等待內(nèi)核將數(shù)據(jù)從網(wǎng)卡拷貝到內(nèi)核空間
  2. 內(nèi)核將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間

有人會(huì)好奇壶笼,內(nèi)核數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間,這樣會(huì)不會(huì)有點(diǎn)浪費(fèi)雁刷?

畢竟實(shí)際上只有一塊內(nèi)存覆劈,能否直接把內(nèi)存地址指向用戶空間可以讀取沛励?

Linux中有個(gè)叫mmap的系統(tǒng)調(diào)用责语,可以將磁盤文件映射到內(nèi)存,省去了內(nèi)核和用戶空間的拷貝目派,但不支持網(wǎng)絡(luò)通信場(chǎng)景坤候!

各種I/O模型的區(qū)別就是這兩個(gè)步驟的方式不一樣。

同步阻塞I/O

用戶線程發(fā)起read調(diào)用后就阻塞了企蹭,讓出CPU白筹。內(nèi)核等待網(wǎng)卡數(shù)據(jù)到來,把數(shù)據(jù)從網(wǎng)卡拷貝到內(nèi)核空間谅摄,接著把數(shù)據(jù)拷貝到用戶空間徒河,再把用戶線程叫醒。

image

同步非阻塞I/O

用戶進(jìn)程主動(dòng)發(fā)起read調(diào)用送漠,這是個(gè)系統(tǒng)調(diào)用顽照,CPU由用戶態(tài)切換到內(nèi)核態(tài),執(zhí)行內(nèi)核代碼闽寡。

內(nèi)核發(fā)現(xiàn)該socket上的數(shù)據(jù)已到內(nèi)核空間代兵,將用戶線程掛起,然后把數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間爷狈,再喚醒用戶線程奢人,read調(diào)用返回。

用戶線程不斷發(fā)起read調(diào)用淆院,數(shù)據(jù)沒到內(nèi)核空間時(shí)何乎,每次都返回失敗,直到數(shù)據(jù)到了內(nèi)核空間,這次read調(diào)用后臭增,在等待數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間這段時(shí)間里,線程還是阻塞的各墨,等數(shù)據(jù)到了用戶空間再把線程叫醒。

image

I/O多路復(fù)用

用戶線程的讀取操作分成兩步:

  • 線程先發(fā)起select調(diào)用启涯,問內(nèi)核:數(shù)據(jù)準(zhǔn)備好了嗎贬堵?
  • 等內(nèi)核把數(shù)據(jù)準(zhǔn)備好了恃轩,用戶線程再發(fā)起read調(diào)用
  • 在等待數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間這段時(shí)間里,線程還是阻塞的

為什么叫I/O多路復(fù)用黎做?

因?yàn)橐淮蝧elect調(diào)用可以向內(nèi)核查多個(gè)數(shù)據(jù)通道(Channel)的狀態(tài)叉跛。

image

NIO API可以不用Selector,就是同步非阻塞蒸殿。使用了Selector就是IO多路復(fù)用筷厘。

異步I/O

用戶線程發(fā)起read調(diào)用的同時(shí)注冊(cè)一個(gè)回調(diào)函數(shù),read立即返回宏所,等內(nèi)核將數(shù)據(jù)準(zhǔn)備好后酥艳,再調(diào)用指定的回調(diào)函數(shù)完成處理。在這個(gè)過程中爬骤,用戶線程一直沒有阻塞充石。

image

信號(hào)驅(qū)動(dòng)I/O

可以把信號(hào)驅(qū)動(dòng)I/O理解為“半異步”,非阻塞模式是應(yīng)用不斷發(fā)起read調(diào)用查詢數(shù)據(jù)到了內(nèi)核沒有霞玄,而信號(hào)驅(qū)動(dòng)把這個(gè)過程異步了骤铃,應(yīng)用發(fā)起read調(diào)用時(shí)注冊(cè)了一個(gè)信號(hào)處理函數(shù),其實(shí)是個(gè)回調(diào)函數(shù)溃列,數(shù)據(jù)到了內(nèi)核后劲厌,內(nèi)核觸發(fā)這個(gè)回調(diào)函數(shù)膛薛,應(yīng)用在回調(diào)函數(shù)里再發(fā)起一次read調(diào)用去讀內(nèi)核的數(shù)據(jù)听隐。

所以是半異步。

NioEndpoint組件

Tomcat的NioEndpoint實(shí)現(xiàn)了I/O多路復(fù)用模型哄啄。

工作流程

Java的多路復(fù)用器的使用:

  • 創(chuàng)建一個(gè)Selector雅任,在其上注冊(cè)感興趣的事件,然后調(diào)用select方法咨跌,等待感興趣的事情發(fā)生
  • 感興趣的事情發(fā)生了沪么,比如可讀了,就創(chuàng)建一個(gè)新的線程從Channel中讀數(shù)據(jù)

NioEndpoint包含LimitLatch锌半、Acceptor禽车、Poller、SocketProcessor和Executor共5個(gè)組件刊殉。

image

LimitLatch

連接控制器殉摔,控制最大連接數(shù),NIO模式下默認(rèn)是8192记焊。

image

當(dāng)連接數(shù)到達(dá)最大時(shí)阻塞線程逸月,直到后續(xù)組件處理完一個(gè)連接后將連接數(shù)減1。
到達(dá)最大連接數(shù)后遍膜,os底層還是會(huì)接收客戶端連接碗硬,但用戶層已不再接收瓤湘。
核心代碼:

public class LimitLatch {
    private class Sync extends AbstractQueuedSynchronizer {

        @Override
        protected int tryAcquireShared() {
            long newCount = count.incrementAndGet();
            if (newCount > limit) {
                count.decrementAndGet();
                return -1;
            } else {
                return 1;
            }
        }

        @Override
        protected boolean tryReleaseShared(int arg) {
            count.decrementAndGet();
            return true;
        }
    }

    private final Sync sync;
    private final AtomicLong count;
    private volatile long limit;

    // 線程調(diào)用該方法,獲得接收新連接的許可恩尾,線程可能被阻塞
    public void countUpOrAwait() throws InterruptedException {
      sync.acquireSharedInterruptibly(1);
    }

    // 調(diào)用這個(gè)方法來釋放一個(gè)連接許可弛说,則前面阻塞的線程可能被喚醒
    public long countDown() {
      sync.releaseShared(0);
      long result = getCount();
      return result;
   }
}

用戶線程調(diào)用LimitLatch#countUpOrAwait拿到鎖,若無法獲取特笋,則該線程會(huì)被阻塞在AQS隊(duì)列剃浇。

AQS又是怎么知道是阻塞還是不阻塞用戶線程的呢?

由AQS的使用者決定猎物,即內(nèi)部類Sync決定虎囚,因?yàn)镾ync類重寫了AQS#tryAcquireShared():若當(dāng)前連接數(shù)count < limit,線程能獲取鎖蔫磨,返回1淘讥,否則返回-1。

如何用戶線程被阻塞到了AQS的隊(duì)列堤如,由Sync內(nèi)部類決定什么時(shí)候喚醒蒲列,Sync重寫AQS#tryReleaseShared(),當(dāng)一個(gè)連接請(qǐng)求處理完了搀罢,又可以接收新連接蝗岖,這樣前面阻塞的線程將會(huì)被喚醒。

LimitLatch用來限制應(yīng)用接收連接的數(shù)量榔至,Acceptor用來限制系統(tǒng)層面的連接數(shù)量抵赢,首先是LimitLatch限制,應(yīng)用層處理不過來了唧取,連接才會(huì)堆積在操作系統(tǒng)的Queue铅鲤,而Queue的大小由acceptCount控制。

Acceptor

Acceptor實(shí)現(xiàn)了Runnable接口枫弟,因此可以跑在單獨(dú)線程里邢享,在這個(gè)死循環(huán)里調(diào)用accept接收新連接。一旦有新連接請(qǐng)求到達(dá)淡诗,accept方法返回一個(gè)Channel對(duì)象骇塘,接著把Channel對(duì)象交給Poller去處理。

一個(gè)端口號(hào)只能對(duì)應(yīng)一個(gè)ServerSocketChannel韩容,因此這個(gè)ServerSocketChannel是在多個(gè)Acceptor線程之間共享的款违,它是Endpoint的屬性,由Endpoint完成初始化和端口綁定宙攻。

可以同時(shí)有過個(gè)Acceptor調(diào)用accept方法奠货,accept是線程安全的。

初始化

protected void initServerSocket() throws Exception {
    if (!getUseInheritedChannel()) {
        serverSock = ServerSocketChannel.open();
        socketProperties.setProperties(serverSock.socket());
        InetSocketAddress addr = new InetSocketAddress(getAddress(), getPortWithOffset());

        serverSock.socket().bind(addr,getAcceptCount());
    } else {
        // Retrieve the channel provided by the OS
        Channel ic = System.inheritedChannel();
        if (ic instanceof ServerSocketChannel) {
            serverSock = (ServerSocketChannel) ic;
        }
        if (serverSock == null) {
            throw new IllegalArgumentException(sm.getString("endpoint.init.bind.inherited"));
        }
    }
    // 阻塞模式
    serverSock.configureBlocking(true); //mimic APR behavior
}
  • bind方法的 getAcceptCount() 參數(shù)表示os的等待隊(duì)列長(zhǎng)度座掘。當(dāng)應(yīng)用層的連接數(shù)到達(dá)最大值時(shí)递惋,os可以繼續(xù)接收連接柔滔,os能繼續(xù)接收的最大連接數(shù)就是這個(gè)隊(duì)列長(zhǎng)度,可以通過acceptCount參數(shù)配置萍虽,默認(rèn)是100
image

ServerSocketChannel通過accept()接受新的連接睛廊,accept()方法返回獲得SocketChannel對(duì)象,然后將SocketChannel對(duì)象封裝在一個(gè)PollerEvent對(duì)象中杉编,并將PollerEvent對(duì)象壓入Poller的Queue里超全。

這是個(gè)典型的“生產(chǎn)者-消費(fèi)者”模式,Acceptor與Poller線程之間通過Queue通信邓馒。

Poller

本質(zhì)是一個(gè)Selector嘶朱,也跑在單獨(dú)線程里。

Poller在內(nèi)部維護(hù)一個(gè)Channel數(shù)組光酣,它在一個(gè)死循環(huán)里不斷檢測(cè)Channel的數(shù)據(jù)就緒狀態(tài)疏遏,一旦有Channel可讀,就生成一個(gè)SocketProcessor任務(wù)對(duì)象扔給Executor去處理救军。

內(nèi)核空間的接收連接是對(duì)每個(gè)連接都產(chǎn)生一個(gè)channel财异,該channel就是Acceptor里accept方法得到的scoketChannel,后面的Poller在用selector#select監(jiān)聽內(nèi)核是否準(zhǔn)備就緒唱遭,才知道監(jiān)聽內(nèi)核哪個(gè)channel戳寸。

維護(hù)了一個(gè) Queue:

image

SynchronizedQueue的方法比如offer、poll拷泽、size和clear都使用synchronized修飾疫鹊,即同一時(shí)刻只有一個(gè)Acceptor線程讀寫Queue。

同時(shí)有多個(gè)Poller線程在運(yùn)行跌穗,每個(gè)Poller線程都有自己的Queue订晌。

每個(gè)Poller線程可能同時(shí)被多個(gè)Acceptor線程調(diào)用來注冊(cè)PollerEvent虏辫。

Poller的個(gè)數(shù)可以通過pollers參數(shù)配置蚌吸。

職責(zé)

  • Poller不斷地通過內(nèi)部的Selector對(duì)象向內(nèi)核查詢Channel狀態(tài),一旦可讀就生成任務(wù)類SocketProcessor交給Executor處理
image

Poller循環(huán)遍歷檢查自己所管理的SocketChannel是否已超時(shí)砌庄。若超時(shí)就關(guān)閉該SocketChannel

SocketProcessor

Poller會(huì)創(chuàng)建SocketProcessor任務(wù)類交給線程池處理羹唠,而SocketProcessor實(shí)現(xiàn)了Runnable接口,用來定義Executor中線程所執(zhí)行的任務(wù)娄昆,主要就是調(diào)用Http11Processor組件處理請(qǐng)求:Http11Processor讀取Channel的數(shù)據(jù)來生成ServletRequest對(duì)象佩微。

Http11Processor并非直接讀取Channel。因?yàn)門omcat支持同步非阻塞I/O萌焰、異步I/O模型哺眯,在Java API中,對(duì)應(yīng)Channel類不同扒俯,比如有AsynchronousSocketChannel和SocketChannel奶卓,為了對(duì)Http11Processor屏蔽這些差異一疯,Tomcat設(shè)計(jì)了一個(gè)包裝類叫作SocketWrapper,Http11Processor只調(diào)用SocketWrapper的方法去讀寫數(shù)據(jù)夺姑。

Executor

線程池墩邀,負(fù)責(zé)運(yùn)行SocketProcessor任務(wù)類,SocketProcessor的run方法會(huì)調(diào)用Http11Processor來讀取和解析請(qǐng)求數(shù)據(jù)盏浙。我們知道眉睹,Http11Processor是應(yīng)用層協(xié)議的封裝,它會(huì)調(diào)用容器獲得響應(yīng)废膘,再把響應(yīng)通過Channel寫出竹海。

Tomcat定制的線程池,它負(fù)責(zé)創(chuàng)建真正干活的工作線程丐黄。就是執(zhí)行SocketProcessor#run站削,即解析請(qǐng)求并通過容器來處理請(qǐng)求,最終調(diào)用Servlet孵稽。

Tomcat的高并發(fā)設(shè)計(jì)

高并發(fā)就是能快速地處理大量請(qǐng)求许起,需合理設(shè)計(jì)線程模型讓CPU忙起來,盡量不要讓線程阻塞菩鲜,因?yàn)橐蛔枞跋福珻PU就閑了。

有多少任務(wù)接校,就用相應(yīng)規(guī)模的線程數(shù)去處理猛频。

比如NioEndpoint要完成三件事情:接收連接、檢測(cè)I/O事件和處理請(qǐng)求蛛勉,關(guān)鍵就是把這三件事情分別定制線程數(shù)處理:

  • 專門的線程組去跑Acceptor鹿寻,并且Acceptor的個(gè)數(shù)可以配置
  • 專門的線程組去跑Poller,Poller的個(gè)數(shù)也可以配置
  • 具體任務(wù)的執(zhí)行也由專門的線程池來處理诽凌,也可以配置線程池的大小

總結(jié)

I/O模型是為了解決內(nèi)存和外部設(shè)備速度差異毡熏。

  • 所謂阻塞或非阻塞是指應(yīng)用程序在發(fā)起I/O操作時(shí),是立即返回還是等待
  • 同步和異步侣诵,是指應(yīng)用程序在與內(nèi)核通信時(shí)痢法,數(shù)據(jù)從內(nèi)核空間到應(yīng)用空間的拷貝,是由內(nèi)核主動(dòng)發(fā)起還是由應(yīng)用程序來觸發(fā)杜顺。

Tomcat#Endpoint組件的主要工作就是處理I/O财搁,而NioEndpoint利用Java NIO API實(shí)現(xiàn)了多路復(fù)用I/O模型。

讀寫數(shù)據(jù)的線程自己不會(huì)阻塞在I/O等待上躬络,而是把這個(gè)工作交給Selector尖奔。

當(dāng)客戶端發(fā)起一個(gè)HTTP請(qǐng)求時(shí),首先由Acceptor#run中的

socket = endpoint.serverSocketAccept();

接收連接,然后傳遞給名稱為Poller的線程去偵測(cè)I/O事件提茁,Poller線程會(huì)一直select仗嗦,選出內(nèi)核將數(shù)據(jù)從網(wǎng)卡拷貝到內(nèi)核空間的 channel(也就是內(nèi)核已經(jīng)準(zhǔn)備好數(shù)據(jù))然后交給名稱為Catalina-exec的線程去處理,這個(gè)過程也包括內(nèi)核將數(shù)據(jù)從內(nèi)核空間拷貝到用戶空間這么一個(gè)過程甘凭,所以對(duì)于exec線程是阻塞的稀拐,此時(shí)用戶空間(也就是exec線程)就接收到了數(shù)據(jù),可以解析然后做業(yè)務(wù)處理了丹弱。

作者:JavaEdge.
原文鏈接:https://blog.csdn.net/qq_33589510/article/details/119082311

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末德撬,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子躲胳,更是在濱河造成了極大的恐慌蜓洪,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,546評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件坯苹,死亡現(xiàn)場(chǎng)離奇詭異隆檀,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)粹湃,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,224評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門恐仑,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人为鳄,你說我怎么就攤上這事裳仆。” “怎么了孤钦?”我有些...
    開封第一講書人閱讀 164,911評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵歧斟,是天一觀的道長(zhǎng)。 經(jīng)常有香客問我偏形,道長(zhǎng)静袖,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,737評(píng)論 1 294
  • 正文 為了忘掉前任俊扭,我火速辦了婚禮队橙,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘统扳。我一直安慰自己喘帚,他們只是感情好畅姊,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,753評(píng)論 6 392
  • 文/花漫 我一把揭開白布咒钟。 她就那樣靜靜地躺著,像睡著了一般若未。 火紅的嫁衣襯著肌膚如雪朱嘴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,598評(píng)論 1 305
  • 那天,我揣著相機(jī)與錄音萍嬉,去河邊找鬼乌昔。 笑死,一個(gè)胖子當(dāng)著我的面吹牛壤追,可吹牛的內(nèi)容都是我干的磕道。 我是一名探鬼主播,決...
    沈念sama閱讀 40,338評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼行冰,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼溺蕉!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起悼做,我...
    開封第一講書人閱讀 39,249評(píng)論 0 276
  • 序言:老撾萬榮一對(duì)情侶失蹤疯特,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后肛走,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體漓雅,經(jīng)...
    沈念sama閱讀 45,696評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,888評(píng)論 3 336
  • 正文 我和宋清朗相戀三年朽色,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了邻吞。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,013評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡葫男,死狀恐怖吃衅,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情腾誉,我是刑警寧澤徘层,帶...
    沈念sama閱讀 35,731評(píng)論 5 346
  • 正文 年R本政府宣布,位于F島的核電站利职,受9級(jí)特大地震影響趣效,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜猪贪,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,348評(píng)論 3 330
  • 文/蒙蒙 一跷敬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧热押,春花似錦西傀、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,929評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至牙寞,卻和暖如春饺鹃,著一層夾襖步出監(jiān)牢的瞬間莫秆,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,048評(píng)論 1 270
  • 我被黑心中介騙來泰國(guó)打工悔详, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留镊屎,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,203評(píng)論 3 370
  • 正文 我出身青樓茄螃,卻偏偏與公主長(zhǎng)得像缝驳,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子归苍,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,960評(píng)論 2 355

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