第十二章 Java內存模型與線程html
一、硬件效率與一致性java
- 因爲計算機的存儲設備與處理器的運算速度有幾個數量級的差距,因此現代計算機系統都不得不加入一層讀寫速度儘量接近處理器運算速度的高速緩存(Cache)來做爲內存與處理器之間的緩衝。
- 每一個處理器都有本身的高速緩存,而它們又共享同一主內存(Main Memory),當多個處理器的運算任務都涉及同一塊主內存區域時,將可能致使各自的緩存數據不一致,爲了解決一致性的問題,須要各個處理器訪問緩存時都遵循一些協議,在讀寫時要根據協議來進行操做,這類協議有MSI、MESI(Illinois Protocol)、MOSI、Synapse、Firefly及Dragon Protocol等。
二、Java內存模型數據庫
主內存和工做內存:編程
- Java內存模型主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。此處的變量(Variable)與Java編程中的變量略有區別,它包括實例變量/靜態字段和構成數組對象的元素,不包括局部變量和方法參數(線程私有)。Java內存模型和Java內存域沒有聯繫。
- Java內存模型規定全部變量都存儲在主存(Main Memory)中(虛擬機內存的一部分)。每條線程還有本身的工做內存(Working Memory),線程的工做內存保存了被線程使用到的變量的主內存副本拷貝,線程對變量的全部操做(讀取/賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量。不一樣線程之間也沒法直接訪問對方工做內存中的變量,線程間變量值的傳遞均須要經過主存來完成。
內存間交互操做:數組
- 關於主內存與工做內存之間具體的交互協議,即一個變量如何從主內存拷貝到工做內存、如何從工做內存同步回主內存之類的實現細節,Java內存模型中定義瞭如下8種操做來完成。
- lock(鎖定):做用於主內存的變量,它把一個變量標識爲一條線程獨佔的狀態。
- unlock(解鎖):做用於主內存的變量,它把一個處於鎖定狀態的變量釋放出來,釋放後的變量才能夠被其餘線程鎖定。
- read(讀取):做用於主內存的變量,它把一個變量的值從主內存傳輸到線程的工做內存中,以便隨後的load動做使用。
- load(載入):做用於工做內存的變量,它把read操做從主內存中獲得的變量值放入工做內存的變量副本中。
- use(使用):做用於工做內存的變量,它把工做內存中一個變量的值傳遞給執行引擎,每當虛擬機遇到一個須要使用到變量的值的字節碼指令時將會執行這個操做。
- assign(賦值):做用於工做內存的變量,它把一個從執行引擎接收到的值賦給工做內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操做。
- store(存儲):做用於工做內存的變量,它把工做內存中一個變量的值傳送到主內存中,以便隨後的write操做使用。
- write(寫入):做用於主內存的變量,它把store操做從工做內存中獲得的變量的值放入主內存的變量中。
- 若是要把一個變量從主內存複製到工做內存,那就要順序地執行read和load操做,若是要把變量從工做內存同步回主內存,就要順序地執行store和write操做。注意,Java內存模型只要求上述兩個操做必須按順序執行,而沒有保證是連續執行。也就是說,read與load之間、store與write之間是可插入其餘指令的,如對主內存中的變量a、b進行訪問時,一種可能出現順序是read a、read b、load b、load a。除此以外,Java內存模型還規定了在執行上述8種基本操做時必須知足以下規則:
- 不容許read和load、store和write操做之一單獨出現,即不容許一個變量從主內存讀取了但工做內存不接受,或者從工做內存發起回寫了但主內存不接受的狀況出現。
- 不容許一個線程丟棄它的最近的assign操做,即變量在工做內存中改變了以後必須把該變化同步回主內存。
- 不容許一個線程無緣由地(沒有發生過任何assign操做)把數據從線程的工做內存同步回主內存中。
- 一個新的變量只能在主內存中「誕生」,不容許在工做內存中直接使用一個未被初始化(load或assign)的變量,換句話說,就是對一個變量實施use、store操做以前,必須先執行過了assign和load操做。
- 一個變量在同一個時刻只容許一條線程對其進行lock操做,但lock操做能夠被同一條線程重複執行屢次,屢次執行lock後,只有執行相同次數的unlock操做,變量纔會被解鎖。
- 若是對一個變量執行lock操做,那將會清空工做內存中此變量的值,在執行引擎使用這個變量前,須要從新執行load或assign操做初始化變量的值。
- 若是一個變量事先沒有被lock操做鎖定,那就不容許對它執行unlock操做,也不容許去unlock一個被其餘線程鎖定住的變量。
- 對一個變量執行unlock操做以前,必須先把此變量同步回主內存中(執行store、write操做)。
對於volatile型變量的特殊規則:緩存
- 只有當線程T對變量V執行的前一個動做是load的時候,線程T才能對變量V執行use動做;而且,只有當線程T對變量V執行的後一個動做是use的時候,線程T才能對變量V執行load動做。線程T對變量V的use動做能夠認爲是和線程T對變量V的load、read動做相關聯,必須連續一塊兒出現(這條規則要求在工做內存中,每次使用V前都必須先從主內存刷新最新的值,用於保證能看見其餘線程對變量V所作的修改後的值)。
- 只有當線程T對變量V執行的前一個動做是assign的時候,線程T才能對變量V執行store動做;而且,只有當線程T對變量V執行的後一個動做是store的時候,線程T才能對變量V執行assign動做。線程T對變量V的assign動做能夠認爲是和線程T對變量V的store、write動做相關聯,必須連續一塊兒出現(這條規則要求在工做內存中,每次修改V後都必須馬上同步回主內存中,用於保證其餘線程能夠看到本身對變量V所作的修改)。
- 假定動做A是線程T對變量V實施的use或assign動做,假定動做F是和動做A相關聯的load或store動做,假定動做P是和動做F相應的對變量V的read或write動做;相似的,假定動做B是線程T對變量W實施的use或assign動做,假定動做G是和動做B相關聯的load或store動做,假定動做Q是和動做G相應的對變量W的read或write動做。若是A先於B,那麼P先於Q(這條規則要求volatile修飾的變量不會被指令重排序優化,保證代碼的執行順序與程序的順序相同)。
對於long和double型變量的特殊規則:併發
- Java內存模型要求lock、unlock、read、load、assign、use、store、write這8個操做都具備原子性,可是對於64位的數據類型(long和double),在模型中特別定義了一條相對寬鬆的規定:容許虛擬機將沒有被volatile修飾的64位數據的讀寫操做劃分爲兩次32位的操做來進行,即容許虛擬機實現選擇能夠不保證64位數據類型的load、store、read和write這4個操做的原子性,這點就是所謂的long和double的非原子性協定(Nonatomic Treatment ofdouble and long Variables)。
原子性、可見性與有序性:函數
- Java內存模型是圍繞着在併發過程當中如何處理原子性、可見性和有序性這3個特徵來創建的,咱們逐個來看一下哪些操做實現了這3個特性。
- 原子性(Atomicity):由Java內存模型來直接保證的原子性變量操做包括read、load、assign、use、store和write,咱們大體能夠認爲基本數據類型的訪問讀寫是具有原子性的(例外就是long和double的非原子性協定)。
- 可見性(Visibility):可見性是指當一個線程修改了共享變量的值,其餘線程可以當即得知這個修改。
- 有序性(Ordering):若是在本線程內觀察,全部的操做都是有序的;若是在一個線程中觀察另外一個線程,全部的操做都是無序的。
先發生原則:
- 它是判斷數據是否存在競爭、線程是否安全的主要依據,依靠這個原則,下面是Java內存模型下一些「自然的」先行發生關係,這些先行發生關係無須任何同步器協助就已經存在,能夠在編碼中直接使用。若是兩個操做之間的關係不在此列,而且沒法從下列規則推導出來的話,它們就沒有順序性保障,虛擬機能夠對它們隨意地進行重排序。
- 程序次序規則(Program Order Rule):在一個線程內,按照程序代碼順序,書寫在前面的操做先行發生於書寫在後面的操做。準確地說,應該是控制流順序而不是程序代碼順序,由於要考慮分支、循環等結構。
- 管程鎖定規則(Monitor Lock Rule):一個unlock操做先行發生於後面對同一個鎖的lock操做。這裏必須強調的是同一個鎖,而「後面」是指時間上的前後順序。
- volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操做先行發生於後面對這個變量的讀操做,這裏的「後面」一樣是指時間上的前後順序。
- 線程啓動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每個動做。
- 線程終止規則(Thread Termination Rule):線程中的全部操做都先行發生於對此線程的終止檢測,咱們能夠經過Thread.join()方法結束、Thread.isAlive()的返回值等手段檢測到線程已經終止執行。
- 線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過Thread.interrupted()方法檢測到是否有中斷髮生。
- 對象終結規則(Finalizer Rule):一個對象的初始化完成(構造函數執行結束)先行發生於它的finalize()方法的開始。
- 傳遞性(Transitivity):若是操做A先行發生於操做B,操做B先行發生於操做C,那就能夠得出操做A先行發生於操做C的結論。
三、Java與線程
線程的實現:
- 實現線程主要有3種方式:使用內核線程實現、使用用戶線程實現和使用用戶線程加輕量級進程混合實現。
- 使用內核線程實現:
- 內核線程(Kernel-Level Thread,KLT)就是直接由操做系統內核(Kernel,下稱內核)支持的線程,這種線程由內核來完成線程切換,內核經過操縱調度器(Scheduler)對線程進行調度,並負責將線程的任務映射到各個處理器上。
- 每一個內核線程能夠視爲內核的一個分身,這樣操做系統就有能力同時處理多件事情,支持多線程的內核就叫作多線程內核(Multi-Threads Kernel)
- 程序通常不會直接去使用內核線程,而是去使用內核線程的一種高級接口——輕量級進程(Light Weight Process,LWP),輕量級進程就是咱們一般意義上所講的線程,因爲每一個輕量級進程都由一個內核線程支持,所以只有先支持內核線程,纔能有輕量級進程。
- 這種輕量級進程與內核線程之間1:1的關係稱爲一對一的線程模型。
- 缺點:首先因爲是基於內核線程實現的,因此各類線程操做,如建立、析構及同步,都須要進行系統調用。而系統調用的代價相對較高,須要在用戶態(User Mode)和內核態(Kernel Mode)中來回切換。其次,每一個輕量級進程都須要有一個內核線程的支持,所以輕量級進程要消耗必定的內核資源(如內核線程的棧空間),所以一個系統支持輕量級進程的數量是有限的。
- 使用用戶線程實現:
- 從廣義上來說,一個線程只要不是內核線程,就能夠認爲是用戶線程(User Thread,UT)而狹義上的用戶線程指的是徹底創建在用戶空間的線程庫上,系統內核不能感知線程存在的實現。
- 用戶線程的創建、同步、銷燬和調度徹底在用戶態中完成,不須要內核的幫助。若是程序實現得當,這種線程不須要切換到內核態,所以操做能夠是很是快速且低消耗的,也能夠支持規模更大的線程數量,部分高性能數據庫中的多線程就是由用戶線程實現的。這種進程與用戶線程之間1:N的關係稱爲一對多的線程模型。
- 使用用戶線程的優點在於不須要系統內核支援,劣勢也在於沒有系統內核的支援,全部的線程操做都須要用戶程序本身處理。使用用戶線程實現的程序通常都比較複雜。
- 使用用戶線程加輕量級進程混合實現
- 在這種混合實現下,既存在用戶線程,也存在輕量級進程。用戶線程仍是徹底創建在用戶空間中,所以用戶線程的建立、切換、析構等操做依然廉價,而且能夠支持大規模的用戶線程併發。
- 而操做系統提供支持的輕量級進程則做爲用戶線程和內核線程之間的橋樑,這樣可使用內核提供的線程調度功能及處理器映射,而且用戶線程的系統調用要經過輕量級線程來完成,大大下降了整個進程被徹底阻塞的風險。
- 在這種混合模式中,用戶線程與輕量級進程的數量比是不定的,即爲N:M的關係。許多UNIX系列的操做系統,如Solaris、HP-UX等都提供了N:M的線程模型實現。
Java線程調度:
- 線程調度是指系統爲線程分配處理器使用權的過程,主要調度方式有兩種,分別是協同式線程調度(Cooperative Threads-Scheduling)和搶佔式線程調度(Preemptive Threads-Scheduling)。
- 協同式調度:若是使用協同式調度的多線程系統,線程的執行時間由線程自己來控制,線程把本身的工做執行完了以後,要主動通知系統切換到另一個線程上。
- 搶佔式調度:若是使用搶佔式調度的多線程系統,那麼每一個線程將由系統來分配執行時間,線程的切換不禁線程自己來決定(在Java中,Thread.yield()可讓出執行時間,可是要獲取執行時間的話,線程自己是沒有什麼辦法的)。在這種實現線程調度的方式下,線程的執行時間是系統可控的,也不會有一個線程致使整個進程阻塞的問題,Java使用的線程調度方式就是搶佔式調度。
狀態轉換:
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):已終止線程的線程狀態,線程已經結束執行。
第十三章 線程安全與鎖優化
一、線程安全
Java語言中的線程安全:
- 按照線程安全的「安全程度」由強至弱來排序,咱們能夠將Java語言中各類操做共享的數據分爲如下5類:不可變、絕對線程安全、相對線程安全、線程兼容和線程對立。
- 不可變:不可變(Immutable)的對象必定是線程安全的,不管是對象的方法實現仍是方法的調用者,都不須要再採起任何的線程安全保障措施,final關鍵字帶來的可見性,只要一個不可變的對象被正確地構建出來(沒有發生this引用逃逸的狀況),那其外部的可見狀態永遠也不會改變,永遠也不會看到它在多個線程之中處於不一致的狀態。「不可變」帶來的安全性是最簡單和最純粹的。
- 絕對線程安全:一個類要達到「無論運行時環境如何,調用者都不須要任何額外的同步措施」,一般須要付出很大的,甚至有時候是不切實際的代價。在Java API中標註本身是線程安全的類,大多數都不是絕對的線程安全。
- 相對線程安全:相對的線程安全就是咱們一般意義上所講的線程安全,它須要保證對這個對象單獨的操做是線程安全的,咱們在調用的時候不須要作額外的保障措施,可是對於一些特定順序的連續調用,就可能須要在調用端使用額外的同步手段來保證調用的正確性。在Java語言中,大部分的線程安全類都屬於這種類型,例如Vector、HashTable、Collections的synchronizedCollection()方法包裝的集合等。
- 線程兼容:線程兼容是指對象自己並非線程安全的,可是能夠經過在調用端正確地使用同步手段來保證對象在併發環境中能夠安全地使用,咱們日常說一個類不是線程安全的,絕大多數時候指的是這一種狀況。Java API中大部分的類都是屬於線程兼容的,如與前面的Vector和HashTable相對應的集合類ArrayList和HashMap等。
- 線程對立:線程對立是指不管調用端是否採起了同步措施,都沒法在多線程環境中併發使用的代碼。因爲Java語言天生就具有多線程特性,線程對立這種排斥多線程的代碼是不多出現的,並且一般都是有害的,應當儘可能避免。一個線程對立的例子是Thread類的suspend()和resume()方法,若是有兩個線程同時持有一個線程對象,一個嘗試去中斷線程,另外一個嘗試去恢復線程,若是併發進行的話,不管調用時是否進行了同步,目標線程都是存在死鎖風險的,若是suspend()中斷的線程就是即將要執行resume()的那個線程,那就確定要產生死鎖了。也正是因爲這個緣由,suspend()和resume()方法已經被JDK聲明廢棄(@Deprecated)了。常見的線程對立的操做還有System.setIn()、Sytem.setOut()和System.runFinalizersOnExit()等。
線程安全的實現方法:
- 互斥同步(Mutual Exclusion&Synchronization):是常見的一種併發正確性保障手段。同步是指在多個線程併發訪問共享數據時,保證共享數據在同一個時刻只被一個(或者是一些,使用信號量的時候)線程使用。而互斥是實現同步的一種手段,臨界區(Critical Section)、互斥量(Mutex)和信號量(Semaphore)都是主要的互斥實現方式。所以,在這4個字裏面,互斥是因,同步是果;互斥是方法,同步是目的。在Java中,最基本的互斥同步手段就是synchronized關鍵字,還可使用java.util.concurrent(下文稱J.U.C)包中的重入鎖(ReentrantLock)來實現同步。
- 非阻塞同步(Non-Blocking Synchronization):基於衝突檢測的樂觀併發策略,通俗地說,就是先進行操做,若是沒有其餘線程爭用共享數據,那操做就成功了;若是共享數據有爭用,產生了衝突,那就再採起其餘的補償措施(最多見的補償措施就是不斷地重試,直到成功爲止),這種樂觀的併發策略的許多實現都不須要把線程掛起,所以這種同步操做稱爲非阻塞同步(Non-Blocking Synchronization)。經常使用的有:測試並設置(Test-and-Set)、比較並交換(Compare-and-Swap,下文稱CAS)。
- 無同步方案:要保證線程安全,並非必定就要進行同步,二者沒有因果關係。同步只是保證共享數據爭用時的正確性的手段,若是一個方法原本就不涉及共享數據,那它天然就無須任何同步措施去保證正確性,所以會有一些代碼天生就是線程安全的。可重入代碼(Reentrant Code)和線程本地存儲(Thread Local Storage)。
二、鎖優化
自旋鎖與自適應自旋:
- 若是物理機器有一個以上的處理器,能讓兩個或以上的線程同時並行執行,咱們就可讓後面請求鎖的那個線程「稍等一下」,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖。爲了讓線程等待,咱們只需讓線程執行一個忙循環(自旋),這項技術就是所謂的自旋鎖。
- 在JDK 1.6中引入了自適應的自旋鎖。自適應意味着自旋的時間再也不固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。
鎖消除:
- 鎖消除是指虛擬機即時編譯器在運行時,對一些代碼上要求同步,可是被檢測到不可能存在共享數據競爭的鎖進行消除。
- 鎖消除的主要斷定依據來源於逃逸分析的數據支持,若是判斷在一段代碼中,堆上的全部數據都不會逃逸出去從而被其餘線程訪問到,那就能夠把它們當作棧上數據對待,認爲它們是線程私有的,同步加鎖天然就無須進行。
鎖粗化:
- 原則上,咱們在編寫代碼的時候,老是推薦將同步塊的做用範圍限制得儘可能小——只在共享數據的實際做用域中才進行同步,這樣是爲了使得須要同步的操做數量儘量變小,若是存在鎖競爭,那等待鎖的線程也能儘快拿到鎖。
- 大部分狀況下,上面的原則都是正確的,可是若是一系列的連續操做都對同一個對象反覆加鎖和解鎖,甚至加鎖操做是出如今循環體中的,那即便沒有線程競爭,頻繁地進行互斥同步操做也會致使沒必要要的性能損耗。
輕量級鎖:
- 輕量級鎖是JDK 1.6之中加入的新型鎖機制,它名字中的「輕量級」是相對於使用操做系統互斥量來實現的傳統鎖而言的,所以傳統的鎖機制就稱爲「重量級」鎖。
偏向鎖:
- 這個鎖會偏向於第一個得到它的線程,若是在接下來的執行過程當中,該鎖沒有被其餘的線程獲取,則持有偏向鎖的線程將永遠不須要再進行同步。
推薦博客:
http://www.javashuo.com/article/p-daisayll-cs.html
轉載請於明顯處標明出處:
http://www.javashuo.com/article/p-amblhnno-bd.html