#線程會共享進程範圍內的資源,例如記憶體句柄和檔案句柄,但每個執行緒都有各自的程式計數器(Program Counter)、堆疊以及局部變數等。
在同一個程式中的多個執行緒也可以同時調度到多個CPU上執行。
#Java中的主要同步機制是關鍵字synchronized,它提供了一種獨佔的加鎖方式,但「同步」這個術語還包括volatile類型的變量,明確鎖定(Explicit Lock)以及原子變量。
如果當多個執行緒存取同一個可變的狀態變數時沒有使用合適的同步,那麼程式就會出現錯誤。有三種方式可以修復這個問題:
#不在執行緒之間共享改狀態變數。
將狀態變數修改為不可變的變數。
在存取狀態變數時使用同步。
線程安全定義:當多個執行緒存取某個類別時,這個類別總是都能表現出正確的行為,那麼就稱這個類別是線程安全的。
無狀態物件一定是執行緒安全的。
大多數競態條件的本質:基於一個可能失效的觀察結果來做出判斷或是執行某個計算。這種類型的競態條件變成「先檢查後執行」:首先觀察到某個條件為真(例如檔案X不存在),然後根據這個觀察結果採用對應的動作(建立檔案X),但事實上,在你觀察到這個結果以及開始創建文件之間,觀察結果可能變得無效(另一個線程在這段期間創建了文件X),從而導致了各種問題(未預期的異常、資料被覆蓋、文件被破壞等)。
假定有兩個操作A和B,如果從執行A的執行緒來看,當另一個執行緒執行B時,要麼將B全部執行完,要麼完全不執行B ,那麼A和B對彼此來說是原子的。原子操作是指,對於存取同一個狀態的所有操作(包括該操作本身)來說,這個操作是一個以原子方式執行的操作。
在實際情況中,應盡可能使用現有的執行緒安全性物件(例如AtomicLong)來管理類別的狀態。與非線程安全的物件相比,判斷線程安全物件的可能狀態及其狀態轉換情況要更為容易,因此也更容易維護和驗證線程安全性。
要保持狀態的一致性,就需要在單一原子操作中更新所有相關的狀態變數。
重入的一種實作方法是,為每個鎖定關聯一個取得計數值和一個擁有者執行緒。當計數值為0時,這個鎖就被認為是沒有被任何執行緒持有。當執行緒請求一個未持有的鎖時,JVM將會記下鎖的持有者,並且將取得計數值設為1。如果同一個執行緒再次取得這個鎖,計數值將會遞增,而當執行緒退出同步程式碼區塊時,計數器會相應地遞減。當計數值為0時,這個鎖將會被釋放。
並非所有資料都需要鎖的保護,只有被多個執行緒同時存取的可變資料才需要透過鎖定來保護。
當執行時間較長的計算或可能無法快速完成的操作時(例如,網路IO或控制台IO),一定不要持有鎖定。
狀態的理解,我認為是類別的成員變數。無狀態物件就是成員變數不能儲存數據,或是可以儲存資料但是這個資料不可變。無狀態物件是線程安全的。如果方法中存在成員變量,就需要對這個成員變數進行相關的線程安全的操作。
不要一味地在方法前加synchronized,這可以保證線程安全,但是方法的並發功能會減弱,導致本來可以支持並發的方法變成堵塞,導致程序處理速度的變慢。
synchronized包圍的程式碼要盡可能的短,但是要保證有影響的所有成員變數在一起。沒有關係的成員變數可以用多個synchronized包圍。
#加鎖的意義不僅限於互斥行為,還包括記憶體可見性。為了確保所有執行緒都能看到共享變數的最新值,所有執行讀取操作或寫入操作的執行緒都必須在同一個鎖上同步。
Java語言提供了一個稍微弱的同步機制,即volatile變量,用來確保將變數的更新操作通知到其他執行緒。當把變數宣告為volatile類型後,編譯器與執行時間都會注意到這個變數是共享的,因此不會將該變數上的操作與其他記憶體操作一起重新排序。 volatile變數不會被緩存在暫存器或對其他處理器不可見的地方,因此在讀取volatile類型的變數時總是會傳回最新寫入的值。
在存取volatile變數時不會執行加鎖操作,因此也不會使執行緒阻塞,因此volatile變數是一種比synchronized關鍵字更輕量級的同步機制。
volatile變數通常會用做某個操作完成、發生中斷或是狀態的標誌。 volatile的語意不足以確保遞增操作(count )的原子性,除非你能確保只有一個執行緒對變數執行寫入操作。
加鎖機制既可以確保可見性又可以確保原子性,而volatile變數只能確保可見性。
當且僅當滿足以下所有條件時,才應該使用volatile變數:
對變數的寫入運算不依賴變數的當前值,或者你能確保只有單一執行緒更新變數的值。
該變數不會與其他狀態變數一起納入不變形條件中。
存取該變數時不需要加鎖。
「發布(Publish)」一個物件的意思是指,使物件能夠在目前作用域之外的程式碼中使用。
當某個不該發佈的物件被發佈時,這種情況就稱為逸出(Escape)。
不要在建構過程中使this引出逸出。
如果想在建構函式中註冊一個事件監聽器或啟動線程,那麼可以使用一個私有的建構函式和一個公共的工廠方法(Factory Method),從而避免不正確的構造過程。
堆疊封閉是執行緒封閉的一種特例,在堆疊封閉中,只有透過局部變數才能存取物件。
維持執行緒封閉性的一個更規範方法是使用ThreadLocal,這個類別能夠使執行緒中的某個值與保存值的物件關聯起來。
ThreadLocal物件通常用於防止可變的單一實例物件(Singleton)或全域變數進行共用。
當滿足以下條件時,物件才是不可變的:
#物件建立以後其狀態就不能修改。
物件的所有域都是final型別。
物件是正確建立的(在物件的建立期間,this參考沒有逸出)。
不可變物件一定是線程安全的。
要安全地發布一個對象,對象的參考以及對象的狀態必須同時對其他執行緒可見。一個正確建構的物件可以透過以下方式來安全地發布:
在靜態初始化函數中初始化一個物件參考。
將物件的參考儲存到volatile類型的域或AtomicReferance物件中。
將物件的參考儲存到某個正確建構物件的final類型域中。
將物件的參考儲存到一個由鎖定保護的網域。
在沒有額外的同步的情況下,任何執行緒都可以安全地使用被安全性發布的事實不可變物件。
物件的發布需求取決於它的可變性:
#不可變物件可以透過任意機制來發佈。
事實不可變物件必須透過安全方式來發布。
可變物件必須透過安全方式來發布,並且必須是線程安全的或由某個鎖定保護起來。
在並發程式中使用和共享物件時,可以使用一些實用的策略,包括:
線程封閉。線程封閉的物件只能由一個線程擁有,物件被封閉在該線程中,並且只能由這個線程修改。
只讀共享。在沒有額外同步的情況下,共享的唯讀物件可以由多個執行緒並發訪問,但任何執行緒都不能修改它。共享的唯讀物件包括不可變物件和事實不可變物件。
線程安全共享。線程安全的物件在其內部實現同步,因此多個線程可以透過物件的公有介面來存取而不需要進一步的同步。
保護物件。被保護的物件只能透過持有特定的鎖來存取。保護對象包括封裝在其他執行緒安全性物件中的對象,以及已發佈的並由某個特定鎖保護的對象。
發佈和逸出的理解:就是說一個類別中的成員變數或物件可以被其他的類別所引用使用就是發布,如用static修飾的靜態變數或是目前呼叫方法的物件。逸出是指該成員變數或物件在本來不應該被多執行緒引用的情況下暴露出去被引用,導致其值可能被錯誤修改的問題。一句話,不要隨便擴大一個類別以及內部使用成員變數和方法的作用域。這也是封裝應該考慮的問題。
this逸出:即在建構方法的內部類別中啟動另一個執行緒引用了這個對象,但是這時這個物件還沒有建構完成,可能會導致出乎意料的錯誤。解決方法是建立一個工廠方法,然後將構造器設定成私有建構器。
final修改的成員變數需要在建構器在建構器中初始化,否則物件實例化後這個成員變數不能賦值。 final修飾的成員變數是引用物件時,這個物件的位址不能修改,但是這個物件的值是可以修改的。
安全地發布一個物件的四種方式的理解,如A類中有B類的引用:
A的靜態初始化方法,如public static A a = new A(b);這樣的靜態工廠類別中,引用B的時候初始化B。
A類別中的B成員變數用volatile b或是AtomicReferance b這樣修飾。
A類別中的B成員變數用final B b這樣修飾。
A類別中的方法使用到B的時候用synchronized(lock){B…}包圍。
事實不可變物件很簡單的理解就是技術上是可變的,但是在業務邏輯處理中是不會去修改的物件。
相關推薦:
以上是JAVA並發程式設計總結:線程安全性、物件的分享的詳細內容。更多資訊請關注PHP中文網其他相關文章!