Java多線程基礎(chǔ)——線程模型

前言

??在Android應(yīng)用開發(fā)中,由于Android系統(tǒng)的單線程模型(UI主線程)罢低,使得一些耗時操作必須放在子線程執(zhí)行;又由于線程間需要交互信息网持,在多線程環(huán)境中,需要做好同步操作萍倡,以防止不可預(yù)期的錯誤發(fā)生辟汰。因此帖汞,掌握多線程相關(guān)知識對于開發(fā)尤為重要戴而。比如在我們常用的Okhttp所意,Rxjava等框架中都可以看見多線程的身影(ThreadPoolExecutor)

硬件概述

??由于計算機(jī)的存儲設(shè)備(IO)與處理器(CPU)的運算能力之間有幾個數(shù)量級的差距鹿鳖,所以現(xiàn)代計算機(jī)系統(tǒng)都不得不加入一層讀寫速度盡可能接近處理器運算速度的高速緩存(cache)來作為內(nèi)存與處理器之間的緩沖:將運算需要使用到的數(shù)據(jù)復(fù)制到緩存中,讓運算能快速進(jìn)行,當(dāng)運算結(jié)束后再從緩存同步回內(nèi)存之中沒這樣處理器就無需等待緩慢的內(nèi)存讀寫了命满。
??基于高速緩存的存儲交互很好地解決了處理器與內(nèi)存的速度矛盾胶台,但是引入了一個新的問題:緩存一致性(Cache Coherence)。在多處理器系統(tǒng)中韩脏,每個處理器都有自己的高速緩存铸磅,而他們又共享同一主存,如下圖所示:多個處理器運算任務(wù)都涉及同一塊主存吹散,需要一種協(xié)議可以保障數(shù)據(jù)的一致性八酒,這類協(xié)議有MSI、MESI界轩、MOSI及Dragon Protocol等。Java虛擬機(jī)內(nèi)存模型中定義的內(nèi)存訪問操作與硬件的緩存訪問操作是具有可比性的抖甘,后續(xù)將介紹Java內(nèi)存模型与殃。
??除此之外,為了使得處理器內(nèi)部的運算單元能竟可能被充分利用米奸,處理器可能會對輸入代碼進(jìn)行亂起執(zhí)行(Out-Of-Order Execution)優(yōu)化爽篷,處理器會在計算之后將對亂序執(zhí)行的代碼進(jìn)行結(jié)果重組逐工,保證結(jié)果準(zhǔn)確性。與處理器的亂序執(zhí)行優(yōu)化類似棕硫,Java虛擬機(jī)的即時編譯器中也有類似的指令重排序(Instruction Recorder)優(yōu)化袒啼。

model_cache.png

現(xiàn)代計算機(jī)設(shè)備可能不止有一級緩存蚓再,可能還有二級,三級緩存靶庙,一般CPU讀取數(shù)據(jù)的優(yōu)先級是寄存器(register)->高速緩存(一級娃属,二級,三級等待)->主內(nèi)存

Java內(nèi)存模型

??Java內(nèi)存模型的主要目標(biāo)是定義程序中各個變量的訪問規(guī)則恬吕,即在虛擬機(jī)中將變量存儲到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)须床。 Java內(nèi)存模型中規(guī)定了所有的變量都存儲在主內(nèi)存中,每條線程還有自己的工作內(nèi)存(可以與前面將的處理器的高速緩存類比)钠惩,線程的工作內(nèi)存中保存了該線程使用到的變量到主內(nèi)存副本拷貝篓跛,線程對變量的所有操作(讀取、賦值)都必須在工作內(nèi)存中進(jìn)行蔬咬,而不能直接讀寫主內(nèi)存中的變量沐寺。不同線程之間無法直接訪問對方工作內(nèi)存中的變量,線程間變量值的傳遞均需要在主內(nèi)存來完成狐援,線程究孕、主內(nèi)存和工作內(nèi)存的交互關(guān)系如下圖所示:


java_thread.png

內(nèi)存間交互操作

??關(guān)于主內(nèi)存與工作內(nèi)存之間的具體交互協(xié)議厨诸,即一個變量如何從主內(nèi)存拷貝到工作內(nèi)存、如何從工作內(nèi)存同步到主內(nèi)存之間的實現(xiàn)細(xì)節(jié)批钠,Java內(nèi)存模型定義了以下八種操作來完成:

·lock(鎖定):  作用于主內(nèi)存的變量得封,把一個變量標(biāo)識為一條線程獨占狀態(tài)忙上。
·unlock(解鎖):作用于主內(nèi)存變量闲坎,把一個處于鎖定狀態(tài)的變量釋放出來,釋放后的變量才可以被其他線程鎖定梗逮。
·read(讀刃辶铩):  作用于主內(nèi)存變量,把一個變量值從主內(nèi)存?zhèn)鬏數(shù)骄€程的工作內(nèi)存中底哗,以便隨后的load動作使用
·load(載入):  作用于工作內(nèi)存的變量跋选,它把read操作從主內(nèi)存中得到的變量值放入工作內(nèi)存的變量副本中。
·use(使用):   作用于工作內(nèi)存的變量坠韩,把工作內(nèi)存中的一個變量值傳遞給執(zhí)行引擎炼列,每當(dāng)虛擬機(jī)遇到一個需要使用變量的值的字節(jié)碼指令時將會執(zhí)行這個操作。
·assign(賦值):作用于工作內(nèi)存的變量须蜗,它把一個從執(zhí)行引擎接收到的值賦值給工作內(nèi)存的變量目溉,每當(dāng)虛擬機(jī)遇到一個給變量賦值的字節(jié)碼指令時執(zhí)行這個操作缭付。
·store(存儲): 作用于工作內(nèi)存的變量,把工作內(nèi)存中的一個變量的值傳送到主內(nèi)存中秫舌,以便隨后的write的操作绣檬。
·write(寫入): 作用于主內(nèi)存的變量,它把store操作從工作內(nèi)存中一個變量的值傳送到主內(nèi)存的變量中墨缘。

??如果要把一個變量從主內(nèi)存中復(fù)制到工作內(nèi)存镊讼,就需要按順尋地執(zhí)行read和load操作平夜,如果把變量從工作內(nèi)存中同步回主內(nèi)存中,就要按順序地執(zhí)行store和write操作玩裙。Java內(nèi)存模型只要求上述操作必須按順序執(zhí)行,而沒有保證必須是連續(xù)執(zhí)行寝受。也就是read和load之間罕偎,store和write之間是可以插入其他指令的颜及,如對主內(nèi)存中的變量a、b進(jìn)行訪問時讯蒲,可能的順序是read a肄扎,read b,load b旭等, load a衡载。Java內(nèi)存模型還規(guī)定了在執(zhí)行上述八種基本操作時痰娱,必須滿足如下規(guī)則:

·不允許read和load、store和write操作之一單獨出現(xiàn)
·不允許一個線程丟棄它的最近assign的操作鲸睛,即變量在工作內(nèi)存中改變了之后必須同步到主內(nèi)存中坡贺。
·不允許一個線程無原因地(沒有發(fā)生過任何assign操作)把數(shù)據(jù)從工作內(nèi)存同步回主內(nèi)存中拴念。
·一個新的變量只能在主內(nèi)存中誕生褐缠,不允許在工作內(nèi)存中直接使用一個未被初始化(load或assign)的變量。即就是對一個變量實施use和store操作之前公般,必須先執(zhí)行過了assign和load操作官帘。
·一個變量在同一時刻只允許一條線程對其進(jìn)行l(wèi)ock操作,lock和unlock必須成對出現(xiàn)
·如果對一個變量執(zhí)行l(wèi)ock操作酗捌,將會清空工作內(nèi)存中此變量的值涌哲,在執(zhí)行引擎使用這個變量前需要重新執(zhí)行l(wèi)oad或assign操作初始化變量的值
·如果一個變量事先沒有被lock操作鎖定阀圾,則不允許對它執(zhí)行unlock操作;也不允許去unlock一個被其他線程鎖定的變量涡真。
·對一個變量執(zhí)行unlock操作之前肾筐,必須先把此變量同步到主內(nèi)存中(執(zhí)行store和write操作)。

指令重排序

??在執(zhí)行程序時為了提高性能剧劝,編譯器和處理器經(jīng)常會對指令進(jìn)行重排序讥此。重排序分成三種類型:

·編譯器優(yōu)化的重排序谣妻。編譯器在不改變單線程程序語義放入前提下,可以重新安排語句的執(zhí)行順序他巨。
·指令級并行的重排序〖踅現(xiàn)代處理器采用了指令級并行技術(shù)來將多條指令重疊執(zhí)行辈灼。如果不存在數(shù)據(jù)依賴性,處理器可以改變語句對應(yīng)機(jī)器指令的執(zhí)行順序司志。
·內(nèi)存系統(tǒng)的重排序。由于處理器使用緩存和讀寫緩沖區(qū)囚霸,這使得加載和存儲操作看上去可能是在亂序執(zhí)行拓型。

同步關(guān)鍵字

Java提供了volatile和synchronized來保證同步

volatile

禁止指令重排序贸营,保證內(nèi)存可見性,但不保證原子性揣云。僅作用于變量邓夕。

synchronized

禁止指令重排序阎毅,保證內(nèi)存可見性,保證原子性矿咕。作用于方法狼钮,代碼塊熬芜。

原子性

由Java內(nèi)存模型來直接保證的原子性變量包括read,load,assign,use,store,write,我們大致可以認(rèn)為基本數(shù)據(jù)類型的訪問讀寫是具備原子性的(long,double除外)

可見性

可見性是指當(dāng)一個線程修改了共享變量的值瑞侮,其他線程能夠立即得知這個修改半火。volatile就是這個作用季俩。
除了vloatile外种玛,Java還有synchronized和final能保證可見性。同步塊的可見性是由"對一個變量執(zhí)行unlock操作之前娱节,必須先把次變量同步回主內(nèi)存中"這條規(guī)則獲得的祭示,而final的可見性是指:被final修飾的字段在構(gòu)造其中一旦初始化完成质涛,并且構(gòu)造器沒有把this的引用傳遞出去(this引用逃逸是一件很危險的事,其他線程有可能通過這個引用訪問到“初始化了一半”的對象)怒炸,那在其他線程中就能看見final的值毡代。

有序性

如果在本線程內(nèi)觀察教寂,所有的操作都是有序的;如果在一個線程中觀察另一個線程导梆,所有的操作都是無需的迂烁。前半句是指“線程內(nèi)表現(xiàn)為串行的語義”婚被,后半句是指“指令重排序”現(xiàn)象和“工作內(nèi)存與主內(nèi)存同步延遲”現(xiàn)象。

Java線程的實現(xiàn)

線程是比進(jìn)程更輕量級的調(diào)度執(zhí)行單位灾茁,線程的引入北专,可以把一個進(jìn)程的資源分配和執(zhí)行調(diào)度分開旬陡,各個線程即可以共享進(jìn)程資源(內(nèi)存地址描孟、文件I/O等)砰左,又可以獨立調(diào)度(線程是CPU調(diào)度的基本單位)场航。
實現(xiàn)線程主要有3中方式:使用內(nèi)核線程實現(xiàn)溉痢、使用用戶線程實現(xiàn)和使用用戶線程加輕量級進(jìn)程混合實現(xiàn)。

內(nèi)核線程

一對一線程模型 髓削,內(nèi)核線程(Kernel-Level Thread,KLT)是直接有操作系統(tǒng)內(nèi)核支持的線程立膛,這種線程由內(nèi)核來完成線程切換汽畴,內(nèi)核通過操作調(diào)度器(Scheduler)對線程進(jìn)行調(diào)度忍些,并負(fù)責(zé)將線程的任務(wù)映射帶各個處理器上。
程序一般不會直接去使用內(nèi)核線程廓握,而是去使用內(nèi)核線程的一種高級接口——輕量級進(jìn)程(Light Weight Process,LWP)嘁酿,輕量級進(jìn)程就是我們通常意義上所講的線程闹司,由于每個輕量級進(jìn)程都由一個內(nèi)核線程支持游桩,因此只有先支持內(nèi)核線程,才能有輕量級進(jìn)程盹憎。這種輕量級進(jìn)程與內(nèi)核線程間1:1的關(guān)系稱為一對一的線程模型铐刘,如下圖所示:

kernel_thread.png

優(yōu)點:每個輕量級進(jìn)程都成為一個獨立的調(diào)度單元,即使其中一個在系統(tǒng)調(diào)用中阻塞了挂签,也不會影響整個進(jìn)程工作竹握。
缺點:由于是基于內(nèi)核線程實現(xiàn)的辆飘,所以各種線程操作蜈项,如創(chuàng)建续挟,析構(gòu)和同步诗祸,都需要進(jìn)行系統(tǒng)調(diào)用。而系統(tǒng)調(diào)用的代價高昂博个,需要在用戶態(tài)(User Mode)和內(nèi)核態(tài)(Kernel Mode)來回切換盆佣。其次

用戶線程

一對N線程模型 械荷,用戶程序完全模擬線程的行為吨瞎,編碼時操作的線程是用戶程序模擬的線程,操作系統(tǒng)內(nèi)核不能感知你做了創(chuàng)建字旭、調(diào)度等等線程操作着绊。許多編程語言最初使用過這種方式归露,但現(xiàn)在基本放棄了這種。


userthread.png

用戶線程加輕量級進(jìn)程混合實現(xiàn)

N對M線程模型 往果,也有很多語言使用混合實現(xiàn)一铅。


hybrid.png

線程調(diào)度

線程調(diào)度是指系統(tǒng)為線程分配處理器使用權(quán)的過程潘飘,主要的調(diào)度方式有兩種:

協(xié)同式線程調(diào)度(Cooperative Threads-Scheduling)

這種方式是原始方式卜录,由一個線程執(zhí)行完通知另一個線程戈擒。已經(jīng)很少使用,很容易造成阻塞艰毒。

搶占式線程調(diào)度(Preemptive Threads-Scheduling)

主流方式筐高,由系統(tǒng)來根據(jù)一系列復(fù)雜的規(guī)則為每個線程分配執(zhí)行時間,線程的切換不是由線程自己做主(Java可以有Thread.yield()來讓出執(zhí)行時間丑瞧,但是沒有獲取執(zhí)行時間的方式)柑土。
Java語言一共設(shè)置了10個優(yōu)先級(Thread.MIN_PRIORITY到Thread.MAX_PRIORITY,1-10绊汹,正常值是5);在兩個線程同時處于ready狀態(tài)時稽屏,優(yōu)先級越高的線程越容易被烯烴選擇執(zhí)行。
但是不要依賴優(yōu)先級灸促,因為它并不靠譜,最終結(jié)果仍取決于OS浴栽。比如Java有10種優(yōu)先級荒叼,而linux優(yōu)先級從-20-19,那怎么對應(yīng)呢典鸡;比如windows有7中優(yōu)先級被廓。

狀態(tài)切換

Java語言定義了5中線程狀態(tài),在任意一個時間點萝玷,一個線程只能有且只有一種狀態(tài)嫁乘,這5種狀態(tài)如下:

·新建(New): 創(chuàng)建后,尚未啟動的線程
·運行(Runnable): 包括了操作系統(tǒng)狀態(tài)的Running和Ready球碉,即此時線程可能運行蜓斧,也可能CPU正在為它分配時間片,還沒運行睁冬。
·無限期等待(Waiting):處于這種狀態(tài)的線程不會被分配CPU執(zhí)行時間挎春,它們要等其他線程顯示的喚醒。調(diào)用如下方法會時當(dāng)前線程進(jìn)入Waiting狀態(tài):
        沒有設(shè)置timeout的java.lang.Object#wait();
        沒有設(shè)置timeout的threadObj.join()/threadObj.join(0)方法
        LockSupport.park()方法
·限期等待:同上,只是設(shè)置了timeout直奋,在時間到了后自動喚醒
        java.lang.Object#wait(long);
        java.lang.Thread#sleep(long);
        threadObj.join(n)方法
        LockSupport.parkNanos(obj ,n)方法
        LockSupport.parkUntil(obj ,n)方法
·阻塞(Blocked):線程被阻塞了能庆,阻塞與等待的區(qū)別是:阻塞狀態(tài)在等待一個排它鎖,這個時間在另外一個線程放棄這個所的時候發(fā)生脚线;而等待則是等待一段時間搁胆,或者被喚醒。
·結(jié)束(Terminated):已終止的線程狀態(tài)邮绿,線程已結(jié)束執(zhí)行渠旁。
state.png

參考:深入理解Java虛擬機(jī),作者:周志明船逮,侵刪一死。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市傻唾,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌承耿,老刑警劉巖冠骄,帶你破解...
    沈念sama閱讀 216,544評論 6 501
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異加袋,居然都是意外死亡凛辣,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,430評論 3 392
  • 文/潘曉璐 我一進(jìn)店門职烧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來扁誓,“玉大人,你說我怎么就攤上這事蚀之』雀遥” “怎么了?”我有些...
    開封第一講書人閱讀 162,764評論 0 353
  • 文/不壞的土叔 我叫張陵足删,是天一觀的道長寿谴。 經(jīng)常有香客問我,道長失受,這世上最難降的妖魔是什么讶泰? 我笑而不...
    開封第一講書人閱讀 58,193評論 1 292
  • 正文 為了忘掉前任,我火速辦了婚禮拂到,結(jié)果婚禮上痪署,老公的妹妹穿的比我還像新娘。我一直安慰自己兄旬,他們只是感情好狼犯,可當(dāng)我...
    茶點故事閱讀 67,216評論 6 388
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般辜王。 火紅的嫁衣襯著肌膚如雪劈狐。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,182評論 1 299
  • 那天呐馆,我揣著相機(jī)與錄音肥缔,去河邊找鬼。 笑死汹来,一個胖子當(dāng)著我的面吹牛续膳,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播收班,決...
    沈念sama閱讀 40,063評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼坟岔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了摔桦?” 一聲冷哼從身側(cè)響起社付,我...
    開封第一講書人閱讀 38,917評論 0 274
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎邻耕,沒想到半個月后鸥咖,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,329評論 1 310
  • 正文 獨居荒郊野嶺守林人離奇死亡兄世,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,543評論 2 332
  • 正文 我和宋清朗相戀三年啼辣,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片御滩。...
    茶點故事閱讀 39,722評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡鸥拧,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出削解,到底是詐尸還是另有隱情富弦,我是刑警寧澤,帶...
    沈念sama閱讀 35,425評論 5 343
  • 正文 年R本政府宣布氛驮,位于F島的核電站舆声,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏柳爽。R本人自食惡果不足惜媳握,卻給世界環(huán)境...
    茶點故事閱讀 41,019評論 3 326
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望磷脯。 院中可真熱鬧蛾找,春花似錦、人聲如沸赵誓。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,671評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至幻枉,卻和暖如春碰声,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背熬甫。 一陣腳步聲響...
    開封第一講書人閱讀 32,825評論 1 269
  • 我被黑心中介騙來泰國打工胰挑, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人椿肩。 一個月前我還...
    沈念sama閱讀 47,729評論 2 368
  • 正文 我出身青樓瞻颂,卻偏偏與公主長得像,于是被迫代替她去往敵國和親郑象。 傳聞我的和親對象是個殘疾皇子贡这,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,614評論 2 353

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