Java併發編程實戰讀書筆記——第五章 基礎構建模塊

5.1 同步容器類

同步容器類包括Vector和Hashtable,JDK1.2添加了一些功能相似的類,這些同步的封裝器都是由Collections.synchronizedXxx等工廠方法創建的。這些類實現線程安全的方式是:將它們狀態封裝起來,並對每個公有方法都進行同步,使得每次只有一個線程能訪問容器的狀態。

5.1.1 同步容器類的問題

同步容器類都是線程安全的,但在某些情況下可能需要額外的客戶端加鎖來保護複合操作。容器上常見的複合操作包括:迭代(反覆訪問元素,直到遍歷容器中所有元素)、跳轉(根據指定順序找到當前元素的下一個元素)以及條件運算(例如若沒有則添加)。這些複合操作在沒有客戶端加鎖的情況下仍然是線程安全的,但當其他線程併發地修改容器時,它們可能會表現出意料之外的行爲。

getLast和deleteLast,都會執行先檢查再運行操作。getLast可能拋出ArrayIndexOutOfBoundException異常。

客戶端加鎖,通過獲得容器類的鎖,使之成爲原子操作。

迭代方法也可以通過在客戶端加鎖來解決不可行迭代的問題,但要犧牲一些伸縮性。

5.1.2 迭代器與ConcurrentModificationException

無論是直接迭代還是使用for-each循環語法,對容器類進行迭代的標準方式都是使用Iterator。然而,如果有其他線程併發地修改容器,那麼即使使用迭代器也無法避免在迭代期間對容器加鎖。在設計同步容器類的迭代器時並沒有考慮到併發修改的問題,並且它們表現出的行爲是"及時失敗"fail-fast的。這意味着,當它們發現容器在迭代過程中被修改時,就會拋出一個ConcurrentModificationException異常。

及時失敗的迭代器只是捕獲併發錯誤,因此只能作爲問題的預警指示器。它們採用的實現方式是,將計數器的變化與容器關聯起來:如果在迭代期間計數器被修改,那麼hasNext或next將拋出ConcurrentModificationException。然而,這種檢查是在沒有同步的情況下進行的,因此可能會看到失效的計數值,而迭代器可能並沒有意識到已經發生了修改。這是一種設計上的權衡,從而降低併發修改操作的檢測代碼對程序性能帶來的影響。

使用for-each循環語法對List容器進行迭代。從內部來看,java將生成使用Iterator的個代碼,反覆調用hasNext和next來迭代List對象。與Vector一樣,要想避免出現ConcurrentModificationException,就必須在迭代過程持有容器的鎖。

然而,有時候開發人員不希望在迭代期間對容器加鎖。當容器的規模很大的時候,或者在每個元素上執行操作的時候很長,那麼這樣線程等待的時間會很長。而且可能會產生死鎖。即使不存在飢餓或者死鎖的風險,長時間地對容器加鎖也會降低程序的可伸縮性。持有鎖的時間越長,那麼在鎖上的競爭就越激烈,如果等待線程較多,那麼將極大地降低吞量和CPU的利用率。

另一種替代方案是克隆容器,並在副本上進行迭代。由於副本被封閉在線程內,就避免了ConcurrentModificationExcepiton(克隆過程中仍然需要對容器加鎖)。在克隆過程中存在顯著的性能開銷。這種方式的好壞取決於多個因素,包括容器的大小,在每個元素上執行的工作,迭代操作相對於容器其他操作的調用頻率,以及在響應時間和吞吐量方面的需求。

在單線程代碼中也可能拋出ConcurrentModifyException異常。當對象直接從容器中刪除而不是通過Iterator.remove來刪除時,就會拋出這個異常。

5.1.3 隱藏迭代器

加黑的方法會將字符串連接操作轉換爲調用StringBuilder.append(Object),而這個方法又會調用容器的toString()方法,標準容器的toString方法將迭代容器,並在每個元素上調用toString來生成容器內容的格式化表示。

這個方法會拋出ConcurrentModifycationException,真正的問題是HidderIterator不是線程安全的。如果HiddenIterator用synchronizedSet來包裝HashSet,並且對同步代碼進行封裝,那麼就不會發生這種錯誤。

正如封裝對象的狀態有助於維持不變性條件一樣,封閉的同步機制同樣有助於確保實施同步策略。

容器的hashCode和equals等方法也會間接地執行迭代操作,當容器作爲另一個容器的元素或鍵值時,就會出現這種情況。同樣,containsAll、removeAll和retainAll等方法,以及把容器作爲參數的構造函數,都會對容器進行迭代,所以這些間接的迭代操作都可能報出ConcurrentModificationException。

5.2 併發容器

Java5.0提供了多種併發容器類來改進同步容器的性能。同步容器將所有對容器狀態的訪問都串行化,以實現它們的線程安全性。代價是來得降低併發性,當多個線程競爭容器的鎖時,吞吐量將嚴重減低。

另一方面,併發容器是針對多個線程併發訪問設計的。增加了ConcurrentHashMap用來替代同步且基於散列的Map,以及CopyOnWriteArrayList,用於在遍歷操作爲主要操作情況下代替同步的List。在新的ConcurrentMap接口中增加了對一些常見覆合操作的支持,例如"若沒有剛添加"、替換以及有條件刪除等。

通過併發容器來代替同步容器,可以極大地提高伸縮性並降低風險。

Java5.0增加了兩種新的容器類型:Queue和BlockingQueue。Queue用來保存一組等待處理的元素。

ConcurrentLinkedQueue,先進先出隊列。

PriorityQueue,非併發的優先隊列。

Queue上的操作不會阻塞,如果隊列爲空,返回空。然後可以用List來模擬Queue的行爲。Queue是用LinkedList來實現Queue,Queue能去掉List的隨機訪問需求,從而實現更高效的併發。

BlockingQueue擴展了Queue,增加了可阻塞的插入和獲取等操作。隊列爲空,阻塞獲取直到出現一個可用元素,如果有界隊列滿了,那麼獲取元素的操作將一直阻塞,直到隊列中出現可用的空間。在生產者——消費者設計模式中,阻塞隊列是非常有用的。

Java6引入了ConcurrentListMap和ConcurrentSkipListSet,分別作爲同步的SortedMap和SortedSet的併發替代品。例如用synchronizedMap包裝的TreeMap或TreeSet。

5.2.1 ConcurrentHashMap

同步容器類在執行每個操作期間都持有一個鎖。HashMap.get或List.contains可能包含大量的工作,而其他的線程在這段時間都不能訪問該容器。

ConcurrentHash使用分段鎖,任意讀取線程可以併發的訪問Map,執行讀取操作的線程和執行寫入操作的線程可以併發地訪問Map,並且一定數量的寫入線程可以併發地修改Map。ConcurrentHashMap帶來的結果是,在併發訪問環境下將實現更高的吞吐量,而在單線程環境中只損失小的性能。

儘管有這些改進,但仍然有一些需要權衡的因素。對於一些要在整個Map上進行計算的方法,例如size和isEmpty,這些方法的語義被略微減弱了以反映容器的併發特性,估計值。

ConcurrentHashMap中沒有實現對Map加鎖以提供獨佔訪問。在Hashtable和synchronizedMap中,獲得Map的鎖能防止其他線程訪問這個Map。

ConcurrentHashMap代替同步Map能進一步提高代碼的可伸縮性,只有當應用程序需要加鎖Map以進行獨佔訪問時,才應該放棄使用ConcurrentHashMap。

5.2.2 額外的原子Map操作

ConcurrentHashMap不能被加鎖 來執行獨佔訪問,因此我們無法使用客戶端加鎖來創建新的原子操作。但是一些常見的複合操作,例如若沒有則添加,若相等則移除,若相等剛替換等,都已經實現爲原子操作並且在ConcurrentMap接口中聲明。

5.2.3 CopyOnWriteArrayList

CopyOnWriteArrayList用於替代同步List,在某些情況下它提供了更好的併發性能,並且在迭代期間不需要對容器進行加鎖或複製。

寫入時複製(Copy-On-Write)容器的線程安全性在於,只要正確地發佈一個事實不可變的對象,那麼在訪問該對象時就不再需要進一步的同步。每次修改時,都會創建並先更新發佈一個新的容器副本,從而實現可變性。寫入時複製容器的迭代器保留了一個指向底層基礎數組的引用,這個數組當前位於迭代器的起始位置,由於它不會被修改,因此在對其進行同步時只需要確保數組內容的可見性。因此多個線程可以同時對這個容器進行迭代,而不會彼此干擾或者與修改容器線程相互干擾。寫入時複製容器返回的迭代器不會拋出ConcurrentModificationException,並且返回的元素與迭代器創建時的元素完全一致,而不必考慮之後修改操作所帶來的影響。

僅當迭代操作遠遠多於修改操作時,才應該候用"寫入時複製"容器。這個準則很好地描述了許多事件通知系統:在分發通知時需要迭代已註冊監聽器鏈表,並調用每一個監聽器,在大多數情況下,註冊和註銷事件監聽器的操作遠少於接收事件通知的操作。CPJ 2.4.4。

5.3 阻塞隊列和生產者—消費者模式

阻塞隊列提供了可阻塞的put和take方法,以及支持實時的offer和poll方法。如果隊列已經滿了,那麼put方法將阻塞直到有空間可用;如果隊列爲空,那麼take方法會阻塞到直到有元素可用。隊列可以是有界的也可以是無界的,無界隊列永遠都不會充滿 ,因此put永遠不會阻塞。

阻塞隊列支持生產者—消費者這種設計模式,該模式把找出需要完成的工作與執行工作這兩個過程分離開來,並把工作項放到一個"待完成"列表中以便隨後處理,而不是找出後立即處理。它能簡化開發過程,因爲它消除了生產者類和消費者類之間的代碼依賴性,此外,該模式還將生產數據的過程與使用數據的過程解耦開來以簡化工作負載的管理,因爲這兩個過程在處理數據的速率上有所不同。

在基於阻塞隊列構建的生產者—消費者設計中,當數據生成時,生產者把數據放入隊列,而當消費者準備處理數據時,將從隊列中獲取數據。生產者不需要知道消費者的標識和數量,只需要將數據放隊列即可,同樣,消費者也不需要知道生着者是誰。BlockingQueue簡化了生產者—消費者設計的實現過程,它支持任意數量的生產者和消費者。一種最常見的生產者—消費者設計模式就是線程池與工作隊列的組合,在Executor任務執行框架中就體現了這種模式。

阻塞隊列簡化了消費者程序的編碼,因爲take操作會一直阻塞直到有可用的數據。如果生產者不能儘快地產生工作項使消費者保持忙碌,那麼消費者就只能一直等待,直到有工作可做。在某些情況下,例如服務器應用程序中,沒有任何客戶請求服務,而在其他一些情況下,這也表示需要調整生產者線程數量和消費者線程數量之間的比率,從而實現更高的資源利用率。

在構建高可靠的應用程序時,有界隊列是一種強大的資源管理工具:它們能抵制並防止產生過多的工作項,使應用程序在負荷過載的情況下變得更加健壯。

雖然生產者消費者模式能夠將生產者和消費者的代碼彼此解耦開來,但它們的行爲仍然會通過共享工作隊列間接地耦合在一起。開發人員應該儘早地通過阻塞隊列在設計中構建資源管理機制。如果阻塞隊列不能完全符合設計需求,那麼還可以通過信號量(Semaphore)來創建其他的阻塞數據結構。

在類庫中包含了BlockingQueue的多種實現。

其中LinkedBlockingQueue和ArrayBlockingQueue是FIFO隊列,分別與LinkedList和ArrayList的類似,但比同步List有更好的併發性能。PriorityBlockingQueue是一個按優先級排序的隊列,當你希望按照某種順序而不是FIFO來處理元素時,這個隊列將非常有用。正如其它有序容器一樣,它可以根據元素的自然順序來比較元素(如果它們實現了Comparable方法),也可以使用Comparator來比較。

SynchronousQueue,實際上它不是一個真正的隊列,因爲它不會爲隊列中元素維護存儲空間。它維護一組線程,這些線程在等待着把元素加入或移出隊列。如果以洗盤子爲例,就相當於沒有盤架,而是將洗好的盤子直接放入下一個空閒的烘乾機中。這種實現可以直接交付工作,從而降低了數據從生產者到消費者的延遲。而且還會將更多的任務信息反饋給生產者。交付被接受時,它就知道消費者已經得到了任務,而不是簡單地把任務放入了一個隊列。因爲SynchronousQueue沒有存儲功能,因此put和take會一直阻塞,直到有另一個線程已經準備好參與到交付過程中。僅當有足夠多的消費者,並且總是有一個消費者準備好獲取交付的工作時,才適合使用同步隊列。

5.3.1 示例:桌面搜索

代理程序,它將掃描本地驅動器上的文件並建立索引以便隨後進行索引 。

生產者:在某個文件層次結構中搜索符合索引標準的文件,並將它們的名稱放入工作隊列。

消費者:即從隊列中取出文件名稱並對它們建立索引。

將文件遍歷與建立索引等功能分解爲獨立的操作,比將所有功能都放到一個操作中實現有着更高的代碼可讀性和可重用性:每個操作只需完成一個任務,並且阻塞隊列將負責所有的控制流。

性能優勢,生產者和消費者可以併發的執行。如果一個是I/O密集型,一個是CPU密集型,那麼併發執行的吞吐率要高於串行執行的吞吐率。

問題:消費者線程永遠不會退出。

5.3.2 串行線程封閉

在java.util.concurrent中實現的各種阻塞隊列都包含了足夠的內部同步機制,從而安全地將對象從生產者線程發佈到消費者線程。

對於可變對象,生產者——消費者這種設計與阻塞隊列一起,促進了串行線程封閉,從而將對象所有權從生產者交付給消費者。線程封閉對象只能由單個線程擁有,但可以通過安全地發佈對象來轉移所有權。在轉移所有權後,也只有另一個線程能獲得這個對象的訪問權限,並且發佈對象的線程不會再訪問它。這種安全的發佈確保了對象狀態對於新的所有者來說是可見的,並且由於最初的所有者不會訪問它,因此對象將被封閉在新的線程中。新的所有者線程可以對該對象做任意修改,因爲這具有獨佔的訪問權。

對象池利用了串行線程封閉,將對象借給一個請求線程。只要對象池包含足夠的內部同步來安全地發佈池中的對象,並且只要客戶代碼本身不會發布池中的對象,或者在將對象返回給對象池後就不再使用它,那麼就可以安全地在線程之前傳遞所有權。

可能使用其他發佈機制來傳遞可變對象的所有權,但必須只有一個線程能授受被轉移的對象。阻塞隊列簡化了這項工作。除此這外,還可以通過ConcurrentMap的原子方法remove或者AtomicReference的原子方法compareAndSet來完成這項工作。

5.3.3 雙端隊列與工作密取

Java 6增加了兩種容器類型,Deque(發音爲"deck")和BlockingDeque,它們分別對Queue和BlockingQueue進行了擴展。Deque是一個雙端隊列,實現了隊列頭和隊列尾的高效插入和移除。具體實現包括ArrayDeque和LinkedBlockingDeque。

雙端隊列與工作密取(Work Stealing)。在生產者消費者模式中,所有的消費者有一個共同的工作隊列,而在工作密取設計中,每個消費者都有各自的雙端隊列。如果一個消費者完成了自己雙端隊列中的全部工作,那麼它可以從其他消費者雙端隊列末尾祕密地獲取工作。

更高的可伸縮性,因爲工作者線程不會在單個共享隊列上發生競爭。在大多數時候都是訪問自己的雙端隊列,從而極大地減少了競爭。當工作者線程需要訪問另一個隊列時,它會從隊列的尾部而不是從頭部獲取,因此進一步降低了競爭程度。

工作密取非常適用於既是消費者也是生產者的問題—當執行某個工作的時可能導致出再更多的工作。例如,網頁抓蟲程序中處理一個頁面時,通常會發現有更多的頁面需要處理,類似的還有許多搜索圖的算法,例如在垃圾回收階段對堆進行標記,都可以通過工作密取機制來實現高效並行。當一個工作線程找到新的任務單元時,它會將其放到自己的隊列的末尾(或者在工作共享設計模式中,放入其他工作者線程的隊列中)。當雙端隊列爲空時,它會在另一個線程的隊列隊尾查找新的任務,從而確保每個線程都保持忙碌狀態。

5.4 阻塞方法與中斷方法

線程可能會被阻塞或者暫停執行,原因有多種:等待I/O操作結束,等待獲得一個鎖,等待從Thread.sleep方法中醒來,或是等待另一個線程的計算結果。當線程阻塞時,它通常被掛起,並處於某種阻塞狀態(BLOCKED、WAITING或TIME_WAITING)。被阻塞的線程必須等待某個不受它控制的事件發生後才能繼續執行,例如等待I/O操作完成,等待某個鎖可用,或者等待外部計算的結束。當某個外部事件發生時,線程被置回RUNNABLE狀態,並可以再次被調度執行。

BlockingQueue的put和take等方法會拋出受檢查異常InterruptedException,這與類庫中其他一些方法的做法相同,例如Thread.sleep。當某個方法拋出InterruptedException時,表示該方法是一個阻塞方法,如果這個方法被中斷,那麼它將努力提前結束阻塞狀態。

Thread提供了interrupt方法,用於中斷線程或者查詢線程是否已被中斷。每個線程都有一個布爾類型的屬性,表示線程的中斷狀態,當中斷線程時將設置這個狀態。

中斷是一種協作機制。一個線程不能強制其他線程停止正在執行的操作而去執行其他的操作。當線程A中斷B時,A僅僅是要求B在執行到某個可以暫停的地方停止正在執行的操作——前提是如果線程B願意停止下來。雖然在API或者語言規範中沒有爲中斷定義任何特定應用級別的語義,但最常使用中斷的情況就是取消某個操作。方法對中斷請求的響應度越高,就越容易及時取消那些執行時間很長的操作。

當在代碼中調用一個將拋出InterruptedException異常的方法時,你自己的方法也就變成了一個阻塞方法,並且必須要處理對中斷的響應。對於庫代碼來說,有兩種選擇:

傳遞InterruptedEception。避開這個異常通常是最明智的策略——只需要把InterruptedEception傳遞給方法的調用者。傳遞InterruptedException的方法包括,根本不捕獲該異常,或者捕獲該異常,然後在執行某個簡單清理工作後再次拋出這個異常。

恢復中斷。有時候不能拋出InterruptedException,例如當代碼是Runnable的一部分時。在這些情況下,必須捕獲InterruptedException,並通過調用當前線程上的interrupt方法恢復中斷狀態,這樣在調用棧中更高層的代碼將看到引發了一箇中斷。

還可以用一些更復雜的中斷處理方法。

然而在出現InterruptedException時不應該做的事情是,捕獲它但不做出任何響應。這將使用調用棧上更高層的代碼無法對中斷採取處理措施,因爲線程被中斷的證據已經丟失。只有在一種特殊的情況下才能屏蔽中斷,即對Thread進行擴展,並且能控制調用棧上所有更高層的代碼。

5.5同步工具類

阻塞隊列、信號量(Semaphore)、柵欄(Barrier)以及閉鎖(Latch)

所有的同步工具類都包含一些特定的結構化屬性:它們封裝了一些狀態,這些狀態將決定執行同步工具類的線程是繼續執行還是等待,此外還提供了一些方法對狀態進行操作,以及另一些方法高效地等待同步工具類進入到預期狀態。

5.5.1 閉鎖

閉鎖是一種同步工具類,可以延遲線程的進度直到其到達終止狀態。

**閉鎖的作用相當於一扇門:在閉鎖到達結束狀態之前,這扇門一直是關閉的,並且沒有任何線程能通過,當到達結束狀態時,這扇門會打開並允許所有的線程通過。**當閉鎖到達結束狀態後,將不會再改變狀態,因此這扇門將永遠保持打開狀態。**閉鎖可以用來確保某些活動直到其他活動都完成後才繼續執行。**例如:

  1. 確保某個計算在其需要的所有資源都被初始化之後才繼續執行。
  2. **確保某個服務在其他依賴的所有其他服務都已經啓動之後才啓動。**每個服務都有一個相關的二元閉鎖。當啓動服務S時,將首先在S依賴的其他服務的閉鎖上等待,在所有依賴的服務都啓動後會釋放閉鎖S,這樣其他依賴S的服務都能繼續執行。
  3. 等待直到某個操作的所有參與者都就緒再繼續執行。

**CountDownLatch可以使一個或多個線程等待一組事件發生。閉鎖狀態包括一個計數器,該計數器被初始化爲一個正整數,表示需要等待的事件數量。**countDown方法遞減計數器,表示有一個事件已經發生了,而await方法等待計數器達到0,這表示所有需要等待的事件都已經發生。如果計數器的值非零,那麼await會一直阻塞下到計數器爲0,或者等待中的線程中斷,或者等待超時。

閉鎖的兩種常用方法。創建一定數量的線程,利用它們併發的執行任務。它使用兩個閉鎖,分別表示startGate和endGate。起始門計數器的數值爲1,而結束門的計數器的初始值爲工作線程的數量。每個工作線程首先要在啓動門上等待,從而確保所有線程都就緒後纔開始執行。而每個線程最後一件事情是將調用結束門的countDown方法減1,這能使主線程高效地等待直到所有工作線程都執行完成,因此可以統計所消耗的時間。

5.5.2 Future Task

FutureTask也可以用做閉鎖,Future語義表示一種抽象的可生成結果的計算。FutureTask表示的計算是通過Callable來實現的,相當於一種可生成結果的Runnable,並且處於以下3種狀態:等待運行,正在運行和運行完成。執行完成表示計算的所有可能結束方式包括正常結束、由於取消而結束和由於異常而結束等。當FutureTask進入完成狀態後,它會永遠停止在這個狀態上。

Future.get的行爲取決於任務的狀態。如果任務已經完成,那麼get會立即返回結果,否則get將阻塞直到任務進入完成狀態,然後返回結果或者拋出異常。FutureTask將計算結果從執行計算的線程傳遞到獲取這個結果的線程,而FutureTask的規範確保了這種傳遞過程能實現結果的安全發佈。

FutureTask在Executor框架中表示異步任務,可以用來表示一些時間較長的計算。

5.5.3 信號量

計數信號量用來控制同時訪問某個特定資源的操作數量,或者同時執行某個指定操作的數量。某種資源池,或者對容器施加邊界。

Semaphore中管理一組許可permit,可以通過構造函數來指定。在執行操作的時候可以首先獲得許可,並在使用後釋放。如果沒有,acquire將阻塞直到有許可(或者中斷或者超時),release方法返回信號量。

5.5.4 柵欄

Barrier類型於閉鎖,它能阻塞一組線程直到某個事件發生。關鍵區別在於,所有線程必須同時到達柵欄位置,才能繼續執行。閉鎖用於等待事件,而柵欄用於等待其他線程。

CyclicBarrier可以使一定數量的參與方反覆地在柵欄位置彙集,它在並行迭代算法中非常有用:這種算法通常將一個問題拆分成一系列相互獨立的子問題,當線程到達柵欄位置將調用await方法,這個方法將阻塞直到所有線程都到達柵欄位置。如果所有線程都到達了柵欄位置,那麼柵欄打開,此時所有線程都被釋放,而柵欄將被重置以使用下次使用。

CyclicBarrier還可以使你將一個柵欄操作傳遞給構造函數,這是一個Runnable,當成功通過柵欄時(在一個子任務線程中)執行它,但在阻塞線程釋放之前是不執行的。

5.6 構建高效且可伸縮的結果緩存

重用之前的計算結果能降低延遲,提高吞吐量,但卻需要消耗更多的內存。

簡單的緩存可能會將性能瓶頸轉變成可伸縮性瓶頸(線程安全性),即使緩存是用於提升單線程的性能。

  1. 用HashMap,使用synchronize,可伸縮性問題
  2. 用ConcurrentHashMap,會出現重複計算的問題
  3. 用ConcurrentHashMap和FutureTask,更小概率出現計算相同的值
  4. 用ConcurrentMap.putIfAbsent()代替map.put()避免3中的問題

第一部分小結:

可變狀態是至關重要的,所有的併發問題都可以歸結爲如何協調對併發狀態的訪問。可變狀態越少,就越容易確保線程安全性。

儘量將域聲明爲final類型的,除非需要它們是可變的。

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

​ 不可變對象能極大地降低併發編程的複雜性。它們更爲簡單而且安全,可以任意共享而無須使用加鎖或保護性複製等機制。

封裝有助於管理複雜性:

​ 在編寫線程安全的程序時,雖然可以將所有數據保存在全局變量中,但爲什麼要這樣做?將數據封裝在對象中,更易於維持不變性條件:將同步機制封裝在對象中,更易於遵循同步策略。

用鎖來保護每個可變變量。

當保護同一個不變性條件中的所有變量時,要使用同一個鎖。

在執行復合操作期間,要持有鎖。

如果從多個線程中訪問同一個可變變量時沒有同步機制,那麼程序會出現問題。

不要故作聰明地推斷出不需要使用同步。

在設計過程中考慮線程安全,或者在文檔中明確地指出它不是線程安全的。

將同步策略文檔化。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章