在沒有同步的狀況下,編譯器以及運行時等收可能對操做的執行順序進行一些意想不到的調整,在缺少足夠同步的多線程程序中,要想對內存操做的執行順序進行判斷,幾乎沒法獲得正確的結論。程序員
1.缺少同步的程序中可能出產生錯誤結果的一種狀況:失效數據緩存
當線程在沒有同的狀況下讀取變量時,可能會獲得一個失效值,但至少這個值是由某個線程設置的值,而不是一個隨機值。這種安全性保證也被稱爲最低安全性。安全
最低安全性適用於絕大多數變量,可是存在一個例外:非volatile類中的64位數值變量。服務器
Java內存模型要求,變量的讀取操做和寫入操做都必須是原子操做,但對非volatile類型的long和double變量,JVM容許將64位的讀操做或寫操做分解爲兩個32位操做。多線程
當讀取一個非volatile類型的long變量時,若是對該變量的讀操做和寫操做在不一樣的線程中執行,俺麼極可能會讀到某個值得高32位和另外一個值得低32位。所以,即便不考慮失效數據問題,在多線程程序中使用共享可變的long和double等類型的變量也是不安全的。除非用關鍵字volatile來聲明他們,或者用鎖來保護起來。併發
加鎖的含義不只僅侷限於互斥行爲,還包括內存可見性,爲了確保全部線程都能看到共享變量的最新值,全部執行讀操做或者寫操做的線程必須在同一個鎖上同步。函數
volatile變量,用來確保將變量的更新操做通知到其餘線程。當變量聲明爲volatile類型後,編譯器與運行時都會注意到這個變量是共享德的,所以不會將該變量上的操做與其餘內存操做儀器重排序。this
volatile變量不會被緩存在寄存器或者其餘處理器不可見的地方,所以在讀取volatile類型的變量時總會返回最新寫入的值。spa
在訪問volatile變量時會執行加鎖操做,所以,也就不會執行線程阻塞,所以volatile變量時一種比synchronized關鍵字更輕量級的同步機制。線程
僅當volatile變量能簡化代碼的實現以及對同步策略的驗證時,才應該使用它們。若是在驗證正確性時須要對可見性進行復雜的判斷,那麼久不要使用volatile變量。volatile變量的正確性使用方式包括:確保它們自身狀態的可見性,確保它們所引用對象的狀態的可見性,以及標識一些更重要的程序生命週期事件的發生(例如,初始化或關閉)。
volatile變量的一種典型用法:檢查某個標記狀態以判斷是否退出循環。
加鎖機制既能夠確保可見性,又能夠確保原子性,而volatile變量只能確保可見性。
當且僅當知足一下全部條件時,才因該使用volatile變量:
a)對變量的寫入操做不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。
b)該變量不會與其餘裝調變量一塊兒歸入不變性條件中
c)在訪問變量時不須要加鎖
發佈(Publish)一個對象的意思是指,是對象可以在當前做用域以外的代碼中使用。
當某個不該該發佈的對象被髮布時,這種狀況就被成爲逸出(Escape)。
發佈對象的最簡單的方法是將對象的引用保存到一個公有的靜態變量中,以便任何類和線程都能看見該對象。
不要在構造過程當中使this引用逸出。
當對象在其構造函數中建立一個線程時,不管是顯式建立(經過將它傳給構造函數)仍是隱式建立(由Thread或Runnable是該對象的一個內部類),this引用都會被新建立的線程共享。在對象還沒有徹底構建以前,先的線程就能夠看見它。在構造函數中建立線程並無錯,可是最好不要當即啓動它,而是經過一個start或initialize方法來啓動。在構造函數中調用一個不可改寫的實例方法時,一樣會致使this引用在構造過程當中逸出。
當訪問共享的可變數據時,一般須要使用同步。一種避免使用同步的方式就是不共享數據。若是僅在單線程內訪問數據,就不須要同步,這種技術稱爲線程封閉(Thread Confinement) ,它是實現線程安全性的最簡單方式之一。當某一個對象封閉在一個線程中時,這種用法將自動實現線程安全性,即便被封閉的對象自己不是線程安全的。
在Swing中大量使用了線程封閉技術。
線程封閉技術另外一種常見應用使JDBC的connection對象,JDBC規範並要求connection對象必須是線程安全的。在典型的服務器應用程序中,線程從鏈接池中獲取一個connection對象,而且用該對象來處理請求,使用完成後再將對象返回給鏈接池。因爲大多數請求都是由的單個線程採用同步的方式來處理,而且在connection對象返回以前,鏈接池不會再將它分配給其餘線程,所以,這種鏈接管理模式在處理請求時隱含地將connection對象封閉在線程中。
在Java語言中並無強制規定某個變量必須鎖來保護,一樣在Java語言中也沒有強制將一個對象封閉在 某個線程中。線程封閉式在程序設計中的一個考慮因素,必須在程序中實現,Java語言及核心庫提供了一些機制來幫助維持線程封閉性,例如局部變量和ThreadLocal類,但即使如此,程序員仍然須要負責確保在線程中的對象不會從線程中逸出。
Ad_hoc線程封閉是指,維護線程封閉性的職責徹底由程序實現類承擔,Ad_hoc線程封閉式很是脆弱的,由於沒有任何一種語言特性,例如可見性修飾符或局部變量,能將對象封閉到目標線程上。
當決定使用線程封閉技術時,一般是由於要將某個特定的子系統實現爲一個單線程子系統。在某種狀況下,單線程子系統提供的簡便性要賽過Ad_hoc線程封閉技術的脆弱性。使用單線程子系統的另外一個緣由是爲了不死鎖。
棧封閉是線程封閉的一種特例,在棧封閉中,只能經過局部變量才能訪問對象局部變量的固有屬性就是封閉在程序的執行線程之中。
若是在線程內部上下文中使用非線程安全的對象,那麼該對象仍然是線程安全的。
維持線程封閉的一種更規範的方法是ThrashLocal,這個類能使線程中的某個值域保存值得對象關聯起來。ThreadLocal提供了get和set等訪問接口或方法,這些方法爲每一個使用該變量的線程都存有一份獨立的副本,所以get老是返回由當前執行線程在調用set時設置的最新值。
ThreadLocal對象一般用於防止對可能的單實例變量或全局變量進行共享。
當某個頻繁執行的操做須要一個臨時變量的操做須要一個臨時變量,例如一個緩衝區,而同時又但願避免在每次執行時都更新分配該對象,就可使用這項技術。
ThreadLocal變量相似於全局變量,它能下降代碼可重用性,並在類之間引入隱含的耦合性,所以在使用時要格外當心。
知足同步的另外一種方法是使用不可變對象(Immutable Object)。
若是某個對象被建立後其狀態不能被修改,那麼這個對象就被稱爲不可變對象。線程安全性是不可變對象的固有屬性之一,它們的不變性條件是由構造函數建立的,只要它們不改變,那麼這些不變性條件就能得以維持。
當知足如下條件時,對象纔是不可變的:
1)對象建立之後其狀態就不能修改
2)對象的全部域都是final類型
3)對象是正確建立的(在對象的建立期間,this引用沒有逸出)
final域:關鍵字final能夠視爲C++中const機制的一種受限版本,用於構造不可變對象。final類型的域是不能修改的(可是若是final域引用的對象是可變的,那麼這些被引用的對象是能夠修改的)。final域能確保初始化過程的安全性,從而能夠不受限制地訪問不可變對象,並在共享這些對象時無需同步。除非須要更高的可見性,不然應將全部的域都聲明爲私有域。除非須要某個域是可變的,不然應將其聲明爲final域。
對於在訪問和更新多個相關變量時出現的競爭問題,能夠經過將這些變量所有保存在一個不可變對象中來消除,若是是一個可變的對象,那麼當線程得到了該對象的引用後,就沒必要擔憂另外一個線程會修改對象的狀態。若是要更新這些變量,那麼能夠建立一個新的容器,但其餘使用原有對象的線程,仍然會看到對象處於一致的狀態。
不正確的發佈:正確的對象被破壞。不能期望一個還沒有被徹底建立的對象擁有完整性。
不可變對象與初始化安全性:任何線程均可以在須要額外的同步的狀況下安全地訪問不可變對象,即便在發佈這些對象是沒有使用任何同步。
1)要安全發佈一個對象,對象的引用以及對象的狀態必須同時對其餘線程可見。一個正確的構造的對象能夠經過一下的方式來安全地發佈:
a)在靜態初始化函數中初始化一個對象引用
b)將對象的引用保存到volatile類型的域或者AtomitReference對象中
c)將對象的引用保存到某個正確構造對象的final類型域中
d)將對象的引用保存到一個由鎖保護的域中
2)線程安全庫中的容器提供了一下的安全發佈保證:
a)經過將一個鍵或者值放入Hashtable,SynchronizedMap或者ConscurrentMap中能夠安全的將它發佈給任何從這些容器中訪問的線程(不管是直接訪問仍是經過迭代器訪問)。
b)經過將某個元素放入Vector,CopyOnWriteArrayList,CopyOnWriteArraySet,SynchronizedList或SynchronizedSet中,能夠將該元素安全的發佈到任何從這些容器中訪問該元素的線程。
c)經過將某個元素放入BlockingQueue或者ConcurrentLinkedQueue中,能夠將該元素安全地發佈到從這些隊列中訪問該元素的線程。
3)一般要發佈一個靜態構造的對象,最簡單和安全的方式是使用靜態的初始化器。靜態初始化器由JVM在類的初始化階段執行。因爲在JVM內部存在着同步機制,所以經過這種方式初始化的任何對象均可以被安全地發佈。
若是一個對象從技術上來看是可變的,但其狀態在發佈後不會再改變,那麼這種對象稱爲「事實不可變對象」。在沒有額外的同步的狀況下,任何線程收能夠安全地使用被安全發佈的事實不可變對象。
對象的發佈需求取決於它的可變性:
1)不可變對象能夠經過任意機制來發布
2)事實不可變對象必須經過安全發佈方式來發布
3)可變對象必須經過安全方式來發布,而且必須是線程安全的或者由某個鎖保護起來
在併發程序中使用和共享對象是可使用一些實用的策略,包括:
1)線程封閉:線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中,而且只能由這個線程修改
2)只讀共享:在沒有額外天同步的狀況下,共享的只讀對象能夠由多個線程併發訪問,但任何線程都不能修改它。共享的只讀對象包括不可變對象和事實不可變對象
3)線程安全共享:線程安全的對象在其內部實現同步,所以多個線程能夠經過對象的公有接口來訪問爲不須要進一步的同步
4)保護對象:被保護的對象只能經過持有特定的鎖來訪問。保護對象包括封裝在其餘線程安全對象中的對象,以及發佈的而且由某個特定鎖保護的對象。