031.[轉] 從類狀態看Java多線程安全併發

從類狀態看Java多線程安全併發


對於Java開發人員來講,i++的併發不安全是人所共知,可是它真的有那麼不安全麼?html

在開發Java代碼時,如何可以避免多線程併發出現的安全問題?這是全部Java程序員都會面臨的問題。本文講述了在開發Java代碼時安全併發設計所須要考慮的點,文中以一張圖展開,圍繞着Java類狀態,討論各類狀況下的併發安全問題。當理解了Java類的各類變量狀態在併發狀況下的表現,在寫Java多線程代碼時就能夠作到心中有數,遊刃有餘,寫出更加安全、健壯、靈活的多線程併發代碼。java

目錄git

  • 1. 多線程併發簡介
  • 2. 從類狀態看Java安全併發
  • 3. Java安全併發分解
    • 3.1 無狀態類
    • 3.2 有狀態類
    • 3.3 私有狀態類
    • 3.4 共享狀態類
    • 3.5 不可變狀態類(常量狀態)
    • 3.6 可變狀態類
    • 3.7 非阻塞設計
    • 3.8 阻塞設計
      • 3.8.1 資源死鎖(resource deadlock)
      • 3.8.2 鎖順序死鎖(lock-ordering deadlock)
      • 3.8.3 狀態公開
  • 4. 類的靜態狀態
  • 5. 類外部狀態和多線程安全併發
  • 6. 小結
  • 7. 演示代碼
  • 8. 參考資料

1. 多線程併發簡介

在現代操做系統中,CPU的調度都是以線程爲基本單位,各個線程各自獨立獲取到CPU時間片並執行代碼指令,這就是多線程併發。於此同時,同一進程中的全部線程將共享當前進程的內存地址空間,這些線程能夠訪問當前內存地址空間上的同一個變量。若一個線程在使用某個變量時,另外一個線程對這個變量進行修改,將形成不可預測的結果,這也是多線程併發問題。程序員

一個簡單的例子是,當一個線程循環讀取一個數組時,另一個線程對這個數組內對象進行刪除,則前面一個線程可能讀取失敗或讀取的是髒數據。算法

在多線程併發中,若一段代碼的執行不能按預期正確地進行,或者執行的最終結果不可預測,則咱們說這段代碼併發不安全。換句話說,若線程之間可以按照預期執行代碼,操做數據並獲取到指望的結果,則實現了安全的併發編程

2. 從類狀態看Java安全併發

類狀態是指類中所聲明的變量,不管是公有變量、私有變量,亦或static和final修飾的變量,都是不一樣形式的類狀態。按照Java語法,類變量有以下各類形式,數組

  • 公有變量(public)、私有變量(private)、保護變量(protect)
  • 靜態變量(static)
  • 不可變變量(final)
  • 外部變量、內部變量、局部變量

這些類變量在運行時刻,映射到JVM內存中各類對象。Java安全併發設計,其核心在於如何處理這些變量在併發中的表現,掌握它們的特性是Java安全併發設計的關鍵。安全

下圖從類狀態出發,簡要的說明了Java類變量的各類狀態形式,及其相關的併發安全性,多線程

Java安全併發設計

其中,併發

  • 綠色方塊說明多線程併發安全。
  • 桔紅色方塊說明多線程併發不安全,會出現問題。
  • 圖中的Java類是指徹底依據面向對象設計,即:類成員變量被聲明爲私有,類方法只對類內部成員變量進行操做。
  • 有狀態是指Java類中有成員變量聲明,不管是公有、私有仍是保護變量,亦或static和final;無狀態則指類中無任何成員變量聲明。
  • 私有狀態是指類成員變量經過ThreadLocal進行了線程隔離,實現了按線程進行變量的分配;而共享狀態則指類變量能夠被多線程訪問。
  • 不可變狀態是指類成員變量被聲明爲final,是一種常量狀態。
  • 靜態狀態是指類成員變量被聲明爲static。
  • 阻塞是指線程在執行代碼前,必須獲取鎖,這個鎖只有一個,經過鎖實現了代碼的多線程串行執行。

須要注意的是,該圖是以Java語言爲例來講明如何設計併發安全的對象類,但實踐中,圖中所涉及的狀態、私有狀態、不可變狀態、非阻塞和阻塞訪問,這些概念也應該適用於更多面對對象的編程語言。

下面將對上圖中各個類狀態進行一一講解,介紹各個狀態下併發設計的要點。

3. Java安全併發分解

3.1 無狀態類

一個無狀態類是指其沒有任何聲明的成員變量,例如,

public class StatelessClass { public void increment() { int i = 0; String msg = String.valueOf(i++); log.info(msg); } } 

無狀態類是線程安全的。上述類中的increment()方法中,有兩個本地變量i和msg,這兩個本地變量都在方法棧空間上分配,因爲棧內存空間是按線程各自獨立的,相互隔離,所以棧空間上的變量是線程安全的。

由此還能夠知道,在方法調用中分配的變量和對象,若在棧退出後變量或對象引用被JVM釋放(不會被外部再訪問到),則這個變量和對象也是線程安全的。關於本地變量和JVM棧空間的更多介紹,能夠參考這篇文章

3.2 有狀態類

和無狀態相反,有狀態類是指類中有聲明的成員變量,例如

public class StatefulClass { private int i=0; public void increment() { i++; } } 

上面的類聲明瞭一個int i的類變量,並初始化爲0。大多數狀況下Java類都是屬於有狀態類。

有狀態是致使線程不安全的必要條件,但它不是充分條件,請繼續看下文。

3.3 私有狀態類

若Java類的狀態經過ThreadLocal等方法,使得狀態被隔離在各個線程中,相互不干擾,例如,

public class PrivateStateClass { private ThreadLocal<Integer> i = new ThreadLocal<>(); public void set(int i) { i.set(i); } public void increment() { Integer value = i.get(); i.set(value + 1); } } 

上面的類聲明瞭一個ThreadLocal i的變量,這個類狀態按各個線程進行了隔離,爲一種私有狀態,在執行increment()方法時能夠被多線程安全訪問。

3.4 共享狀態類

正常的Java成員變量是線程共享的,即多個線程經過Java類提供的類方法訪問類對象時,類對象中的成員變量能夠被共享訪問到,這是大多數狀況下的應用場景。

共享狀態在多線程併發時,不必定就是不安全,其又能夠分爲常量狀態和可變狀態兩種狀況來討論,請見下文。

3.5 不可變狀態類(常量狀態)

下面的Java類中,有一個Integer PI變量被聲明爲final,這說明這個變量是一個常量對象,初始化以後再也不改變。

public class FinalStateClass { private final Integer PI = 3.14; public double calculate(double radius) { return PI*radius*radius; } } 

多線程訪問上述的calculate()方法是線程安全的。

final聲明使得變量變爲常量狀態,多線程在訪問時不能更改狀態,在必定程度上實現了只讀,從而是線程安全的。

3.6 可變狀態類

對於可變的共享狀態,當多線程訪問時,必然出現協同操做和同步問題,若代碼設計不當,則很容易出現線程不安全問題。

對於可變共享狀態的訪問,是多線程併發設計時的考慮重點。爲了實現線程安全,通常經過下面兩種方法,

  • 非阻塞設計(多線程並行執行,經過算法實現線程安全)
  • 阻塞設計(加鎖,使得多線程實現串行執行)

下面是這兩種方法的簡單比較,

  非阻塞設計 阻塞設計
多線程執行 並行執行 串行執行
安全實現方法 經過算法設計 經過鎖
吞吐性能
優勢 無死鎖,線程不會被阻塞掛起 經過鎖能夠實現可控的線程調度
缺點 算法實現複雜,在高度競爭狀況下,吞吐性能會低於鎖 線程的掛起和上下文切換、死鎖

更詳細的討論見下文。

3.7 非阻塞設計

下面的Java類經過原子變量AtomicInteger實現非阻塞的自增算法。

public class AtomicStateClass { private AtomicInteger i = new AtomicInteger(0); public void increment() { i.incrementAndGet(); } } 

能夠看到increment()方法沒有添加任何鎖,可是它能夠實現多線程的安全自增操做。AtomicInteger其原理是經過CAS算法,即compareAndSet()方法,先查看變量是否變化,若沒有變化則設置值,如有變化,則從新嘗試,在絕大數狀況下,值的設置在第一次嘗試就成功。

更多非阻塞算法設計,好比非阻塞的棧、非阻塞的鏈表插入操做,見這裏

3.8 阻塞設計

阻塞是指經過鎖來控制線程對類狀態的訪問,使得當前狀態只能由一個線程訪問,其它訪問線程則掛起等待,一直等到鎖被釋放後,全部的等待線程競爭鎖,得到下一次訪問權。

鎖的設計,使得線程各自之間實現同步,串行執行代碼指令,避免了競爭狀態。可是於此同時,它也帶來了死鎖的困擾。若兩個線程之間相互持有對方須要的資源或鎖,則進入死鎖狀態。

JVM在解決死鎖上沒有提供較好的辦法機制,更多的是提供監控工具來查看。對於死鎖問題,最終解決方案是依賴開發者實現的代碼,增長更多的資源,減小鎖的碰撞,實現鎖的有序持有和不定時釋放,都是避免死鎖的有效方案。

3.8.1 資源死鎖(RESOURCE DEADLOCK)

資源死鎖是一種普遍的死鎖定義,簡單例子是,一個打印任務須要得到打印機和文件對象,若一個線程得到了打印機,而另一個線程得到了文件對象,相互都不釋放得到的資源,則出現資源死鎖狀況。

增長更多的資源,是解決此類死鎖的有效方案。

3.8.2 鎖順序死鎖(LOCK-ORDERING DEADLOCK)

下面是一個鎖順序死鎖的演示代碼,

public class LockOrderingDeadLock { public void transferMoney(Account from, Account to, Integer amount) { synchronized (from) { synchronized (to) { from.debit(amount); to.credit(amount); } } } } 

若同時啓動兩個線程,分別執行下面兩個操做,

  • 線程1:transferMoney(accountA, accountB, 100)
  • 線程2:transferMoney(accountB, accountA, 100)

則頗有可能出現死鎖狀態,由於線程1在握有accountA對象鎖的同時,線程2也握有accountB的鎖。下面是對transferMoney方法測試過程當中,經過JConsole觀察到的死鎖狀況,

死鎖2
圖1:pool-1-thread-5握有account@120f74e3的鎖,等待account@3e9369b9的鎖

死鎖1
圖2:pool-1-thread-8握有account@3e9369b9的鎖,等待account@120f74e3的鎖

解決辦法之一,是實現鎖的按序持有,即對於任何兩個對象鎖A和B,先進行排序(排序算法必須是穩定有序),不管是哪一個線程,都必須按照鎖的排序,依次獲取,從而避免相互持有對方須要的鎖。

3.8.3 狀態公開

狀態公開是指類成員變量被公開,在必定程度上破壞了面向對象設計的數據封裝性。對類方法再好的阻塞設計,一旦狀態被公開,其併發安全性都會功虧一簣

見下面的例子,類中定義了一個personList的對象,方法insert()和iterate()經過synchronized進行了阻塞加鎖,其只能運行一個線程進入類方法執行操做。

public class PublicStateClass { public ArrayList<String> personList = new ArrayList<>(); public synchronized void insert(String person) { personList.add(person); } public synchronized void iterate() { Integer size = personList.size(); for (int i = 0; i < size; i++) { System.out.println(personList.get(i)); } } } 

但多線程訪問insert()和iterate()方法時,並不必定線程安全,主要緣由是personList被聲明瞭公開對象,使得類以外的線程能夠輕易地訪問到personList變量,從而致使personList的狀態不一致,在iterate整個person列表時,可能列表中的對象已被刪除。

這是類狀態公開致使的線程安全問題,究其緣由,還要歸結於沒有作好類的面對對象設計,對外部沒有隱藏好數據。

下面的getList方法返回也會致使一樣的問題,

public class PublicStateClass { private ArrayList<String> personList = new ArrayList<>(); public List getList() { return personList; } } 

對於這樣的問題,推薦的作法是,成員變量聲明爲私有,在執行讀操做時,對外克隆一份數據副本,從而保證類內部數據對象不被泄露,

public class PublicStateClass { private ArrayList<String> personList = new ArrayList<>(); public List getList() { return (List) personList.clone(); } } 

4. 類的靜態狀態

類的靜態狀態是指類中被static聲明的成員變量,這個狀態會在類初次加載時初始化,被全部的類對象所共享。Java程序員對這個static關鍵字應該不會陌生,其使用的場景仍是很是普遍,好比一些常量數據,因爲沒有必要在每一個Java對象中存儲一份,爲了節省內存空間,不少時候聲明爲static變量。

但static變量併發不安全,從面向對象設計來講,一旦變量聲明爲靜態,則做用空間擴大到整個類域,若被聲明爲公共變量,則成爲全局性的變量,static的變量聲明大大破壞了類的狀態封裝。

爲了使靜態變量變得多線程併發安全,final聲明是它的「咖啡伴侶」。在阿里巴巴的編碼規範中,其中一條是,如果static成員變量,必須考慮是否爲final

5. 類外部狀態和多線程安全併發

上文在講併發設計時,都是針對類內部狀態,即類內部成員變量被聲明爲私有,類方法只對類內部變量進行操做,這是一種簡化的應用場景,針對的是依據徹底面向對象設計的Java類。一種更常見的狀況是,類方法須要對外部傳入的對象進行操做。這個時候,類的併發設計則和外部狀態息息相關。

例如,

public class StatelessClass { public void iterate(List<Person> personList) { Integer size = personList.size(); for (int i = 0; i < size; i++) { System.out.println(personList.get(i)); } } } 

上面的類是一個無狀態類,裏面沒有任何聲明的變量。可是iterate方法接受一個personList的列表對象,由外部傳入,personList是一個外部狀態

外部狀態相似上文中內部狀態公開,不管在類方法上作如何的參數定義(使用ThreadLocal/final進行聲明定義),作如何併發安全措施(加鎖,使用非阻塞設計),類方法其對狀態的操做都是不安全的。外部狀態的安全性取決於外部的併發設計。

一個簡單的處理方法,在調用類方法的地方,傳入一個外部狀態的副本,隔離內外部數據的關聯性。

6. 小結

類狀態的併發,本質上是內存共享數據對象的多線程訪問問題。只有對代碼中各個Java對象變量的狀態特性掌握透徹,寫起併發代碼時將事倍功半。

下面的類中,整個hasPosition()方法被synchronized修飾,

public class UserLocator { private final Map<String, String> userLocations = new HashMap<>(); public synchronized boolean hasPositioned(String name, String position) { String key = String.format("%s.location", name); String location = userLocations.get(key); return location != null && position.equals(location); } } 

但仔細查看能夠知道外部變量name和position、內部變量key和location都是併發安全,只有userLocations這個變量存在併發風險,須要加鎖保護。所以,將上面的方法進行以下調整,將減小鎖的粒度,有效提升併發效率。

public class UserLocator { private final Map<String, String> userLocations = new HashMap<>(); public boolean hasPositioned(String name, String position) { String key = String.format("%s.location", name); String location; synchronized (this) { location = userLocations.get(key); } return location != null && position.equals(location); } } 

因而可知,瞭解類中各個變量特性對寫好併發安全代碼的重要性。在這個基礎上,優化鎖的做用範圍,減小鎖的粒度,實現鎖分段,均可以作到信手拈來,遊刃有餘。

關於類狀態,說了這麼多,最後給一個全文性總結:面向對象進行類設計,隱藏好數據,控制好類的狀態,從嚴控制變量的訪問範圍,能private儘可能private,能final儘可能final,這些都將有助於提升代碼的併發健壯性。

7. 演示代碼

全部的演示代碼在以下的代碼倉庫中,

8. 參考資料

  1. 《Java併發編程實戰》 [美] Brian Goetz 等 著,童雲蘭 等 譯,ISBN:9787111370048。
  2. IBM DeveloperWorks:非阻塞算法簡介
相關文章
相關標籤/搜索