多線程(英語:multithreading),是指從軟件或者硬件上實(shí)現(xiàn)多個(gè)線程并發(fā)執(zhí)行的技術(shù)。具有多線程能力的計(jì)算機(jī)因有硬件支持而能夠在同一時(shí)間執(zhí)行多于一個(gè)線程,進(jìn)而提升整體處理性能。具有這種能力的系統(tǒng)包括對稱多處理機(jī)、多核心處理器以及芯片級多處理或同時(shí)多線程處理器。
粗粒度交替多線程
一個(gè)線程持續(xù)運(yùn)行,直到該線程被一個(gè)事件擋住而制造出長時(shí)間的延遲(可能是內(nèi)存load/store操作,或者程序分支操作)[4]。
舉例來說
周期 i :接收線程 A 的指令 j周期 i+1:接收線程 A 的指令 j+1周期 i+2:接收線程 A 的指令 j+2,而這指令緩存失敗周期 i+3:線程調(diào)度器介入,切換到線程 B周期 i+4:接收線程 B 的指令 k周期 i+5:接收線程 B 的指令 k+1[5]硬件成本
此種多線程硬件支持的目標(biāo),是允許在擋住的線程與已就緒的線程中快速切換。
這些新增功能的硬件有這些優(yōu)勢:
線程切換能夠在一個(gè) CPU 周期內(nèi)完成(實(shí)際上可以沒有開銷,上個(gè)周期在運(yùn)行線程A,下個(gè)周期就已在運(yùn)行線程B)。這樣子看起來像是每個(gè)線程是獨(dú)自運(yùn)行的,沒有其他線程與目前共享硬件資源。對操作系統(tǒng)來說,通常每個(gè)虛擬線程都被視做一個(gè)處理器。這樣就不需要很大的軟件變更(像是特別寫支持多線程的操作系統(tǒng))。為了要在各個(gè)現(xiàn)行中的線程有效率的切換,每個(gè)現(xiàn)行中的線程需要有自己的暫存設(shè)置(register set)。像是為了能在兩個(gè)線程中快速切換,硬件的寄存器需要兩次例示(instantiated)。
細(xì)粒度交替式多線程
執(zhí)行過程很像桶形處理器(Barrel Processor)就像這樣:
周期 i :接收線程 A 的一個(gè)指令周期 i+1:接收線程 B 的一個(gè)指令周期 i+2:接收線程 C 的一個(gè)指令這種線程的效果是會將所有從運(yùn)行流水線中的資料從屬(data dependency)關(guān)系移除掉。因?yàn)槊總€(gè)線程是相對獨(dú)立,流水線中的一個(gè)指令層次結(jié)構(gòu)需要從已跑完流水線中的較舊指令代入輸出的機(jī)會就相對的變小了。而在概念上,這種多線程與操作系統(tǒng)的核心先占多任務(wù)(pre-exemptive multitasking)相似。
1.基本概念
進(jìn)程:在操作系統(tǒng)中運(yùn)行的程序就是進(jìn)程,比如:QQ、播放器等。
線程:一個(gè)進(jìn)程可以有多個(gè)線程,如:視頻可以同時(shí)聽聲音、看圖像。
并行:多個(gè)cpu實(shí)例或者多臺機(jī)器同時(shí)執(zhí)行各自的處理邏輯,是真正的同時(shí)。
并發(fā):通過cpu調(diào)度算法,讓用戶看上去同時(shí)執(zhí)行,實(shí)際上從cpu操作層面不是真正的同時(shí)。
2.線程的生命周期
1)有新建(New)、就緒(Runnable)、運(yùn)行(Running)、阻塞(Blocked)、死亡(Dead)共5種狀態(tài)。
新建狀態(tài):new關(guān)鍵字新建一個(gè)線程后,該線程就處于新建狀態(tài),此時(shí)僅由JVM為其分配內(nèi)存,并初始化成員變量的值。
就緒狀態(tài):線程對象調(diào)用start()方法之后,該線程處于就緒狀態(tài)。Java虛擬機(jī)會為其創(chuàng)建方法調(diào)用棧和程序計(jì)數(shù)器,等待調(diào)度運(yùn)行。
運(yùn)行狀態(tài):處于就緒狀態(tài)的線程獲得了CPU,開始執(zhí)行run()方法的線程執(zhí)行體,此時(shí)該線程處于運(yùn)行狀態(tài)。
阻塞狀態(tài):處于運(yùn)行狀態(tài)的線程失去所占用資源后,便進(jìn)入阻塞狀態(tài)。
死亡狀態(tài):線程run()方法執(zhí)行結(jié)束后進(jìn)入死亡狀態(tài)。此外,如果線程執(zhí)行了interrupt()或stop()方法,也會以異常退出的方式進(jìn)入死亡狀態(tài)。
2)線程狀態(tài)的控制:
start():啟動當(dāng)前線程,自動調(diào)用當(dāng)前線程的run()方法。
run():通常需要重寫Thread類中的此方法,將創(chuàng)建的線程要執(zhí)行的操作方法聲明在此方法中。
yield():釋放當(dāng)前CPU的執(zhí)行權(quán)。
join():若線程a中調(diào)用線程b的join(),則線程a進(jìn)入阻塞狀態(tài),直到線程b執(zhí)行完成后線程a才結(jié)束阻塞狀態(tài)。
sleep(long militime):讓線程睡眠指定的毫秒數(shù),指定時(shí)間內(nèi),線程是阻塞狀態(tài),不會釋放鎖。該方法可以再任何場景下調(diào)用。
wait():執(zhí)行此方法,當(dāng)前線程會進(jìn)入阻塞狀態(tài),并釋放同步監(jiān)視器(鎖)。該方法必須在同步代碼塊和同步方法中才能調(diào)用。
notify():喚醒被wait的一個(gè)線程,多個(gè)線程wait時(shí),喚醒優(yōu)先度最高的。
notifyAll():喚醒所有被wait的線程。
LockSupport():LockSupport.park()和LockSupport.unpark()實(shí)現(xiàn)線程的阻塞和喚醒。
3.多線程的5種創(chuàng)建方式
① 繼承Thread類,重寫run()方法。
② 實(shí)現(xiàn)Runnable接口,重寫run()方法。
③ 匿名內(nèi)部類的方式,重寫run()方法。相當(dāng)于繼承了Thread類。new Thread(){ public void run(){邏輯功能} }.start();
④ Lambda表達(dá)式創(chuàng)建,相當(dāng)于實(shí)現(xiàn)Runnable接口的方法。new Thread(()->{邏輯功能}).start();
⑤ 線程池創(chuàng)建。Executor pool = Executors.newFixedThreadPool(); pool.excute(new Runnable(){public void run(){邏輯功能}});
線程池創(chuàng)建的一些參數(shù):
corePoolSize:隊(duì)列沒滿時(shí),線程最大并發(fā)數(shù)。
maximumPoolSizes:隊(duì)列滿后線程能夠達(dá)到的最大并發(fā)數(shù)。
keepAliveTime:空閑線程過多久被回收的時(shí)間限制。
unit:keepAliveTime的時(shí)間單位。
workQueue:阻塞的隊(duì)列類型。
RejectedExecutionHandler:超出maximumPoolSizes + workQueue時(shí),任務(wù)會交給RejectedExecutionHandler來處理。
4.線程的同步
為了防止多個(gè)線程訪問一個(gè)數(shù)據(jù)對象時(shí),對數(shù)據(jù)造成破壞,采用線程同步來保證多線程安全訪問競爭資源。
1)普通同步方法:synchronized關(guān)鍵字加在普通方法上,此時(shí)鎖就是當(dāng)前實(shí)例對象,進(jìn)入同步方法前要獲取當(dāng)前實(shí)例的鎖。
2)靜態(tài)同步方法:synchronized關(guān)鍵字加在靜態(tài)方法上,此時(shí)鎖就是當(dāng)前類的class對象,進(jìn)入同步方法前要獲取當(dāng)前類對象的鎖。
3)同步方法塊:synchronized關(guān)鍵字加在代碼塊前,小括號中指定鎖是什么,進(jìn)入同步代碼塊前就需要獲取指定的鎖。
synchronized底層實(shí)現(xiàn):
數(shù)據(jù)在JVM內(nèi)存的存儲:Java對象頭、moniter對象監(jiān)視器。
① 在JVM虛擬機(jī)中,對象在內(nèi)存中的存儲布局分為三個(gè)區(qū)域:對象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)、對齊填充(Padding)。Java對象頭包括:類型指針(Klass Pointer)和標(biāo)記字段(Mark Word)。類型指針是對象只想它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個(gè)指針來確定該對象是哪個(gè)類的實(shí)例。標(biāo)記字段用于存儲對象自身的運(yùn)行時(shí)數(shù)據(jù),比如哈希碼、鎖狀態(tài)標(biāo)志、線程持有的鎖等。所以,synchronized使用的鎖對象是存儲在Java對象頭里的標(biāo)記字段里。
② moniter:對象監(jiān)視器可以類比為一個(gè)特殊的房間,房間中有一些被保護(hù)的數(shù)據(jù),monitor保證每次只有一個(gè)線程能進(jìn)入房間,進(jìn)入即為持有monitor,退出即為釋放monitor。使用synchronized加鎖的同步代碼塊在字節(jié)碼引擎中執(zhí)行時(shí),主要就是通過鎖對象monitor的取用(monitorenter)與釋放(monitorexit)來實(shí)現(xiàn)的。
5.多線程引入問題
1)線程安全問題
① 原子性:常通過synchronized或者ReentrantLock來保證原子性。
② 可見性:指一個(gè)線程修改了某個(gè)變量值,其他線程能夠立即得到這個(gè)修改的值。每個(gè)線程都有自己的工作內(nèi)存,工作內(nèi)存和主存間要通過store和load進(jìn)行交互??梢娦詥栴}常使用volatile關(guān)鍵字解決。當(dāng)一個(gè)共享變量被volatile修飾時(shí),它會保證修改的值立即更新到主存,當(dāng)其他線程需要讀取時(shí),會去主存中讀取新值,而普通共享變量不能保證可見性,因?yàn)槠浔恍薷暮笏⑿禄刂鞔娴臅r(shí)間是不確定的。
2)線程死鎖
由于兩個(gè)或多個(gè)線程互相持有對方所需的資源,導(dǎo)致線程都處于等待狀態(tài),無法繼續(xù)執(zhí)行。
3)上下文切換
多線程有線程創(chuàng)建和線程上下文切換的開銷。CPU通常會給不同的線程分配時(shí)間片,當(dāng)CPU從一個(gè)線程切換到另外一個(gè)線程的時(shí)候,CPU需要保存當(dāng)前線程的本地?cái)?shù)據(jù),程序指針等狀態(tài),并加載下一個(gè)要執(zhí)行的線程的本地?cái)?shù)據(jù)、程序指針等,這個(gè)切換就稱為上下文切換。通常使用無鎖并發(fā)編程、CAS算法、協(xié)程等方式解決。
6.使用ReentrantLock
Java語言直接提供了synchronized關(guān)鍵字用于加鎖,但這種鎖存在兩個(gè)問題:①很重,②獲取時(shí)必須一直等待,沒有額外的嘗試機(jī)制。java.util.concurrent.locks包提供的ReentrantLock用于替代synchronized加鎖。調(diào)用ReentrantLock()對象的lock()方法獲取鎖,最后調(diào)用unlock()方法手動釋放鎖。ReentrantLock是可重入鎖,和synchronized一樣,一個(gè)線程可以多次獲取同一個(gè)鎖。和synchronized不同的是,ReentrantLock可以嘗試獲取鎖,即:調(diào)用ReentrantLock()對象的tryLock()方法,其中傳入等待時(shí)間,時(shí)間單位,如果在這個(gè)時(shí)間后仍然沒有獲取到鎖,tryLock()方法會返回false,程序可以去做其他事情。
synchronized可以配合wait()和notify()實(shí)現(xiàn)線程在條件不滿足時(shí)等待,條件滿足時(shí)喚醒,而ReentrantLock則需要借助Condition對象來實(shí)現(xiàn),注意Condition對象必須來自于ReentrantLock()對象調(diào)用newCondition()方法,這樣才能獲得一個(gè)綁定了ReentrantLock實(shí)例的Condition實(shí)例。Condition提供了await()、signal()、signalAll()方法,與wait()、notify()、notifyAll()是一致的。
① await():會釋放當(dāng)前鎖,進(jìn)入等待狀態(tài);
② signal():會喚醒某個(gè)等待線程;
③ signalAll():會喚醒所有等待線程;
此外,和tryLock()類似,await()可以在等待指定時(shí)間后,如果還沒有被其他線程通過signal()或signalAll()喚醒,可以自己醒來:
if ( condition.await(1, TimeUnit.SECOND) ) {
// 被其他線程喚醒
} else {
// 指定時(shí)間內(nèi)沒有被其他線程喚醒
}
7.使用ReadWriteLock
ReentrantLock保證了只有一個(gè)線程可以執(zhí)行臨界區(qū)代碼,但是有些時(shí)候,這種保護(hù)有點(diǎn)過頭。有些方法只是讀取數(shù)據(jù),并不修改數(shù)據(jù),此時(shí)應(yīng)該允許多個(gè)線程同時(shí)調(diào)用才對。使用ReadWriteLock可以解決這個(gè)問題,它可以保證:
① 只允許一個(gè)線程寫入(此時(shí)其他線程既不能寫入也不能讀取);
② 沒有寫入時(shí),多個(gè)線程允許同時(shí)讀(提高性能);
使用方法:new出來ReadWriteLock()對象實(shí)例后,調(diào)用該對象的readLock()和writeLock()分別獲取讀鎖和寫鎖實(shí)例,接著在讀方法中使用讀鎖,寫方法中使用寫鎖。
private final ReadWriteLock rwlock = new ReentrantReadWriteLock();
private final Lock rlock = rwlock.readLock();
private final Lock wlock = rwlock.writeLock();
8.使用StampedLock
ReadWriteLock讀的過程中不允許寫,是一種悲觀的讀鎖。其實(shí)讀的過程中大概率不會有寫操作的發(fā)生,所以并發(fā)效率有待提高。StampedLock和ReadWriteLock相比,讀的過程中也允許獲取寫鎖后寫入。這樣一來,我們讀的數(shù)據(jù)就可能不一致,所以,需要一點(diǎn)額外的代碼來判斷讀的過程中是否有寫入,這種讀鎖是一種樂觀鎖。StampedLock是不可重入鎖,不能在一個(gè)線程中反復(fù)獲取同一個(gè)鎖。其提供了將悲觀讀鎖升級為寫鎖的功能,它主要使用在if-then-update的場景:即先讀,如果讀的數(shù)據(jù)滿足條件,就返回,如果讀的數(shù)據(jù)不滿足條件,再嘗試寫。
使用方法:new出來StampedLock()對象實(shí)例后,調(diào)用該對象的readLock()和writeLock()可以分別獲取讀鎖和寫鎖實(shí)例并上鎖,同時(shí)還會返回版本號,釋放的時(shí)候調(diào)用unclockWrite()或者unlockRead()方法需要傳入版本號。調(diào)用tryOptimisticRead()獲得樂觀讀鎖,同時(shí)返回版本號,它在操作數(shù)據(jù)前并沒有通過 CAS設(shè)置鎖的狀態(tài),僅僅通過位運(yùn)算測試,所以不需要顯式地釋放鎖。通常獲取樂觀鎖,讀入數(shù)據(jù)后,會調(diào)用validate()方法傳入版本號stamp進(jìn)行驗(yàn)證,如果中途有寫入,則版本號會發(fā)生變化,方法會返回false,此時(shí)需要通過獲得悲觀鎖再重新讀入數(shù)據(jù)。
9.使用Semaphore
上面的鎖保證同一時(shí)刻只有一個(gè)線程能訪問(ReentrantLock),或者只有一個(gè)線程能寫入(ReadWriteLock)。還有一種受限資源,需要保證同一時(shí)刻最多有N個(gè)線程能訪問,比如同一時(shí)刻最多創(chuàng)建100個(gè)數(shù)據(jù)庫連接,最多允許10個(gè)用戶下載等。這種限制數(shù)量的鎖,用Lock數(shù)組來實(shí)現(xiàn)很麻煩,此時(shí)就可以使用可以使用Semaphore。其本質(zhì)上就是一個(gè)信號計(jì)數(shù)器,用于限制同一時(shí)間的最大訪問數(shù)量。
使用方法:new出來Semaphore()對象實(shí)例,其中傳入允許訪問的線程數(shù)量,在需要控制的方法中,調(diào)用acquire()方法,接著完成功能邏輯,最后調(diào)用release()方法釋放。調(diào)用acquire()可能會進(jìn)入等待,直到滿足條件為止。也可以使用tryAcquire()指定等待時(shí)間。
10.使用Future
Runnable接口有個(gè)問題,它的方法沒有返回值。如果任務(wù)需要一個(gè)返回結(jié)果,那么只能保存到變量,還要提供額外的方法讀取,非常不方便。Callable接口和Runnable接口比,多了一個(gè)返回值功能,并且Callable接口是一個(gè)泛型接口,可以返回指定結(jié)果的類型。線程池對象的submit()方法提交任務(wù)執(zhí)行后會返回一個(gè)Future對象,也支持泛型,其表示一個(gè)未來能獲得結(jié)果的對象。當(dāng)我們提交一個(gè)Callable任務(wù)后,我們會同時(shí)獲得一個(gè)Future對象,然后,我們在主線程某個(gè)時(shí)刻調(diào)用Future對象的get()方法,就可以獲得異步執(zhí)行的結(jié)果。在調(diào)用get()時(shí),如果異步任務(wù)已經(jīng)完成,我們就直接獲得結(jié)果。如果異步任務(wù)還沒有完成,那么get()會阻塞,直到任務(wù)完成后才返回結(jié)果。
Future接口定義的方法有:
get():獲取結(jié)果(可能會等待);
get(long timeout, TimeUnit unit):獲取結(jié)果,但只等待指定的時(shí)間;
cancel(boolean mayInterruptIfRunning):取消當(dāng)前任務(wù);
isDone():判斷任務(wù)是否已完成。
11.使用CompletableFuture
使用Future獲得異步執(zhí)行結(jié)果時(shí),要么調(diào)用阻塞方法get(),要么輪詢看isDone()是否為true,這兩種方法都不是很好,因?yàn)橹骶€程也會被迫等待。CompletableFuture針對Future做了改進(jìn),可以傳入回調(diào)對象,當(dāng)異步任務(wù)完成或者發(fā)生異常時(shí),自動調(diào)用回調(diào)對象的回調(diào)方法。
使用方式:調(diào)用CompletableFuture的supplyAsync()創(chuàng)建CompletableFuture對象,其中傳入實(shí)現(xiàn)了Supplier接口的對象(無傳入值,有返回值),他會被提交給默認(rèn)的線程執(zhí)行。調(diào)用thenAccept()方法,其接收實(shí)現(xiàn)了Consumer接口的對象(有傳入值,無返回值),設(shè)置執(zhí)行完成時(shí)的回調(diào)方法。調(diào)用exceptionally()方法,接收實(shí)現(xiàn)了Function接口的對象,設(shè)置報(bào)異常時(shí)的回調(diào)方法。
12.使用ForkJoin
Java 7開始引入了一種新的Fork/Join線程池,它可以執(zhí)行一種特殊的任務(wù):把一個(gè)大任務(wù)拆成多個(gè)小任務(wù)并行執(zhí)行。
比如:計(jì)算一個(gè)超大數(shù)組的和,可以把數(shù)組拆成兩部分,分別計(jì)算,最后加起來就是最終結(jié)果,這樣可以用兩個(gè)線程并行執(zhí)行,如果拆成兩部分還是很大,我們還可以繼續(xù)拆,用4個(gè)線程并行執(zhí)行。這就是Fork/Join任務(wù)的原理:判斷一個(gè)任務(wù)是否足夠小,如果是,直接計(jì)算,否則,就分拆成幾個(gè)小任務(wù)分別計(jì)算。這個(gè)過程可以反復(fù)“裂變”成一系列小任務(wù)。
使用方法:新建類繼承RecursiveTask,其中重寫compute方法,設(shè)定閾值,如果任務(wù)小于設(shè)定的閾值,就直接計(jì)算,最后返回結(jié)果。如果任務(wù)大于設(shè)定的閾值,就分裂成兩個(gè)小任務(wù),調(diào)用invokeAll()傳入兩個(gè)小任務(wù),再分別調(diào)用兩個(gè)小任務(wù)的join()方法來得到返回結(jié)果,最后將兩個(gè)結(jié)果加起來返回。
13. 使用ThreadLocal
上下文(Context):在一個(gè)線程中,橫跨若干方法調(diào)用,需要傳遞的對象,通常稱之為上下文(Context),它是一種狀態(tài),可以是用戶身份、任務(wù)信息等。
Web應(yīng)用程序是典型的多任務(wù)應(yīng)用,每個(gè)用戶請求頁面時(shí),我們都會創(chuàng)建一個(gè)任務(wù),去完成類似以下的工作:檢查權(quán)限、做工作、保存狀態(tài)、發(fā)送響應(yīng)。如果這些工作中也需要用到上下文(context),此處就是user實(shí)例,可以簡單地直接通過參數(shù)傳入,但是往往一個(gè)方法又會調(diào)用其他很多方法,這樣會導(dǎo)致User傳遞到所有地方,但是給每個(gè)方法都增加一個(gè)Context參數(shù)非常麻煩,而且如果調(diào)用鏈中有無法修改源碼的第三方庫,context就傳不進(jìn)去了。ThreadLocal就很適合解決這個(gè)問題,它可以在一個(gè)線程中傳遞同一個(gè)對象。
public void process(User user) {
checkPermission();
doWork();
saveStatus();
sendResponse();
}
使用方法:ThreadLocal實(shí)例通??偸且造o態(tài)字段初始化,通過設(shè)置一個(gè)User實(shí)例關(guān)聯(lián)到ThreadLocal中,在移除之前,所有方法都可以隨時(shí)獲取到該User實(shí)例。普通的方法調(diào)用一定是同一個(gè)線程執(zhí)行的,所以,該線程中所有方法調(diào)用threadLocalUser.get()獲取的User對象是同一個(gè)實(shí)例。ThreadLocal相當(dāng)于給每個(gè)線程都開辟了一個(gè)獨(dú)立的存儲空間,各個(gè)線程的ThreadLocal關(guān)聯(lián)的實(shí)例互不干擾,特別注意ThreadLocal一定要在finally中清除。因?yàn)楫?dāng)前線程執(zhí)行完相關(guān)代碼后,很可能會被重新放入線程池中,如果ThreadLocal沒有被清除,該線程執(zhí)行其他代碼時(shí),會把上一次的狀態(tài)帶進(jìn)去。其實(shí),可以ThreadLocal看成一個(gè)全局Map:每個(gè)線程獲取ThreadLocal變量時(shí),總是使用Thread自身作為key。