當我們在談論XX是否線程安全時,我們在談論什麼?

判斷線程安全的關鍵是在何處?


線程安全有不止一種定義,而且互不兼容。

根據《Java Concurrency in Practice》的定義,一個線程安全的 class 應當滿足以下三個條件:

? 多個線程同時訪問時,其表現出正確的行為。

? 無論操作系統如何調度這些線程, 無論這些線程的執行順序如何交織(interleaving)。

? 調用端代碼無須額外的同步或其他協調動作。

依據這個定義,C++ 標準庫里的大多數 class 都不是線程安全的,包括 std::string、std::vector、std::map、std::shared_ptr 等等。而 C 系統庫大多數函數是線程安全的,包括 malloc/free/printf/gettimeofday 等等。gethostbyname 通常不是線程安全,但 FreeBSD 的實現用了 thread local storage,因此是安全的。至於 read/write 同一個 fd 是不是線程安全,按 POSIX 定義是,按程序語義則不一定(因為有可能出現 short read/short write);從實現看,Linux 3.14 之前的 write 不是線程安全,多線程寫有 overlap 的可能,3.14 之後才是安全的。

另外一種定義,同一類型的多個對象能分別被各自所屬的不同線程並發訪問,就算是線程安全的。在這個定義下,C++ 標準庫容器和基本類型都是「線程安全的」。為了與前一種定義區別,這個一般叫做 thread compatible。


@陳碩 說的沒錯,定義互不兼容而且還有很多。不過無論如何我們討論的是racing condition是否存在。相對應的,racing condition也幼兒很多互不兼容的定義,他們跟線程安全的定義是isomorphism。

所以要根據實際情況來選擇一個合適的racing condition的定義,從而討論線程安全。


對「線程安全」問題,我想無非就是這幾個方面:

1. 什麼是『線程安全』?

如果一個對象構造完成後,調用者無需額外的操作,就可以在多線程環境下隨意地使用,並且不發生錯誤,那麼這個對象就是線程安全的。

2. 線程安全的幾種程度

線程安全性的前提:對『線程安全性』的討論必須建立在對象內部存在共享變數這一前提,若對象在多條線程間沒有共享數據,那這個對象一定是線程安全的!

2.1. 絕對的線程安全

上述線程安全性的定義即為絕對線程安全的情況,即:一個對象在構造完之後,調用者無需任何額外的操作,就可以在多線程環境下隨意使用。

絕對的線程安全是一種理想的狀態,若要達到這一狀態,往往需要付出巨大的代價。

通常並不需要達到絕對的線程安全。

2.2. 相對的線程安全

我們通常所說的『線程安全』即為『相對的線程安全』,JDK中標註為線程安全的類通常就是『相對的線程安全』,如:Vector、HashTable、Collections.synchronizedXXX。

對於相對線程安全的類,使用它們時一般不需要使用額外的保障措施,但對於一些特定的使用場景,仍然需要額外的操作來保證線程安全,如:

// 讀線程
Thread t1 = new Thread( new Runnable(){
public void run(){
for(int i=0; i&

vector是一個線程安全的容器,它所提供的方法均為同步方法,但上述代碼仍然會出現線程安全性問題:

若線程1讀了一半的元素後暫停,線程2開始執行,並刪除了所有的元素,然後線程1繼續執行,此時發生角標越界異常!

修改方案:加上額外的同步

// 讀線程
Thread t1 = new Thread( new Runnable(){
public void run(){
synchronized( vector ){
for(int i=0; i&

2.3. 線程對立

線程對立指的是:不論調用者採用何種同步措施,都無法達到線程安全的目的。

如Thread類的suspend、resume方法就是線程對立的方法。

suspend方法會暫停線程,但它不會釋放資源,若resume需要請求到該資源才會被運行的話,系統就會進入死鎖狀態。

3. 實現線程安全的方法

3.1. 互斥同步

同步指的是同一時刻,只有一條線程操作『共享變數』。

實現同步的方式有很多:互斥訪問、CAS操作。

互斥會引起阻塞,當一條線程請求一個已經被另一線程使用的鎖時,就會進入阻塞態;而進入阻塞態會涉及上下文切換。因此,使用互斥來實現同步的開銷是很大的。

互斥同步(阻塞式同步)是一種『悲觀鎖』,即它認為總是存在多條線程競爭資源的情況,因此它不管當前是不是真的有多條線程在競爭共享資源,它總是先上鎖,然後再處理。

Java中有兩種實現互斥同步的方式:synchronized和ReentrantLock。

  • synchronized
    • 編譯器會在synchronized同步塊的開始和結束位置加上monitorenter和monitorexit指令;
    • 這兩個指令需要一個reference類型的參數來指名要鎖定和解鎖的對象;
    • 若同步塊沒有明確指定鎖對象,那麼就使用當前對象或當前類的Class對象;
    • 它是一把可重入的鎖,即:當前線程在已經獲得鎖的情況下,可以再次獲取該鎖,因此不會出現當前線程把自己鎖死的情況;
  • ReentrantLock

    它也是一把可重入的鎖,但比synchronized多如下功能:
    • 等待可中斷:若一條線程長時間佔用鎖不釋放,那被阻塞的線程可以選擇放棄等待,而去做別的事;這對於要處理長時間的同步塊時是很有幫助的。
    • 可實現公平鎖:synchronized是一種非公平鎖,即:被阻塞的線程競爭鎖是隨機的;而公平鎖是根據被阻塞線程先來後到的順序給予鎖。ReentrantLock默認是非公平鎖,可以通過構造函數構造公平鎖。
    • 可以綁定多個條件:synchronized可使用wait/notify來實現等待/通知機制,但一個synchronized同步塊只能使用一次,若要使用多次,就需要嵌套同步塊;但ReentrantLock可以通過newCondition創建多個條件。

synchronized和ReentrantLock如何選擇?

優先選擇synchronized!

JDK1.6已經對synchronized做了很多優化,性能與ReentrantLock相差不大。在條件允許的請況下應優先選擇synchronized。

3.2. 非阻塞同步

它是一種『樂觀鎖』,即它總是認為當前沒有線程使用共享資源,因此它不管當前的狀態,直接操作共享資源,若發現產生了衝突,那麼再採取補償措施(如:CAS的補償措施就是不斷嘗試,直到不發生衝突為止),這種方式線程無需進入阻塞態(掛起態),因此稱為『非阻塞同步』。

JUC中各種整形原子類的自增、自減等操作就使用了CAS。

CAS操作過程:CAS操作存在3個值:共享變數V、預期的舊值A、新值B,若V與A相同,則將V更新成B,否則就不更新,繼續循環比較,直到更新完成為止。

CAS操作可能引發的問題:ABA問題。

若V一開始的值為A,但在準備賦新值的過程中A變成了B,又變成了A,而CAS操作誤認為V沒有被改過。

無同步方案

『阻塞式同步』和『非阻塞式同步』都是同一時刻只讓一條線程處理共享數據,而下面的方案使得多條線程之間不存在共享數據,從而無需同步。

  1. 可重入代碼

    如果一塊代碼段只要輸入的值一樣其結果就一樣的話,這段代碼就叫『可重入代碼』。

    這一類代碼天生具有線程安全性,線程隨意切換結果都一樣。
  2. 線程封閉

    線程封閉:把所有涉及共享變數操作的任務都放在一個線程中運行。

    這樣就不存在多條線程同時處理共享變數了,從而達到了線程安全目的。WEB伺服器採用的就是這種方式,它把每個請求封裝在一條線程中處理,從而不存在線程安全性問題。

  3. 不可變對象

    如果是共享的基本數據類型變數,只要被final修飾,它就是不可變的;

    如果是共享的對象,那就要確保它內部的共享成員變數不會被它的行為所改變。

    PS:保證對象內部共享變數不會被改變的方法有很多,最簡單粗暴的方式就是將所有共享變數用final修飾。

PS:不可變對象一定是線程安全的。


以自己的經驗:

我們平常說的線程安全是指一個函數在被多線程並發時能夠正常工作,一般一個函數都會標識自己是線程安全還是非線程安全,這裡的線程安全是指這個。

還有一種是非同步安全(可重入):它比線程安全要求更加嚴格,不僅要在多線程並發時能夠正常工作,也需要在signal handler(軟中斷)函數中被調用,仍然能夠正常工作。可重入的函數一定是線程安全的函數,但是線程安全的函數並不一定是非同步安全安全的函數。

以上兩種標準在posix標準裡面都有定義。

最後提一種比較少見的線程安全的概念,這個概念在標準中並沒有定義,但是在linux多線程/多進程確實會遇到。

fork-safe: 我們知道linux裡面的fork函數會把父進程的內存資源全部複製,並且複製當前的執行fork函數的線程資源也複製一份,父進程的其它線程資源就不再複製,如果其它線程剛好獲得一個鎖,在沒有解鎖前被fork出來一個子進程,那麼在子進程的鎖就沒有人去解它,子進程獲取該鎖的時候就會永遠等待在這個鎖上。

一般情況下我們不推薦多線程下fork子進程,但是由於某些原因不得不這麼做的時候,就要注意這些fork-unsafe的函數了,但是不幸的是這個概念並不是一個比較明確的概念,一般庫函數都不會明確寫出自己是否是fork safe, 以前低版本glibc裡面的malloc就是一個fork-unsafe的函數,但是高版本的glibc的malloc就是一個fork-safe的函數


簡而言之是多個線程同時操作一個實例時,這個實例中的某些狀態是否會混亂,針對混亂解決辦法之一是讓當前線程獨佔實例,即當前線程操作這個實例的時候,其他線程只能等待不能操作,java中的一些線程安全的類就是通過這個辦法(對應關鍵字synchronized)實現線程安全的。


推薦閱讀:

win8 64位系統 4g內存2.8g可用,為硬體保留的內存1189MB,是筆記本,不是台式機。求解決方法??
15塊錢8條DDR4 2133內存不包好壞你們說值嗎?
為什麼大家經常把手機的外存,內存混為一談?
請問如何給老年人選購筆記本電腦?
DDR3和DDR4內存的區別?

TAG:計算機 | 線程 | 線程安全 |