天天看點

到底如何保證線程安全,總結得太好了。。

一、線程安全等級

之前的部落格中已有所提及“線程安全”問題,一般我們常說某某類是線程安全的,某某是非線程安全的。其實線程安全并不是一個“非黑即白”單項選擇題。

按照“線程安全”的安全程度由強到弱來排序,我們可以将java語言中各種操作共享的資料分為以下5類:不可變、絕對線程安全、相對線程安全、線程相容和線程對立。

到底如何保證線程安全,總結得太好了。。

1、不可變

在java語言中,不可變的對象一定是線程安全的,無論是對象的方法實作還是方法的調用者,都不需要再采取任何的線程安全保障措施。如final關鍵字修飾的資料不可修改,可靠性最高。

2、絕對線程安全

絕對的線程安全完全滿足Brian GoetZ給出的線程安全的定義,這個定義其實是很嚴格的,一個類要達到“不管運作時環境如何,調用者都不需要任何額外的同步措施”通常需要付出很大的代價。

3、相對線程安全

相對線程安全就是我們通常意義上所講的一個類是“線程安全”的。

它需要保證對這個對象單獨的操作是線程安全的,我們在調用的時候不需要做額外的保障措施,但是對于一些特定順序的連續調用,就可能需要在調用端使用額外的同步手段來保證調用的正确性。

在java語言中,大部分的線程安全類都屬于相對線程安全的,例如Vector、HashTable、Collections的synchronizedCollection()方法保證的集合。

4、線程相容

線程相容就是我們通常意義上所講的一個類不是線程安全的。

線程相容是指對象本身并不是線程安全的,但是可以通過在調用端正确地使用同步手段來保證對象在并發環境下可以安全地使用。Java API中大部分的類都是屬于線程相容的。如與前面的Vector和HashTable相對應的集合類ArrayList和HashMap等。

5、線程對立

線程對立是指無論調用端是否采取了同步錯誤,都無法在多線程環境中并發使用的代碼。由于java語言天生就具有多線程特性,線程對立這種排斥多線程的代碼是很少出現的。

一個線程對立的例子是Thread類的supend()和resume()方法。如果有兩個線程同時持有一個線程對象,一個嘗試去中斷線程,另一個嘗試去恢複線程,如果并發進行的話,無論調用時是否進行了同步,目标線程都有死鎖風險。正是以如此,這兩個方法已經被廢棄啦。

二、線程安全的實作方法

保證線程安全以是否需要同步手段分類,分為同步方案和無需同步方案。

到底如何保證線程安全,總結得太好了。。

1、互斥同步

互斥同步是最常見的一種并發正确性保障手段。同步是指在多線程并發通路共享資料時,保證共享資料在同一時刻隻被一個線程使用(同一時刻,隻有一個線程在操作共享資料)。而互斥是實作同步的一種手段,臨界區、互斥量和信号量都是主要的互斥實作方式。是以,在這4個字裡面,互斥是因,同步是果;互斥是方法,同步是目的。

在java中,最基本的互斥同步手段就是synchronized關鍵字,synchronized關鍵字編譯之後,會在同步塊的前後分别形成monitorenter和monitorexit這兩個位元組碼品質,這兩個位元組碼指令都需要一個reference類型的參數來指明要鎖定和解鎖的對象。

此外,ReentrantLock也是通過互斥來實作同步。在基本用法上,ReentrantLock與synchronized很相似,他們都具備一樣的線程重入特性。

互斥同步最主要的問題就是進行線程阻塞和喚醒所帶來的性能問題,是以這種同步也成為阻塞同步。從處理問題的方式上說,互斥同步屬于一種悲觀的并發政策,總是認為隻要不去做正确地同步措施(例如加鎖),那就肯定會出現問題,無論共享資料是否真的會出現競争,它都要進行加鎖。

2、非阻塞同步

随着硬體指令集的發展,出現了基于沖突檢測的樂觀并發政策,通俗地說,就是先進行操作,如果沒有其他線程争用共享資料,那操作就成功了;如果共享資料有争用,産生了沖突,那就再采用其他的補償措施。(最常見的補償錯誤就是不斷地重試,直到成功為止),這種樂觀的并發政策的許多實作都不需要把線程挂起,是以這種同步操作稱為非阻塞同步。

非阻塞的實作CAS(compareandswap):CAS指令需要有3個操作數,分别是記憶體位址(在java中了解為變量的記憶體位址,用V表示)、舊的預期值(用A表示)和新值(用B表示)。CAS指令執行時,CAS指令指令時,當且僅當V處的值符合舊預期值A時,處理器用B更新V處的值,否則它就不執行更新,但是無論是否更新了V處的值,都會傳回V的舊值,上述的處理過程是一個原子操作。

CAS缺點:

ABA問題:因為CAS需要在操作值的時候檢查下值有沒有發生變化,如果沒有發生變化則更新,但是一個值原來是A,變成了B,又變成了A,那麼使用CAS進行檢查時會發現它的值沒有發生變化,但是實際上卻變化了。

ABA問題的解決思路就是使用版本号。在變量前面追加版本号,每次變量更新的時候把版本号加一,那麼A-B-A就變成了1A-2B-3C。JDK的atomic包裡提供了一個類AtomicStampedReference來解決ABA問題。這個類的compareAndSet方法作用是首先檢查目前引用是否等于預期引用,并且目前标志是否等于預期标志,如果全部相等,則以原子方式将該引用和該标志的值設定為給定的更新值。

3、無需同步方案

要保證線程安全,并不是一定就要進行同步,兩者沒有因果關系。同步隻是保證共享資料争用時的正确性的手段,如果一個方法本來就不涉及共享資料,那它自然就無需任何同步操作去保證正确性,是以會有一些代碼天生就是線程安全的。

1)可重入代碼

可重入代碼(ReentrantCode)也稱為純代碼(Pure Code),可以在代碼執行的任何時刻中斷它,轉而去執行另外一段代碼,而在控制權傳回後,原來的程式不會出現任何錯誤。所有的可重入代碼都是線程安全的,但是并非所有的線程安全的代碼都是可重入的。

可重入代碼的特點是不依賴存儲在堆上的資料和公用的系統資源、用到的狀态量都是由參數中傳入、不調用 非可重入的方法等。

(類比:synchronized擁有鎖重入的功能,也就是在使用synchronized時,當一個線程得到一個對象鎖後,再次請求此對象鎖時時可以再次得到該對象的鎖)

2)線程本地存儲

如果一段代碼中所需的資料必須與其他代碼共享,那就看看這些共享資料的代碼是否能保證在同一個線程中執行?如果能保證,我們就可以把共享資料的可見範圍限制在同一個線程之内。這樣無需同步也能保證線程之間不出現資料的争用問題。

符合這種特點的應用并不少見,大部分使用消費隊列的架構模式(如“生産者-消費者”模式)都會将産品的消費過程盡量在一個線程中消費完。其中最重要的一個應用執行個體就是經典的Web互動模型中的“一個請求對應一個伺服器線程(Thread-per-Request)”的處理方式,這種處理方式的廣泛應用使得很多Web伺服器應用都可以使用線程本地存儲來解決線程安全問題。

原文連結:

https://blog.csdn.net/qq_26545305/article/details/79516610/

版權聲明:本文為CSDN部落客「LemmonTreelss」的原創文章,遵循CC 4.0 BY-SA版權協定,轉載請附上原文出處連結及本聲明。