咱們已經知道了同步代碼塊和同步方法能夠確保以原子的方式執行操做,但一種常見的誤解是,認爲關鍵字synchronized智能用於實現原子性和肯定「臨界區(Critical Section)」。同步還有另外一個重要的方面:內存可見性(Memory Visibility)。咱們不只但願防止某個線程正在使用對象狀態而另外一個線程在同時修改該狀態,並且但願確保當一個線程修改了對象狀態後,其餘線程可以看到發生的狀態變化。若是沒有同步,那麼這種狀況就沒法實現。你能夠經過顯式的同步或者類庫中內置的同步來確保對象被安全的發佈。java
可見性是一種複雜的屬性,由於可見性中的錯誤老是會違背咱們的直覺。在單線程環境中,若是向某個變量先寫入值,而後在沒有其餘寫入的狀況下讀取這個變量,那麼總能獲得相同的值。然而,當讀取操做和寫入操做在不一樣的線程中執行時,狀況卻並不是如此。一般,咱們沒法確保執行讀取操做的線程能適時地看到其餘線程寫入的值,有時甚至是根本不可能的事情。爲了確保多個線程之間對內存寫入操做的可見性,必須使用同步機制,例如:程序員
public class NoVisibility { private static boolean ready; private static int number; private static class ReaderThread extends Thread { public void run() { while(!ready) Thread.yield(); System.out.println(number); } } public static void main(String[] args) { // 從代碼上來看,先執行run函數,在設置number和ready的值 new ReaderThread().start(); number = 42; ready = true; } }
NoVisibility可能會持續循環下去,由於讀線程可能永遠都看不到ready的值。一種更奇怪的現象是,NoVisibility可能會輸出0,由於讀線程可能看到了寫入ready的值,但卻沒有看到以後寫入number的值,這種現象被稱爲「重排序(Reordering)」。只要在某個線程中沒法檢測到重排序狀況,那麼就沒法確保線程中的操做將按照程序中指定的順序來執行。當主程序先寫入number,而後在沒有同步的狀況下寫入ready,那麼讀線程看到的順序可能與寫入的順序徹底相反。數組
在沒有同步的狀況下,編譯器、處理器以及運行時等均可能對操做的執行順序進行一些意想不到的調整。在缺少足夠同步的多線程程序中,要想對內存操做的執行順序進行判斷,幾乎沒法得出正確的結論。緩存
NoVisibility展現了在缺少同步的程序中可能產生的錯誤結果的一種:失效數據。當讀線程查看ready變量時,可能會獲得一個已經失效的值。除非在每次訪問變量時都使用同步。不然極可能得到該變量的一個失效值。更糟糕的是,失效值可能不會同時出現:一個線程可能得到某個變量的最新值,而得到另外一個變量的失效值。再看來一個例子:安全
// 線程不安全 public class MutableInteger { private int value; public int getValue() { return value; } public void setValue(int value) { this.value = value; } }
MutableInteger不是線程安全的,由於get和set都是在沒有同步的狀況下訪問value的。若是某個線程調用了set,那麼另外一個正在調用get的線程可能會看到更新後的value值,也可能看不到。下面將該類改寫成線程安全的:多線程
// 線程安全 public class MutableInteger { private int value; public synchronized int getValue() { return value; } public synchronized void setValue(int value) { this.value = value; } }
經過對get和set等方法進行同步,可使MutableInteger成爲一個線程安全的類。僅僅對set方法進行同步是不夠的,調用get的線程仍然會看到失效值。併發
當線程在沒有同步的狀況下讀取變量時,可能會獲得一個失效值,但至少這個值是由以前某個線程設置的值,而不是一個隨機值。這種安全性保證也被稱爲最低安全性(out-of-thin-airsafety)。框架
最低安全性適用於絕大多數變量,可是存在一個例外:非volatile類型的64位數值變量(double和long)。Java內存模型要求,變量的讀取操做和寫入操做都必須是原子操做,但對於非volatile類型的long和double變量,JVM容許將64位的讀操做或寫操做分解爲兩個32位的操做。當讀取一個非volatile類型的long變量時,若是對該變量的讀操做和寫操做在不一樣的線程中執行,那麼極可能會讀取到某個值的高32位和另外一個值的低32位。所以,即便不考慮失效數據問題,在多線程程序中使用共享且可變的long和double等類型的變量也不是安全的,除非用關鍵字volatile來聲明他們,或者用鎖保護起來。函數
在訪問某個共享且可變的變量時要求全部線程在同一個鎖上同步,確保某個線程寫入該變量的值對於其餘線程來講都是可見的。不然,若是一個線程在未持有正確鎖的狀況下讀取某個變量,那麼讀到的多是一個失效值。this
加鎖的含義不只僅侷限於互斥行爲,還包括內存可見性。爲了確保全部線程都能看到共享變量的最新值,全部執行讀取操做或者寫入操做的線程都必須在同一個鎖上同步。
Java語言提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操做通知到其餘線程。當把變量聲明爲volatile類型後,編譯器在運行時都會注意到這個變量是共享的,所以不會將該變量上的操做與其餘內存操做一個重排序。volatile變量不會被緩存在寄存器或者對其餘處理器不可見的地方,所以在讀取volatile類型的變量時總會返回最新寫入的值。
注意,在訪問volatile變量時不會執行加鎖操做,所以也就不會使執行線程阻塞,所以volatile變量是一種比sychronized關鍵字更輕量級的同步機制。
volatile變量對可見性的影響比volatile變量自己更爲重要。從內存可見性的角度來看,寫入volatile變量至關於退出同步代碼塊,而讀取volatile變量至關於進入同步代碼塊。然而,並不建議過分依賴volatile變量提供可見性。
僅當volatile變量能簡化代碼的實現以及對同步策略的驗證時,才應該使用它們。若是在驗證正確性時須要對可見性進行復雜的判斷,那麼就不要使用volatile變量。
volatile變量的正確使用方式包括:確保他們自身的狀態的可見性,確保他們所引用的對象的狀態的可見性,以及標識一些重要的程序生命週期事件的發生(例如,初始化或關閉)。
雖然volatile變量很方便,但也存在一些侷限性。volatile變量一般用做某個操做完成、發生中斷或者狀態的標誌。儘管volatile變量也能夠用於表示其餘的狀態信息,但在使用時要很是當心。例如,volatile的語義不足以確保遞增操做(count++)的原子性,除非你能確保只有一個線程對變量執行寫操做。
加鎖機制既能夠確保可見性又能夠確保原子性,而volatile變量只能確保可見性。
當且僅當知足一下全部條件時,才應該使用volatile變量:
發佈(Publish)一個對象的意思是指,使對象可以在當前做用域以外的代碼中使用。在許多狀況中,咱們要確保對象及其內部狀態不被髮布。而在某些狀況下,咱們有須要發佈這個對象,但若是在發佈時要確保線程安全性,則可能須要同步。發佈內部狀態會破壞封裝性,並使程序難以維持不變性條件。當某個不該該發佈的對象被髮布時,這種狀況就被稱爲逸出(Escape)。例如:
public static Set<Secret> knownSecrets; public void initialize() { knownSecrets = new HashSet<Secret>(); }
在initialize方法中實例化一個新的HashSet對象,並將對象的引用保存到knownSecrets中以發佈對象。
當發佈某個對象時,可能會間接發佈其餘對象。若是將一個Secret對象添加到集合knownSecrets中,那麼一樣會發布這個對象,由於任何代碼均可以遍歷這個集合,並得到對這個新Secret對象的引用。一樣,若是從非私有方法中返回一個引用,那麼一樣會發布返回的對象。看一段代碼:
class UnsafeStates { private String[] states = new String[] { "AK", "AL", ... }; public String[] getStates() { return states; } }
若是按照上面的代碼發佈states,就會出現問題,由於任何調用者都能修改這個數組的內容。在這個例子中,數組states已經逸出了它所在的做用域,由於這個本應是私有的變量已經被髮布了。
當發佈一個對象時,在該對象的非私有域中引用的全部對象一樣會被髮布。通常來講,若是一個已經發布的對象可以經過非私有的變量引用和方法調用到達其餘的對象,那麼這些對象也都會被髮布。
最後一種發佈對象或其內部狀態的機制就是發佈一個內部的類實例,例如:
publi class ThisEscape { public ThisEscape(EventSource source) { source.registerListener { new EventListener() { public void onEvent(Event e) { doSomething(e); } } }; } }
不要在構造過程當中使this引用逸出。
當內部的EventListener實例發佈時,在外部封裝的ThisEscape實例也逸出了。當且僅當對象的構造函數返回時,對象才處於可預測的和一致的狀態。所以,當從對象的構造函數中發佈對象時,只是發佈了一個還沒有構造完成的對象。即便發佈對象的語句位於構造函數的最後一行也是如此。若是this引用在構造過程當中逸出,那麼這種對象就被認爲是不正確構造。
可使用工廠方式來防治this引用在構造過程當中逸出。
當訪問共享的可變數據時,一般須要使用同步。一種避免使用同步的方式就是不共享數據。若是僅在單線程內訪問數據,就不須要同步。這種技術被稱爲線程封閉(Thread Confinement),它是實現線程安全性的最簡單方式之一。當某個對象封閉在一個線程中時,這種用法將自動實現線程安全性,即便被封閉的對象自己不是線程安全的。
在Java語言中並無強制規定某個變量必須由鎖來保護,一樣在Java語言中也沒法強制將對象封閉在某個線程中。線程封閉式在程序設計中的一個考慮因素,必須在程序中實現。Java語言及其核心庫提供了一些機制來幫助維持線程封閉性,例如局部變量和ThreadLocal類。但比便如此,程序員仍然須要負責確保封閉在線程中的對象不會從線程中逸出。
Ad-hoc線程封閉是指,維護線程封閉性的職責徹底由程序實現來承擔。Ad-hoc線程封閉式很是脆弱的,由於沒有任何一種語言特性,能將對象封閉到目標線程上。事實上,對線程封閉對象的引用一般保存在公有變量中。
當決定使用線程封閉技術時,一般是由於要將某個特定的子系統實現爲一個單線程子系統。在某些狀況下,單線程子系統提供的簡便性要賽過Ad-hoc線程封閉技術的脆弱性。
舉個例子,在volatile變量上存在一種特殊的線程封閉。只要你能肯定只有單個線程對共享的volatile變量執行寫入操做,那麼就能夠安全的在這些共享的volatile變量上執行「讀取-修改-寫入」的操做。在這種狀況下,至關於修改操做封閉在單個線程中以防止發生競態條件,而且volatile變量的可見性保證還確保了其餘線程能看到最新的值。
因爲Ad-hoc線程封閉技術的脆弱性,所以在程序中儘可能少用它,可能的狀況下,應該使用更強的線程封閉技術(例如,棧封閉和ThreadLocal類)。
棧封閉是線程封閉的一種特例,在棧封閉中,只能經過局部變量才能訪問對象。正如封裝能是的代碼更容易維持不變性條件那樣,同步變量也能使對象更易與封閉在線程中。局部變量的固有屬性之一就是封閉在執行線程中。它們位於執行線程的棧中,其餘線程沒法訪問這個棧。棧封閉比Ad-hoc線程封閉更易於維護,也更加健壯。
public int loadTheArk(Collection<Animal> candidates) { SortedSet<Animal> animals; int numPairs = 0; Animal candidate = null; animals = new TreeSet<Animal>(new SpeciesGenderComparator()); animals.addAll(candidates); for (Animal a : animals) { if (candidate == null || !candidate.isPotentialMate(a)) candidate = a; else { ark.load(new AnimalPair(candidate, a)); ++numPairs; candidate = null; } } return numPairs; }
在上面的代碼中,numPairs不管如何都不會破壞棧封閉性。因爲任何方法都沒法得到對基本類型的引用,所以Java語言的這種語義就確保了基本類型的局部變量封閉在線程內。
在維持對象引用的棧封閉性時,程序員須要多作一些工做以確保被引用的對象不會逸出。在loadTheArk中實例化一個TreeSet對象,並將指向該對象的一個引用確保到animals中。此時,只有一個引用指向集合animals,這個引用被封閉在局部變量中,所以也被封閉在執行線程中。然而,若是發佈了對集合animals的引用,那麼封閉性將被破壞,並致使對象animals的逸出。
維持線程封閉性的一種更規範方式是使用ThreadLocal,這個類能使線程中的某個值與保存值得對象關聯起來。ThreadLocal提供了get與set等訪問接口或方法,這些方法爲每一個使用該變量的線程都存有一份獨立的副本。所以get老是返回由當前執行線程在調用set時設置的最新值。
ThreadLocal對象一般用於防止對可變的單實例變量(Singleton)或全局變量進行共享。例如Connection對象,因爲JDBC的鏈接對象不必定是線程安全的,所以,當多線程應用程序在沒有協同的狀況下使用全局變量時,就不是線程安全的。經過將JDBC的鏈接保存到ThreadLocal對象中,每一個線程都會擁有屬於本身的連接,例如:
public int loadTheArk(Collection<Animal> candidates) { SortedSet<Animal> animals; int numPairs = 0; Animal candidate = null; animals = new TreeSet<Animal>(new SpeciesGenderComparator()); animals.addAll(candidates); for (Animal a : animals) { if (candidate == null || !candidate.isPotentialMate(a)) candidate = a; else { ark.load(new AnimalPair(candidate, a)); ++numPairs; candidate = null; } } return numPairs; }
在實現應用程序框架時大量使用了ThreadLocal。例如,在EJB調用期間,J2EE容器須要將一個事務上下文(Transaction Context)與某個執行中的線程關聯起來。經過將事務上下文保存在靜態的ThreadLocal對象中,能夠很容易地實現這個功能:當框架代碼須要判斷當前運行的是哪個事務時,只需從這個ThreadLocal對象中讀取事務上下文。
開發人員常常濫用ThreadLocal,例如將全部的全局變量都做爲ThreadLocal對象,或者做爲一種「隱藏」方法參數的手段。ThreadLocal變量相似於全局變量,它能下降代碼的課重用性,並在類之間引入隱含的耦合性,所以在使用時須要格外當心。
若是某個對象在被建立後其狀態就不能被修改,那麼這個對象就稱爲不可變對象(Immutable Object)。線程安全性是不可變對象的固有屬性之一,它們的不變性條件是由構造函數建立的,只要它們的狀態不改變,那麼這些不變性條件就能得以維持。
不可變對象必定是線程安全的。
不可變對象很簡單。它們只有一種狀態,而且該狀態由構造函數來控制。在程序設計中,一個最困難的地方就是判斷複雜對象的可能狀態。然而,判斷不可變對象的狀態卻很簡單。
一樣,不可變對象也更加安全。若是將一個可變對象傳遞給不可信的代碼,或者將該對象發佈到不可信代碼能夠訪問它的地方,那麼就很危險 —— 不可信代碼會改變它們的狀態,更糟的是,在代碼中將保留一個對該對象的引用並稍後再其餘線程中修改對象的狀態。另外一方面,不可變對象不會像這樣被惡意代碼或者有問題的代碼破壞,所以能夠安全地共享和發佈這些對象,而無須建立保護性的副本。
雖然在Java語言規範和Java內存模型中都沒有給出不可變性的正式定義,但不可變性並不等於將對象中全部的域都聲明爲final類型,即便對象中全部的域都是final類型的,這個對象也仍然是可變的,由於在final類型的域中能夠保存對可變對象的引用。
當知足一下條件時,對象纔是不可變的:
關鍵字final能夠視爲C++中const機制的一種受限版本,用於構造不可變性對象。final類型的域是不能修改的。然而,在Java內存模型中,final域還有着特殊的語義。fianl域能確保初始化過程的安全性,從而能夠不受限制地訪問不可變對象,並在共享這些對象時無需同步。
即便對象時可變的,經過將對象的某些域聲明爲final類型,仍然能夠簡化對狀態的判斷,所以限制對象的可變性也就至關於限制了該對象可能的狀態集合。僅包含一個或兩個可變狀態的「基本不可變」對象仍然比包含多個可變狀態的對象簡單。經過將域聲明爲final類型,也至關於告訴維護人員這些域是不會變化的。
除非須要某個域是可變的,不然應將其聲明爲final域。
到目前爲止,咱們重點討論的是如何確保對象不被髮布,例如讓對象封閉在線程或另外一個對象的內部。固然,在某些狀況下咱們但願在多個線程之間共享對象,此時必須確保安全的進行共享。看一段代碼:
public Holder holder; public void initialize() { holder = new Holder(42); }
這段代碼中,將引用對象保存到公有域中,那麼還不足以安全得發佈這個對象。因爲存在可見性問題,其餘線程看到的Holder對象將處於一個不一致的狀態,即使在該對象的構造函數中已經正確的構造了不變性條件。這種不正確的發佈致使其餘線程看大還沒有建立完成的對象。
你不能期望一個還沒有被徹底建立的對象擁有完整性。某個觀察該對象的線程將看到對象處於不一致狀態,而後看到對象的狀態忽然發生變化,即便線程在對象發佈後尚未修改過它。例如:
public class Holder { private int n; public Holder(int n) {this.n = n;} public void assertSanity() { if (n != n) throw new AssertionError("this statement is false."); } }
因爲沒有使用同步來確保Holder對象對其餘線程可見,所以將Holder稱爲「未被正確發佈」。這裏面存在兩個問題,首先,除了發佈對象的線程外,其餘線程能夠看到的Holder域是一個失效值,所以將看到一個空引用或者以前的舊值。然而,更糟的狀況是線程看到Holder引用的值是最新的,但Holder狀態的值倒是失效的。狀況變得更加不可預測的是,某個線程在第一次讀取域時獲得失效值,而再次讀取這個域時會獲得一個更新值,這也是assertSainty拋出AssertionError的緣由。
因爲不可變對象是一種很是重要的對象,所以Java內存模型爲不可變對象提供了一種特殊的初始化安全性保證。咱們已經知道,即便某個對象的引用對其餘線程是可見的,也並不意味着對象狀態對於使用該對象的線程來講必定是可見的。爲了確保對象狀態能呈現出一直的視圖,就必須使用同步。
任何線程均可以在不須要額外同步的狀況下安全地訪問不可變對象,即便在發佈這些對象時沒有使用同步。
要安全地發佈一個對象,對象的引用以及對象的狀態必須同事對其餘線程可見。一個正確構造的對象能夠經過如下方式來安全地發佈:
在線程安全容器內部的同步意味着,在將對象放入到某個容器,例如Vector或synchronizedList時,將知足上述最後一條需求。
若是對象在發佈後不會被修改,那麼對於其餘在沒有額外同步的狀況下安全地訪問這些對象的線程來講,安全發佈是足夠的。全部的安全發佈機制都能確保,當對象的引用對全部訪問該對象的線程可見時,對象發佈時的狀態對於全部線程也將是可見的,而且若是對象狀態不會再改變,那麼就足以確保任何訪問都是安全的。
在沒有額外的同步的狀況下,任何線程均可以安全地使用被安全發佈的事實不可變對象。
若是對象在構造後能夠修改,那麼安全發佈只能確保「發佈當時」狀態的可見性。對於可變對象,不只在發佈對象時須要使用同步,並且在每次對象訪問時一樣須要使用同步來確保後續修改操做的可見性。
對象的發佈需求取決於它的可變性:
在併發程序中使用和共享對象時,可使用一些實用的策略,包括: