文章內容均來摘自:《深刻理解Java虛擬機:JVM高級特性與最佳實踐》。內容縮減 |
《Java Concurrency In Practice》做者Brian Goetz對線程安全作了個定義:當多個線程訪問一個對象時,若是不用考慮這些線程在運行時環境下的調度和交替執行,也不須要進行額外的同步,後者在調用方進行任何其它的協調操做,調用這個對象的行爲均可以得到正確的結果,那麼這個對象就是線程安全的。java
它要求線程安全的代碼必須具有一個特性:代碼自己封裝了全部必要的正確性保障(如互斥同步等),令調用者無需關心多線程的問題,更無需本身採起任何措施來保證多線程的正確調用(好比J.U.C包下的一些併發集合框架)。數組
在Java語言中,如何體現線程安全?若是一段代碼不會與其餘線程共享數據,那麼從線程安全的角度來看,程序是串行執行仍是併發執行對它來講是沒有區別的,也就是這段代碼是線程安全的;即多線程安全問題,在於多線程之間存在共享數據訪問操做。安全
那麼有哪些操做時線程安全的?在Java中,按照線程安全的「程度」由強至弱排序,分爲5種:不可變、絕對線程安全、相對線程安全、線程兼容和線程對立。多線程
(特指JDK1.5版本以後,即Java內存模型被修復以後的Java版本),不可變(Immutable)的對象必定是線程安全的,不管對象的方法實現仍是方法的調用者,都不須要採起任何的線程安全保障,若是一個對象是不可變的,那麼它永遠是一致的。併發
若是共享數據是一個基本數據類型,那麼只要在定義的時候使用final修飾就能夠保證它是不可變的(不可變即線程安全),若是共享數據是一個對象,咱們要保證操做該對象不會對它的狀態產生變化,好比java.lang.String類,咱們調用它的substring、replace等操做都會返回一個新對象而不對自身狀態進行變動(一個簡單的作法就是將對象中帶有狀態的變量以final修飾,這樣再初始化以後,便爲不可變)。框架
在Java中,標註爲線程安全的類,通常都不是絕對線程安全的,能夠用java中的一個非絕對線程安全的線程安全類來看看這個」絕對」的意思。性能
好比java.util.Vector是一個線程安全的容器,全部方法都被synchronized修飾,儘管這樣效率很低,但的確是安全的;可是,即便它全部的方法都被修飾成同步,也並不意味着使用它就再也不須要同步手段了。測試
若是A線程刪除了一個元素,致使某個序號不在可用,B線程恰好用那個序號訪問數組就會拋出越界異常,因此須要對A線程的remove操做和B線程的get操做進行同步操做。優化
對於b中的比方就是典型的相對線程安全示例。java中大多數線程安全類都是相對線程安全的,好比Vector、Hashtable、Collections下的synchronizedCollection方法包裝的集合等。spa
對象自己並非線程安全的,可是能夠經過同步手段來保證對象在併發環境中能夠安全使用,日常說的一個類不是線程安全的,大部分指的就是這種狀況。好比相對於c的Vector、Hashtabl的ArrayList、HashMap等。
不管調用端是否採起了同步手段,都沒法在併發環境中使用的代碼。
一個線程對立的例子,就是Thread類的suspend()和resume()方法,好比有兩個線程同時調用一個對象,一個嘗試去中斷線程,另外一個嘗試去恢復線程,若是併發執行的話,不管是否進行了同步,都會產生死鎖的風險。
咱們能夠經過三種方式來實現線程安全。
互斥同步是常見的一種併發正確性保障手段,同步指的是在多個線程併發訪問共享數據的時候,保證共享數據在同一時刻只被一個線程使用(或者一些,使用信號量的時候)。
Java中基本的同步手段就是synchronized關鍵字,synchronized同步塊對同一個線程是可重入的,不會把本身鎖死的狀況(即我得到了這把鎖,在同步塊中還能夠繼續使用而不會鎖住不讓執行);同步塊在線程執行完以前,會阻塞其它線程的進入。
除了synchronized以外,還可使用J.U.C包下的重入鎖ReentrantLock來實現同步,基本用法和synchronized相似,都具備同樣的線程可重入性,只是代碼上的區別,一種是API層面的互斥鎖(利用lock和unlock方法配合try/finally塊來完成),另外一種是原生語法層面的互斥鎖。不過,相比synchronized,ReentrantLock增長了一些高級功能主要有:等待可中斷、可實現公平鎖、可綁定多個條件。
·等待可中斷:指當前持鎖線程長期不釋放鎖,正在等待的線程能夠放棄等待,轉而處理其它事情,可中斷操做對處理執行時間很長的同步塊頗有幫助。
·公平鎖:多個線程在等待同一個鎖的時候,必須按照申請的時間順序依次得到鎖(相似於生活中的排隊,先來後到)。而非公平鎖在鎖被釋放的時候,任一線程均可能得到鎖;synchronized是一個非公平鎖,ReentrantLock默認也是非公平鎖,可是能夠經過參數來設定。
·綁定多個條件:一個ReentrantLock能夠同時綁定多個Conditional對象,而在synchronized中,所對象的wait()和notify()或notifyAll()方法可實現一個隱含的條件,若是要喝多於一個的條件關聯的時候,就不得不額外的添加一個鎖,而ReentrantLock則無需這麼作,只須要屢次調用newConditional便可。
基於性能及虛擬機將來優化的考慮,在能知足同步需求的狀況下,推薦使用synchronized。(在JDK1.6版本及以上synchronized性能基本與ReentrantLock持平)。
互斥同步最主要的問題是進行線程阻塞和喚醒所帶來的性能問題,所以這種同步也叫阻塞同步;從處理問題的方式上來講,互斥同步屬於一種悲觀的併發策略,認爲不去作正確的同步操做(例如加鎖),都會出現問題,不管共享數據是否真的會出現競爭。
而非阻塞同步(樂觀的併發策略),通俗講就是先進行操做,若是無其它線程對共享數據進行爭用,那就操做成功了;若是有爭用,產生了衝突,那就重來,直到成功爲止。(參閱AtomicInteger的incrementAndGet()方法)
樂觀併發策略須要「硬件指令集」,這類指令經常使用的有:
CAS操做有一個邏輯漏洞:若是有一個值是A,而後曾經被改爲了B,後來又改回A,那麼CAS會誤認爲它沒有改變過,這個問題被稱爲CAS的"ABA"問題(J.U.C包下的一個帶有標記的原子引用類AtomicStampedReference,它能夠經過控制變量的版本號來保證CAS的正確性)。可是大部分狀況下"ABA"問題不會影響程序併發的正確性,若是須要解決ABA問題,採用傳統的互斥同步相對比原子類操做更高效。
要保證線程安全,並不必定要進行同步,同步只是保證共享數據的正確性,若是一個方法不涉及共享數據,那它天然就無需任何同步手段去保證正確性了;所以會有一些代碼天生具備線程安全性。
·可重入代碼:可重入代碼具備一些共同的特性,例如不依賴存儲在堆上的數據和公用的系統資源、用到的狀態量都由參數傳入、不調用非可重入的方法等。咱們能夠經過一個簡單的原則來判斷一段代碼是否具備可重入性:例若有一個方法,它的返回結果老是可預測的,只要輸入了相同的數據,給你的都是同一個結果,那它就知足了可重入的要求,那就是線程安全的了。
·線程本地存儲:ThreadLocal