前言
??在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)化袒啼。
現(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)系如下圖所示:
內(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)系稱為一對一的線程模型铐刘,如下圖所示:
優(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)在基本放棄了這種。
用戶線程加輕量級進(jìn)程混合實現(xiàn)
N對M線程模型 往果,也有很多語言使用混合實現(xiàn)一铅。
線程調(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í)行渠旁。
參考:深入理解Java虛擬機(jī),作者:周志明船逮,侵刪一死。