并發處理的廣泛應用是使得Amdahl定律代替摩爾定律成為計算機性能發展源動力的根本原因,也是人類“壓榨”計算機運算能力的最有力武器
硬件的效率與一致性
物理機中為了提升效率而采取的方案:
- 在處理器與內存之間加入一層或多層讀寫速度盡可能接近處理器運算速度的高速緩存。高速緩存在解決處理器與內存的速度矛盾的同時,也帶來了緩存一致性(Cache Coherence)的問題。對此,需要各個處理器在訪問緩存時遵循一些協議
- 處理器可能會對輸入代碼進行亂序執行(Out-Of-Order Execution)優化。處理器會在計算之后將亂序執行的結果重組,保證該結果與順序執行的結果是一致的,但并不保證程序中各個語句計算的先后順序與輸入代碼中的順序一致。與處理器的亂序執行優化類似,java虛擬機的即時編譯器中也有類似的指令重排序(Instruction Reorder)優化
java內存模型
java虛擬機規范中試圖定義一種java內存模型來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓java程序在各種平臺下都能達到一致的內存訪問效果
主內存與工作內存
java內存模型的主要目的是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節
java內存模型規定了所有的變量都存儲在主內存(Main Memory)中。每條線程還有自己的工作內存(Working Memory),線程的工作內存中保存了被該線程使用到的變量的主內存副本拷貝,線程堆變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存中的變量
這里說的主內存、工作內存與java內存區域中的java堆、棧、方法區等不是同一個層次的內存劃分。主內存、工作內存是對硬件的抽象模型;java堆、棧、方法區是對內存區域的細部劃分
內存間交互操作
java內存模型中定義以下8種操作都是原子的、不可再分的(double和long有些例外)
- lock(鎖定):作用于主內存的變量,把一個變量標識為一條線程獨占的狀態
- unlock(解鎖):作用于主內存變量,把一個處于鎖定狀態的變量釋放出來,釋放后的變量才可以被其他線程鎖定
- read(讀取):作用于主內存變量,把一個變量的值從主內存傳輸到線程的工作內存中,以便隨后的load動作使用
- load(載入):作用于工作內存的變量,把一個read操作從主內存中得到的變量值放入工作內存的變量副本中
- use(使用):作用于工作內存的變量,把工作內存中一個變量的值傳遞給執行引起,每當虛擬機遇到一個需要使用到變量的值的字節碼指令時將會執行這個操作
- assign(賦值):作用于工作內存的變量,把一個從執行引擎接收到的值賦給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作
- store(存儲):作用于工作內存的變量,把工作內存中一個變量的值傳送到主內存中,以便隨后的write操作使用
- write(寫入):作用于主內存的變量,把store操作從工作內存中得到的變量的值放入主內存的變量中
執行8種基本操作時必須滿足的規則:
- 不允許read和load、store和write操作之一單獨出現 ==> 不允許一個變量從主內存讀取了但工作內存不接受,或者從工作內存發起回寫了但主內存不接受的情況出現
- 不允許一個線程丟棄它的最近的assign操作 ==> 變量在工作內存中改變之后必須把該變化同步回主內存
- 不允許一個線程無原因地(沒有發生過任何assign操作)把數據從線程的工作內存同步回主內存中
- 對一個變量實施use、store操作之前,必須先執行過了assign和load操作 ==> 一個新的變量只能在主內存中“誕生”,不允許在工作內存中直接使用一個未被初始化的變量
- 一個變量在同一個時刻只允許一條線程對其進行lock操作,但lock操作可以被同一條線程重復執行多次,多次執行lock后,只有執行相同次數的unlock操作,變量才會被解鎖
- 如果一個變量事先沒有被lock操作鎖定,那就不允許對它執行unlock操作,也不允許去unlock一個被其他線程鎖定住的變量
- 對一個變量執行unlock操作之前,必須先把此變量同步回主內存中
對于volatile型變量的特殊規則
volatile關鍵字是java虛擬機提供的最輕量級的同步機制。volatile只能保證對所有線程的可見性,即當一條線程修改了這個變量的值,新值對于其他線程來說時可以立即得知的。但在并發下一樣是不安全的,當兩個線程先后對一個變量執行use并先后執行store時,后面線程的結果會覆蓋前面線程的結果,這是由于java里面的運算并非原子操作
由于volatile變量只能保證可見性,因此只有在符合以下兩條規則的運算場景中才能保證并發安全
- 運算結果并不依賴變量的當前值,或者能夠確保只有單一的線程修改變量的值
- 變量不需要與其他的狀態變量共同參與不變約束
volatile變量能禁止指令重排優化,普通的變量僅僅會保證在該方法的執行過程中所有依賴賦值結果的地方都能獲取到正確的結果,而不能保證變量賦值操作的順序與程序代碼中的執行順序一致
volatile變量的操作規則:
- 在工作內存中,每次使用volatile變量前都必須先從主內存刷新最新的值,用于保證能看見其他線程對volatile變量修改后的值
- 在工作內存中,每次修改volatile變量后都必須立刻同步回主內存中,用于保證其他線程可以看到自己對volatile變量的修改
- volatile變量不會被指令重排序優化,保證代碼的執行順序與程序的程序相同
對于long和double型變量的特殊規則
java內存模型要求lock、unlock、read、load、assign、use、store、write這8個操作都具有原子性,但是對于64位的數據類型(long和double),虛擬機允許將沒有被volatile修飾的64位數據的讀寫操作劃分位兩次32位的操作來執行,這就是所謂的long和double的非原子性協定(Nonatomic Treatment of double and long Variables)
先行發生原則
先行發生是java內存模型中定義的兩項操作之間的偏序關系,如果說操作A先行發生于操作B,操作A產生的影響能被操作B觀察到,“影響”包括修改了內存中共享變量的值、發送了消息、調用了方法等
java內存模型中有一些“天然的”先行發生關系,這些先行發生關系無需任何同步器協助就已經存在,可以在編碼中直接使用
- 程序次序規則(Program Order Rule):在一個線程內,按照程序代碼順序,書寫在前面的操作先行發生于書寫在后面的操作
- 管程鎖定規則(Monitor Lock Rule):一個unlock操作先行發生于后面對同一個鎖的lock操作
- volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操作先行發生與后面對這個變量的讀操作
- 線程啟動規則(Thread Start Rule):Thread對象的start()方法先行發生于此線程的每一個動作
- 線程終止規則(Thread Termination Rule):線程中的所有操作都先行發生于對此線程的終止檢測
- 線程中斷規則(Thread Interruption Rule):對線程interrut()方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生
- 對象終結規則(Finalizer Rule):一個對象的初始化完成先行發生于它的finalize()方法的開始
- 傳遞性(Transitivity):如果操作A先行發生于操作B,操作B先行發生于操作C,那就可以得出操作A先行發生于操作C的結論
java與線程
線程的實現
線程是比進程更輕量級的調度執行單位,線程的引入,可以把一個進程的資源分配和執行調度分開,各個線程既可以共享進程資源,又可以獨立調度。實現線程主要有3種方式:
1.使用內核線程實現
內核線程(Kernel-level Thread, KLT)是由操作系統內核直接支持的線程,由內核來完成線程切換,內核通過操縱調度器(Scheduler)對線程進行調度,并負責將線程的任務映射到各個處理器上
程序一般不會直接去使用內核線程,而是去使用內核線程的一種高級接口——輕量級進程(Light Weight Process, LWP),輕量級進程就是通常意義上所講的線程
輕量級進程的局限性:
- 基于內核線程實現,其創建、析構、同步等操作代價相對較高
- 每個輕量級進程都需要一個內核線程的支持,因此一個系統支持輕量級進程的數量是有限的
2.使用用戶線程實現
用戶線程(User Thread, UT)是指完全建立在用戶空間的線程庫上,系統內核不能感知線程存在的實現。用戶線程的建立、同步、銷毀和調度完全在用戶態中完成,不需要內核的幫助
由于用戶線程沒有系統內核的支援,因此所有的線程操作都需要用戶程序自己處理。線程的創建、切換和調度都是需要考慮的為問題,而且由于操作系統只把處理器支援分配到進程,那諸如“阻塞如何處理”、“多處理器系統中如何將線程映射到其他處理器”這類問題解決起來將會異常困難,甚至不可能完成
3.使用用戶線程加輕量級進行混合實現
線程除了依賴內核線程實現和完全由用戶線程自己實現之外,還可以將內核線程與用戶線程一起使用。在這種混合實現下,用戶線程負責創建、切換、析構等操作,輕量級線程負責真實執行
狀態轉換
java語言中定義了5種線程狀態,在任意一個時間點,一個線程只能有且只有其中的一種狀態,這5種狀態分別如下:
- 新建(New):創建后尚未啟動的線程處于這個狀態
- 運行(Runable):Runable包括了操作系統線程狀態種的Running和Ready,也就是處于此狀態的線程有可能正在執行,也有可能正在等待CPU為它分配執行時間
-
無限期等待(Waiting):處于這種狀態的線程不會被分配CPU執行時間,它們要等待被其他線程顯式地喚醒
- 沒有設置Timeout參數的Object.wait()方法
- 沒有設置Timeout參數的Thread.join()方法
- LockSupport.park()方法
- 限期等待(Timed Waiting):處于這種狀態的線程也不會被分配CPU執行時間,不過無需等待被其他線程顯式地喚醒,在一定時間之后它們會有系統自動喚醒
- Thread.sleep()方法
- 設置了Timeout參數的Object.wait()方法
- 設置了Timeout參數的Thread.join()方法
- LockSupport.parkNanos()方法
- LockSupport.parkUntil()方法
- 阻塞(Blocked):線程被阻塞,等待獲取一個排他鎖
- 結束(Terminated):線程已經結束執行