如何寫出線程不安全的代碼

什麼是線程安全性

很多時候,我們的代碼,在單線程的環境下是可以運行的非常完美,然而,一旦把代碼放到多線程的環境下去接受蹂躪,結果常常是慘不忍睹的。

《Java並發編程實踐》中,給出了線程安全性的解釋:

A class is thread-safe when it continues to behave correctly when accessed from multiple threads.

當一個類,不斷被多個線程調用,仍能表現出正確的行為時,那它就是線程安全的。

這裡的關鍵在於對「正確的行為」的理解,什麼意思呢?多寫幾個線程不安全的代碼你就明白了。

消失的請求數

假設我們需要給Servlet增加一個統計請求數的功能,於是我們使用了一個long變數作為計數器,並在每次請求時都給這個計數器加一(本文的所有代碼,可到Github下載):

public class UnsafeCountingServlet extends GenericServlet implements Servlet { private long count = 0; public long getCount() { return count; } public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { ++count; // To something else... }}

在單線程的環境下,這份代碼絕對正確,然而,當有多個線程同時訪問時,問題就暴露了。

關鍵就在於++count,它看上去只是一個操作,實際上包含了三個動作:

  1. 讀取count
  2. 將count加一
  3. 將count的值到內存中

這是一個「讀取-修改-寫入」的操作序列,因此假設現在count是9,然後:

  1. 線程A進入service方法,讀到count值是9
  2. 在A修改完count的值但是還沒寫入內存之前,線程B也進入service方法,並且讀取了count值,這時候線程B讀取到的count還是9
  3. 最後,兩個線程都對值為9的count,進行了加一的操作,兩次請求下來,計數器只增加了一次。

顯然,這個類,在多線程的環境下,沒有表現出我們預期的行為,所以稱它為線程不安全

意外懷孕

這一次,我們需要寫一個單例,單例很簡單呀,不就是構造函數私有化么:

public class UnsafeSingleton { private UnsafeSingleton instance = null; public UnsafeSingleton getInstance() { if (instance == null) instance = new UnsafeSingleton(); return instance; }}

如果只有一個線程調用我們的代碼,那這個類,永遠不會生出二胎。但是,放在多線程的環境下,它就可能會意外懷孕了:

  1. 線程A調用getInstance方法,這時候instance是null,進入if代碼塊
  2. 在線程A執行new UnsafeSingleton()之前,線程B先跨一步,執行if判斷,這時候instance還是null,嗯,線程B也進去了
  3. 接下來,兩個線程都會執行new UnsafeSingleton()…悲劇就這樣發生了

預期中的計劃生育失敗,我們再一次寫出了線程不安全的代碼。

考題泄漏

如果說前面兩種破壞方式都太過明顯,很難在代碼review中逃過法眼的話,接下來這種方式,就顯得非常高級了。

public class ThisEscape { private final List<Event> listOfEvents; public ThisEscape(EventSource source) { source.registerListener(new EventListener() { public void onEvent(Event e) { doSomething(e); } }); listOfEvents = new ArrayList<Event>(); } void doSomething(Event e) { listOfEvents.add(e); } interface EventSource { void registerListener(EventListener e); } interface EventListener { void onEvent(Event e); } interface Event { }}

這個類的構造函數接收了一個事件源,在構造函數中,會給事件源添加一個監聽器。咋看之下,你也許不會發現這段代碼有什麼問題,其實這裡面暗藏著NullPointerException:

  1. 線程A將事件源傳入構造函數,並且執行了registerListener的代碼
  2. 在線程A給listOfEvents初始化之前,線程B觸發了事件源,由於線程A已經往事件源註冊了監聽器,因此會執行onEvent函數,也就是doSomething(e);
  3. 而此時listOfEvents還沒被初始化,因此listOfEvents.add(e)報空指針異常

這一切的根源都在於,ThisEscape的構造函數,在ThisEscape還沒實例化完成之前,就把this對象泄漏出去,使得外部可以調用實例對象的方法,這就像還沒開考,就把考題給公布出去了,因此稱之為,考題泄漏。

《Java並發編程實踐》將這種誤把對象發布出去的行為,稱為對象逸出(Escape)。

半成品

對象逸出是指不想發布對象,卻不小心發布了。還有一種是,想發布對象,卻在對象還沒製造好之前,就給了對方使用半成品的機會:

public class StuffIntoPublic { public Holder holder; public void initialize() { holder = new Holder(42); }}public class Holder { private int n; public Holder(int n) { this.n = n; } public void assertSanity() { if (n != n) throw new AssertionError("This statement is false."); }}

很難想像,什麼情況下n != n會成立,並拋出異常。大家可以先參考StackOverflow里的解釋,主要是涉及到Java的指令重排,後面會給大家詳細講解。

小結

這篇文章給大家解釋了什麼是線程安全,並且舉了四個線程不安全的例子來加深大家對線程安全的理解:消失的請求數、意外懷孕、考題泄漏、半成品。這四個例子,分別對應三種常見的線程不安全情形:

  1. 讀取-修改-寫入: 對應上面「消失的請求數」的例子
  2. 先檢查後執行:對應上面「意外懷孕」的例子
  3. 發布未完整構造的對象:對應上面「考題泄漏」和「半成品」兩個例子

絕大多數的線程不安全問題,都可以歸結為這三種情形。而這三種情形,其實又可以再縮減為兩種:對象創建時對象創建後不僅僅是在對象創建後的業務邏輯中要考慮線程的安全性,在對象創建的過程中,也要考慮線程安全

後記

這篇文章里只是解釋了為什麼這些代碼會有線程安全問題,並沒有跟大家說如何對代碼進行修改,使之成為「線程安全」,我會在後面的文章中和大家一起詳細探討。

有人可能會說,線程安全嘛,加同步鎖不就可以啦,其實不然,光光同步鎖,就有很多可以探究的了:

  1. 同步鎖的原理是什麼
  2. 鎖的重入(Reentrancy)是什麼
  3. 同步鎖的本質?

更何況,解決並發問題,也絕對不是加鎖這麼簡單,我們還需要了解:

  1. volatile關鍵字的含義
  2. 指令重排是什麼
  3. 如何安全的發布對象
  4. 如何設計一個線程安全的類

再者,解決了線程安全,我們還需要考慮線程的生命周期管理、線程使用的性能問題等:

  1. 如何取消一個線程
  2. 如何關閉一個有很多線程的服務
  3. 如何設計線程池的大小
  4. ThreadPoolExecutor,Future等Java線程框架的使用
  5. 線程被中斷了如何處理
  6. 線程池資源不夠了,有什麼處理策略
  7. 死鎖的N種情形

乃至我們學習Java並發編程最最初始的問題:

  1. 我們為什麼要學習並發編程
  2. 並發和非同步的關係

這些,都是我新的一年裡要和大家一起分享的,分享的內容主要基於《Java並發編程實踐》里提到的知識,我買了中文版和英文版。這是一本很難啃的書,我會一如既往的用通俗易懂的語言來和大家分享我的學習心得。

參考

  • 《Java並發編程實踐》
  • how-is-listing-3-7-working-in-java-concurrency-in-practice
  • not-thread-safe-object-publishing

推薦閱讀:

非同步調用和單線程,多線程的疑惑?
Qt 多線程串口通信問題?
linux線程是如何進行切換的?
在不改變方法簽名(method signature)的情況下, 請描述這段代碼的問題以及如何解決?
測試線程同步中出現的阻塞問題?

TAG:Java | Java并发编程书籍 | 多线程 |