什么是線程安全
過往在使用synchronized關(guān)鍵字的時候廊遍,通常都會和線程安全問題相掛鉤。那么這個線程安全的定義又是什么呢祟滴? 在我學(xué)習《深入JVM虛擬機》這本書中提到了一段話我覺得解釋的不錯: "當多個線程訪問一個對象是刊懈,如果不用考慮這些線程在運行時環(huán)境下的的調(diào)度和交替執(zhí)行,也不需要進行額外的同步捷凄,或者在調(diào)用方進行任何其他的協(xié)調(diào)操作杠览,調(diào)用這對象的行為都可以獲得正確的結(jié)果,那這個對象就是線程安全的 " 這個定義是比較嚴謹?shù)淖菔疲瑥乃拿枋鰜砜淳€程安全必須具備的一個特征就是踱阿; 一段代碼本身封裝了所有必要的正確性保障手段(如互斥同步等), 另調(diào)用者無需關(guān)心多線程的安全問題管钳,更無須自己采取任何措施來保證多線程的正確調(diào)用。
Java語言中的線程安全分類
為了更好的理解線程安全软舌, 我們將線程安全性分解為更多的層次才漆, 將線程安全程度有高到低進行排列: 不可變 -> 絕對線程安全 -> 相對線程安全 -> 線程兼容 -> 線程對立
-
不可變(Immutable)
在Java 語言中, 不可變對象一定是線程安全的(例如String), 無論是對象的方法實現(xiàn)還是方法的調(diào)用者佛点,都不需要采取任何措施來保障線程安全性問題醇滥,在java中實現(xiàn)不可變對象通常用final修飾, 一旦一個不可變對象被正確構(gòu)建出來(沒有發(fā)生引用逃逸問題超营, 具體可參考final關(guān)鍵字的內(nèi)存可見性)鸳玩, 那么對外部的可見狀態(tài)就永遠不會改變,多線程之中就永遠處于一致狀態(tài)演闭,此種安全性也是最簡單和最純粹的不跟。
-
絕對線程安全
絕對安全的定義是滿足所有之前線程安全的定義的, 但是要做到這點基本上是非趁着觯苛刻的窝革。 Java中的大部分API大部分標榜了線程安全的類基本都不是絕對線程安全的,如果操作不當吕座,還是會出現(xiàn)多線程同時讀寫情況下出現(xiàn)數(shù)據(jù)安全問題虐译。比如Vector類在多線程讀和remove的情況下, 可能就會出現(xiàn)數(shù)組越界的異常吴趴。 以及可以參考Collections.SynchronizedList的迭代器(Iterator)漆诽, 該類原作者還特意標明了該方法需要 Must be manually synched by user (用戶手動同步)。此類方法并不符合調(diào)用方任意操作都能獲取正確結(jié)果的線程安全定義锣枝。
-
相對線程安全
相對線程安全就是我們通常定義的線程安全拴泌,需要保證對這個對象單獨操作是線程安全的,我們在調(diào)用的時候不需要做額外的保護措施惊橱,但是對于一些特定順序的連續(xù)調(diào)用蚪腐,就可能需要在調(diào)用段使用額外的同步手段來保證調(diào)用的正確性。Java中比較常見的線程安全類型基本都屬于這種類型税朴,例如上面舉例的Vector回季,HashTable,以及Collections.synchronized集合方法包裝的集合類正林。
-
線程兼容
線程兼容通常是對象本身并不具備線程安全性泡一,但是通常可以通過一些同步手段來實現(xiàn)線程安全觅廓。我們平常所說的線程非安全鼻忠,基本都是在說著一種情況。Java API絕大多數(shù)類都是線程兼容的例如 ArrayList杈绸,HashMap
-
線程對立
線程對立是指無論調(diào)用段是否采取了同步措施帖蔓, 都無法在多線程環(huán)境中并發(fā)使用的代碼矮瘟,這種代碼通常都是有害的,應(yīng)當盡量避免塑娇。
最典型的例子就是Thread類的suspend()和resume()方法澈侠,如果有兩個線程同時持有線程對象,一個嘗試去終端中斷線程埋酬,一個嘗試去恢復(fù)線程哨啃,如果是并發(fā)進行的話,無論調(diào)用時是否進行了線程同步写妥,目標線程都是存在死鎖風險的拳球。也正是這個原因jdk 廢棄了這兩個方法。
線程安全的實現(xiàn)方法
-
互斥同步(悲觀鎖實現(xiàn))
Synchronized是在學(xué)習Java時候最常見的一種線程安全保障手段珍特,基本原理就是在多個線程并發(fā)訪問共享數(shù)據(jù)是祝峻,保證共享數(shù)據(jù)在同一個時刻只能被一個線程鎖占用。 Synchronized在經(jīng)過編譯之后會形成monitorenter和monitorexit兩個字節(jié)指令次坡。根據(jù)虛擬機規(guī)范的要求,在執(zhí)行monitorenter的時候首先嘗試獲取對象的鎖画畅, 如果這個對象沒有被鎖定砸琅,或者當前線程已經(jīng)獲取到該對象的鎖(鎖重入),把鎖的計數(shù)器加1轴踱,相應(yīng)的momitorexit指令會將鎖計數(shù)器減1症脂,當計數(shù)器為0是,鎖被釋放淫僻。 除此之外诱篷,還有java.util.concurrent包下的ReentrantLock也是互斥鎖的一種實現(xiàn),區(qū)別在于一種是java語義層面的實現(xiàn)雳灵,一種是jvm級別的實現(xiàn)棕所。
-
非阻塞同步(樂觀鎖實現(xiàn))
互斥同步雖然實現(xiàn)了保障線程安全問題,但是在線程阻塞和喚醒的同時也帶來了性能問題悯辙,因此也稱為阻塞同步琳省。隨著指令集的發(fā)展,基于CAS(Compare-and-Swap)實現(xiàn)的非阻塞同步出現(xiàn)了躲撰。 基本思想就是先進行操作针贬,如果沒有其他線程爭搶共享數(shù)據(jù),那么操作就成功了拢蛋。如果產(chǎn)生了沖突桦他,則可以通過補償機制(類似重試, 直到成功),這種同步措施并不需要當前線程掛起谆棱。Java中比較常見的就是java.util.concurrent包下的一些Atomic* 類(原子類)快压。不過圆仔,盡管CAS實現(xiàn)了非阻塞的同步,但是也帶來了一些 比如ABA問題和大量線程空轉(zhuǎn)導(dǎo)致的cpu資源浪費等問題嗓节。
-
非同步方案
要保證線程安全荧缘,并不是一定要進行線程同步,同步只是保證在共享數(shù)據(jù)爭用時數(shù)據(jù)的正確性拦宣,但是如果一個方法本身就不涉及共享數(shù)據(jù)截粗,那么他本身就是線程安全的,就沒有必要對其進行同步鸵隧。常見的兩類如下:
- 可重入代碼
- 線程本地存儲(可參考 ThreadLocal)
Synchronized 優(yōu)化點
jdk1.6之后HotSpot虛擬機開發(fā)團隊話費了大量精力去實現(xiàn)各種鎖優(yōu)化技術(shù)绸罗,如適應(yīng)性自旋鎖,鎖消除豆瘫,鎖粗化珊蟀,輕量級鎖和偏向鎖等。
-
自旋鎖和適應(yīng)性自旋鎖
自旋鎖的實現(xiàn)可以參考前面非阻塞同步的內(nèi)容外驱,當有兩個或以上的線程同時爭搶一個共享數(shù)據(jù)的時候育灸, 我們可以讓后面請求鎖的線程不放棄CPU執(zhí)行時間,通過一種忙循環(huán)的方式實現(xiàn)去等待獲取鎖昵宇。但是我們也知道磅崭,這樣的方式勢必會造成CPU的資源浪費。因此通常自旋的線程等待一定時間后如果還沒有獲取到鎖瓦哎,那么就會用傳統(tǒng)的方式將線程掛起砸喻。 jdk1.6之后還引入了自適應(yīng)的自旋鎖,區(qū)別在與自適應(yīng)的自旋鎖自旋的時間不再是固定的了蒋譬,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態(tài)來定割岛。如果一個鎖對象上,自旋等待剛剛成功獲得鎖犯助,進而會將允許自旋等待持續(xù)相對更長的時間癣漆。 如果對于某個鎖,自旋很少成功獲得過剂买,拿在以后要獲取這個鎖是將可能省略掉自旋的過程扑媚,直接掛起,避免CPU資源的浪費雷恃。
- 注: 自旋鎖主要還是在"重量級"鎖的場景下疆股,通過自旋的方式來減少通常線程掛起導(dǎo)致的性能損耗。
-
鎖消除
鎖消除是指虛擬機在運行時倒槐,對一些代碼上要求同步旬痹,但是被檢測到不可能存在共享數(shù)據(jù)競爭的環(huán)境,此時就會消除原有代碼上標明的同步機制,這個操作叫鎖消除两残。鎖消除的判定依據(jù)主要來源于逃逸分析(可參考占小狼的 淺談HotSpot逃逸分析) 永毅。
-
鎖粗化
原則上來說,我們編碼的時候人弓,總是推薦將同步代碼塊的作用范圍限制得盡量小沼死。但是如果一系列的連續(xù)操作都是對同一個對象反復(fù)進行加鎖和解鎖,甚至加鎖操作是在循環(huán)體中崔赌,那即使沒有線程競爭意蛀,頻繁進行互斥同步操作也會導(dǎo)致不必要的性能損耗。此時虛擬機就會把枷鎖同步的范圍擴展(粗化)到整個操作序列的外部健芭,減少反復(fù)加鎖的性能損耗县钥。
-
偏向鎖和輕量級鎖
關(guān)于偏向鎖和輕量級鎖以及鎖膨脹過程,我們在下一個篇幅繼續(xù)說明慈迈。