文章主要參考Java多線程編程指南(核心篇)
線程的基礎知識
我們想要創建一個線程,要不就是實現Runable接口,實現run方法,也可以繼承Thread類,覆蓋run方法,Thread實例是特殊的Runable實現類,所以在創建它的時候Java虛擬機會為其分配調用棧空間,內核線程等資源,成本要相對昂貴一點,我們在使用的時候,如果是要傳遞給其他API使用,直接使用Runable接口實現就行。
需要注意的是:啟動線程的時候,調用Runable實現類的start方法,啟動線程的本質是請求Java虛擬機運行相應的線程,這個線程具體何時能夠運行時由線程調度器(操作系統的一部分)決定
/**
*Thread類:是Runable接口的實現類,有一個含參
*的構造器Thread(Runable target);其對Runable的run方法的實現如下
**/
@Override
public void run(){
if(target !=null){
target.run();
}
}
Thread類的常用方法
static Thread currentThread() :返回當前線程,即當前代碼的執行線程實例
void join() 等待相應線程運行結束,線程A調用線程B的join方法,A線程的運行會暫停,直到線程B運行結束
static void yield:使當前線程主動放棄對處理器的占用,方法不可靠,被調用時當前線程可能仍然能繼續運行
static void sleep(long millis);使當前線程休眠指定的時間
線程的生命周期狀態
New,Runable(Ready(被線程調度器選中)-->Running),Blocked(發起阻塞式IO,申請由其他線程持有的獨占資源),Waiting,Timed_waiting,Terminated;具體的在后面在說
競態
計算結果的正確性與時間有關的現象稱為競態(Race Condition)
競態的兩種模式:read-modify-write:最常見的例子就是count++操作,
load(count,r1);//將變量讀取到r1寄存器
increment(r1);//修改寄存器的值
store(count,r1);//將寄存器的值寫到count對應的內存空間里
check-then-act:讀取某個共享變量的值,根據該變量的值決定下一步的動作是什么,最常見的例子就是if判斷共享變量后進行操作
線程安全
原子性
原子操作:原子操作是針對 共享變量的操作而言的,原子操作是從改 操作的執行以外的線程來描述的。
“不可分割”含義:
- 訪問某個共享變量的操作從其執行線程以外的任何線程來看,該操作要么已經執行結束要么尚未發生。
- 訪問同一組共享變量的原子操作是不能夠被交錯的
實現原子性的方式:CAS(compare-and-swap),鎖
java對long和double型以外的任何類型的變量的寫操作都是原子操作,對任何變量的讀操作都是原子操作。
可見性
JIT編譯器優化,導致共享變量更新不可見;
-
與計算機的存儲系統有關。程序中變量可能會分配到寄存器(Register)而不是主內存中進行存儲。每個處理器都有其存儲器,一個處理器無法讀取另外一個處理器上的寄存器的內容。但是一個處理器可以通過緩存一致性協議(Cache Coherence Protocol),讀取其他處理器的高速緩存中的數據,更新到自己的高速緩存中。稱為
緩存同步
(高速緩存,主內存)。為了保證共享變量更新寫入該處理器的高速緩存或者內存中(而不是在寫緩沖器中),這個過程稱為沖刷處理器
。同時,如果其他處理器在此之前更新了共享變量,那么該處理器必須從其他處理器的高速緩存或者主內存中對相應的變量進行緩存同步。這個過程稱為刷新處理器緩存
。可見性保證是通過更新共享變量的處理器沖刷處理器緩存,并使讀取共享變量的處理器執行刷新處理器緩存的動作來實現
java平臺中只需要使用
volatile
關鍵字聲明就可以保障可見性。JLS保證:父線程在啟動子線程之前對共享變量的更新對子線程可見; 線程終止后該線程對共享變量的更新對于調用該線程的join方法的線程而言是可見的
有序性
重排序
重排序類型 | 重排序表現 | 重排序來源 |
---|---|---|
指令重排序 | 程序順序與源代碼順序不一致 | 編譯器 |
指令重排序 | 執行順序與程序順序不一致 | JIT編譯器、處理器 |
存儲子系統重排序 | 源代碼順序、程序順序和執行順序這三者保持一致,但是感知順序與執行順序不一致 | 高速緩存、寫緩沖器 |
指令重排序
JIT編譯器(字節碼動態生成機器碼)可能會執行指令重排序
處理器也可能會執行指令重排序,使得執行順序與程序順序不一致。處理器的亂序(Out-of-order Execution)。哪條指令就緒就執行哪條指令。指令結果被寫入重排緩沖器(ROB),ROB會進行順序提交,所以不會對單線程程序正確性產生影響。
處理器的亂序執行還采用了猜測執行(Speculation)技術。猜測執行可以導致先執行if語句的語句體。將結果臨時存放到ROB中,再來判斷if的值,如果是true就提交到高速緩存,主內存中,否則就在ROB中丟棄。
存儲子系統重排序
從處理器的角度來說,就只有讀內存操作(LOAD)(從RAM地址加載數據)和寫內存操作(STORE)
所以重排序只有四種(LoadLoad,StoreStore,LoadStore,StoreLoad),這些重排序是指在一個處理器上先后執行操作,而其他處理器對這兩個內存操作的感知順序不同。
貌似串行語義
存在數據依賴關系(兩個操作訪問同一個變量,且其中一個操作為寫操作,包括讀后寫,寫后讀,寫后寫)的語句不會被重排序,而存在控制依賴關系的語句允許被重排序(典型例子:if)
保證內存訪問的順序性
從邏輯上部分禁止重排序,從底層角度來說,禁止重排序是通過調用處理器提供相關指令(內存屏障)來實現的。
volatile,synchronized可以保證有序性。
可見性和有序性的聯系
可見性是有序性的基礎,有序性影響可見性
上下文切換
進程中的一個線程由于時間片用完或者自身原因被迫或者主動暫停其運行時,另外一個線程可以被操作系統選中占用處理器開始或者繼續其運行,這個過程稱為線程切入與切出。在切入切出時操作系統需要保存和恢復相應線程的進度信息。這個進度信息稱為上下文。一般包括通用寄存器的內容和程序計數器的內容
Thread.sleep(long millis)
Object.wait(long timeout)
Thread.yield();(不穩定)
Thread.join()
LockSupport.park()
I/O操作(讀取文件)
等待其他線程持有的鎖
一次上下文切換的時間消耗是微妙級別的
資源調度
資源調度的一個常見特性就是它能否保證公平性:如果資源的任何一個先申請者總是能夠比任何一個后申請者先獲得該資源的獨占權,那么相應的資源調度策略就稱為是公平的,否則稱為不公平。常見策略是維護一個等待隊列。
比較:
非公平調度吞吐量大:資源的持有線程釋放資源的時候,會有等待隊列中的一個線程被喚醒,而該線程從被喚醒到繼續運行需要一定時間,在這段時間內,新來的線程可以先被授予該資源的獨占權。如果新來的線程占用時間不長,它有可能在被喚醒的線程繼續運行之前釋放資源,不影響被喚醒的線程申請資源。這樣就可以減少上下文切換次數。但是如果多數線程占用資源時間相當長,那么就會使剛喚醒的線程由于被搶占又進入暫停。耗費資源。
所以,在線程占有時間較長或者申請資源頻率不高的情況下,可以使用公平調度,可以避免饑餓
但是默認一般采用非公平,提高吞吐率