要編寫線程安全的代碼,其核心在於要對狀態訪問操做進行管理,特別是對共享(Shared)和可變的(Mutable)狀態的訪問。java
「共享」意味着變量能夠由多個線程同時訪問,而「可變」則意味着變量的值在其生命週期內能夠發生變化。咱們將像討論代碼那樣來討論線程安全性,但更側重於如何防止數據在數據上發生不可控的併發訪問。緩存
當多個線程訪問某個狀態變量而且其中有一個線程執行寫入操做時,必須採用同步機制來協同這些線程對變量的訪問。Java 中的主要同步機制是關鍵字 synchronized ,它提供了一種獨佔的加鎖方式,但「同步」這個術語還包括 volatile 類型的變量,顯式鎖(Explicit Lock)以及原子變量。安全
若是當多個線程訪問同一個可變的狀態變量時,沒有使用合適的同步,那麼程序就會出現錯誤。有三種方式能夠修復這個問題:
若是從一開始就設計一個線程安全的類,那麼比在之後再將這個類修改成線程安全的類要容易的多。網絡
當設計線程安全的類時,良好的面向對象技術、不可修改性,以及明晰的不變性規範都能起到必定的幫助做用。
2.1 什麼是線程安全性 併發
給線程安全性給出一個確切的定義:less
當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些線程將如何交替執行,而且在主調代碼中不須要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼就稱爲這個類時線程安全的。
在線程安全類中封裝了必要的同步機制,所以客戶端無須進一步採起同步措施。
示例:一個無狀態的 Servletide
public class StatelessFactorizer implements Servlet { @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { BigInteger i = extractFromRequest(servletRequest); BigInteger[] factors = factor(i); encodeIntoResponse(servletResponse, factors) } }
與大多數Servlet 相同,StatelessFactorizer 是無狀態的:它既不包含任何域,也不包含任何對其餘類中域的引用。計算過程當中的臨時狀態僅存在於線程棧上的局部變量中,而且只能由正在執行的線程訪問。性能
無狀態對象必定是線程安全的。
2.2 原子性this
假設咱們但願增長一個「命中計數器」來統計所處理的請求數量。例以下代碼atom
public class StatelessFactorizer implements Servlet { 【皺眉臉--不要這麼作】 private long count = 0; public long getCount() { return count; } @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { BigInteger i = extractFromRequest(servletRequest); BigInteger[] factors = factor(i); ++count; encodeIntoResponse(servletResponse, factors) } }
不幸的是 ++count 不是原子操做,這是一個「讀取-修改-寫入」的操做序列。(上一節講的競態條件)。
2.2.1 競態條件
當某個計算的正確性取決於多個線程的交替執行時序時,那麼就會發生競態條件。
最多見的競態條件類型就是「先檢查後執行(Check-Then-Act)」操做,即經過一個可能失效的觀測結果來決定下一步的動做。
2.2.2 實例:延遲初始化中的競態條件
public class LazyInitRace { 【皺眉臉--不要這樣作】 private UnsafeSequence instance = null; public UnsafeSequence getInstance() { if (instance == null) { instance = new UnsafeSequence(); } return instance; } }
存在另外一種競態條件,在「讀取-修改-寫入」這種操做。
2.2.3 複合操做
LazyInitRace 須要以原子方式執行(不可分割)的操做。要避免競態條件問題,就必須在某個線程修改該變量時,經過某種方式防止其餘線程使用這個變量,從而確保其餘線程只能在修改操做完成以前或以後讀取和修改狀態,而不是在修改狀態的過程當中。
假定有兩個操做A 和B ,若是從執行 A 的線程來看,當另外一個線程執行 B 時,要麼將 B 所有執行完,要麼徹底不執行 B,那麼 A 和 B 對彼此來講就是原子的。原子操做是指:對於訪問同一個狀態的全部操做(包括該操做自己)來講,這個操做時一個以原子方式執行的操做。
因此,咱們將介紹加鎖機制,這是Java 中用於確保原子性的內置機制。使用 AtomicLong 類型的變量來統計已處理請求的數量。
public class StatelessFactorizer implements Servlet { private AtomicLong count = new AtomicLong(0); public long getCount() { return count.get(); } @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { BigInteger i = extractFromRequest(servletRequest); BigInteger[] factors = factor(i); count.incrementAndGet(); encodeIntoResponse(servletResponse, factors); } }
在 java.util.concurrent.atomic包中包含了一些原子變量類,用於實如今數值和對象引用上的原子狀態轉換。
2.3 加鎖機制
當在Servlet 中添加一個狀態變量時,能夠經過線程安全的對象來管理 Servlet 的狀態以維護 Servlet 的線程安全性。但若是想在 Servlet 中添加更多的狀態,那麼是否只須要添加更多的線程安全狀態變量就足夠了?
假設咱們但願提高 Servlet 的性能:將最近的計算結果緩存起來。看代碼:
public class UnsafeCachingFactorizer implements Servlet { 【皺眉臉--不要這樣作】 private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>(); private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>(); @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { BigInteger i = extractFromRequest(servletRequest); if (i.equals(lastNumber.get())) { encodeIntoResponse(servletResponse, lastFactors.get()); } else { BigInteger[] factors = factor(i); lastNumber.set(i); //同步1 lastFactors.set(factors); //同步2 encodeIntoResponse(servletResponse, factors); } } }
儘管這裏面的原子引用自己都是線程安全的,可是 仍是存在着競態條件。同步1 和 同步2 不是原子操做,可能存在問題。
要保持狀態的一致性,就須要在單個原子操做中更新全部相關的狀態變量
2.3.1 內置鎖
Java提供了一種內置的鎖機制來支持原子性:同步代碼塊(Synchronized Block)。同步代碼塊包括兩部分:一個做爲鎖的對象引用,一個做爲由這個鎖保護的代碼塊。靜態的synchronized 方法以 Class 對象做爲鎖。
Java 的內置鎖至關於一種互斥鎖,這意味着最多隻有一個線程能持有這種鎖。由這個鎖保護的同步代碼會以原子方式執行,多個線程在執行該代碼塊時也不會相互干擾。
在上一個程序清單裏,若是使用關鍵字 synchronized 來修飾 service 方法,所以在同一個時刻只有一個線程能夠執行 service 方法。然而,這種方法卻過於極端,由於多個客戶端沒法同時使用Servlet ,服務響應性很是低,沒法使人接受。
2.3.2 重入
當某個線程請求一個由其餘線程持有的鎖時,發出請求的線程會阻塞。然而,因爲內置鎖是可重入的,所以若是某個線程試圖得到一個由本身持有的鎖,那麼這個請求就會成功。
「重入」 意味着獲取鎖的操做的粒度是「線程」,而不是「調用」。
下面代碼清單:若是內置鎖不是可重入的,那麼這段代碼將發生死鎖。
public class Widget { public synchronized void doSomething() { ... } } public class LoggingWidget extends Widget { public synchronized void doSomething() { System.out.println(toString() + ": calling doSomething"); super.doSomething(); } }
2.4 用鎖來保護狀態
因爲鎖能使其保護的代碼路徑以串行形式來訪問,所以能夠經過鎖來構造一些協議來實現對共享狀態的獨佔訪問。
對於可能被多個線程同時訪問的可變狀態變量,在訪問它時都須要持有同一個鎖,在這種狀況下,咱們稱狀態變量是由這個鎖保護的。
每一個共享的和可變的變量都應該只由一個鎖來保護,從而使維護人員指導是哪個鎖。
一種常見的加鎖約定是,將全部的可變狀態都封裝在對象內部,並經過對象的內置鎖對全部訪問可變狀態的代碼路徑進行同步,使得在該對象上不會發生併發訪問。
當類的不可變性條件涉及多個狀態變量時,那麼還有另一個需求:在不變性條件中的每一個變量都必須由同一個鎖來保護
若是同步能夠避免競態條件,那麼爲何不在每一個方法聲明時都使用同步呢?事實上,若是不加區別的濫用 synchronized ,可能致使程序中出現過多的同步。
此外,將每一個方法都做爲同步方法還可能致使活躍性問題(Liveness)或性能問題(Performance)。
2.5 活躍性與性能
以前有介紹對整個 service 方法進行同步,雖然這種簡單且粗粒度的方法能確保線程安全性,但付出的代價卻很高。咱們將這種應用程序稱爲不良併發(Poor Concurrent)應用程序:可同時調用的數量,不只受到可用處理資源的限制,還受到應用程序自己結構的限制。
緩存最近執行因數分解的數值 及其計算結果的 Servlet:
public class UnsafeCachingFactorizer implements Servlet { private BigInteger lastNumber; private BigInteger[] lastFactors; private long hits; private long cacheHits; public synchronized long getHits() { return hits;} public synchronized double getCacheHitRatio() { return (double) cacheHits / (double) hits; } @Override public void service(ServletRequest servletRequest, ServletResponse servletResponse) throws ServletException, IOException { BigInteger i = extractFromRequest(servletRequest); BigInteger[] factors = null; synchronized (this) { ++hits; if (i.equals(lastNumber)) { ++cacheHits; factors = lastFactors.clone(); } } if (factors == null) { factors = factor(i); synchronized (this) { lastNumber = i; lastFactors = factors.clone(); } } encodeIntoResponse(servletResponse, factors); } }
在上面的改造後的代碼 實現了在簡單性(對整個方法進行同步) 與併發性(對儘量短的代碼路徑進行同步)之間的平衡。在獲取與釋放鎖等操做上都須要必定的開銷,所以若是將同步代碼塊分解的過細,那麼一般並很差,儘管這樣不會破壞原子性。
一般,在簡單性與性能之間存在着相互制約因素。當實現某個同步策略時,必定不要盲目地爲了性能而犧牲簡單性(着可能會破壞安全性)
不管是執行計算密度的操做,仍是在執行某個可能阻塞的操做,若是持有鎖的時間過長,那麼都會帶來活躍性或性能問題
當執行時間較長的計算或者可能沒法快速完成的操做時(例如:網絡 I/O 或控制檯 I/O),必定不要持有鎖。