什麼是線程安全性:java
要編寫線程安全的代碼,其核心在於要對狀態訪問操做進行管理,特別是對共享的和可變的狀態的訪問。「共享」意味着變量能夠由多個線程同時訪問,而「可變」則意味着變量的值在其生命週期內能夠發生變化。
編程
原文出處:http://liuxp0827.blog.51cto.com/5013343/1412874
緩存
一個對象是否須要線程安全的,取決於他是否被多個線程訪問。這指的是在程序中訪問對象的方式,而不是對象要實現的功能。要使得對象時線程安全的,須要採用同步機制來協同對對象可變狀態的訪問。若是沒法實現協同,那麼可能致使數據破壞以及其餘不應出現的結果。
安全
若是當多個線程訪問同一個可變的狀態變量時沒有使用合適的同步,那麼程序就會出現錯誤。有三種方式能夠修復這個問題:1.不在線程之間共享該狀態變量;2.將狀態變量修改成不可變的變量;3.在訪問狀態變量時使用同步。多線程
在線程安全性的定義中,最核心的概念就是正確性。正確性的含義是,某個類的行爲與其規範徹底一致。當多個線程訪問某個類時,無論運行時環境採用何種調度方式或者這些線程將如何交替執行,而且在主調代碼中不須要任何額外的同步或協同,這個類都能表現出正確的行爲,那麼稱這個類是線程安全的。
併發
來看一個基於Servlet的因數分解服務,並逐漸擴展它的功能,同時確保它的線程安全性。{}less
//原文出處:http://liuxp0827.blog.51cto.com/5013343/1412874 @ThreadSafe public class StatelessFactorizer implements Servlet { public void service (ServletRequest req,ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); encodeIntoResponse(resp,factors); } }
與大多數Servlet同樣,StatelessFactorizer是無狀態的,它不包含任何域,也不包含任何對其餘類中域的引用。計算過程當中的臨時狀態僅存在於線程棧上的局部變量中,並只能有正在執行的線程訪問。所以無狀態對象必定是線程安全的。ide
原子性性能
假設咱們但願增長一個「命中計數器」來統計所處理得請求數量。一種直觀的方法是在Servlet中增長一個long類型的域,而且每處理一個請求就將這個值加1:學習
//原文出處:http://liuxp0827.blog.51cto.com/5013343/1412874 @NotThreadSafe //很差的代碼 public class UnsafeCountingFactorizer implements Servlet { private long counts = 0; public long getCounts(){return counts;} public void service (ServletRequest req,ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); ++counts; encodeIntoResponse(resp,factors); } }
UnsafeCountingFactorizer是非線程安全的。雖然遞增造做++counts是一種緊湊的語法,使其看上去只是一個操做,但這個操做並不是原子的,於是他並不會做爲一個不可分割的操做來執行。它包含了三個獨立的操做「讀取counts ——> 修改counts+1 ——> 寫入counts」。 假設計數器的初始值爲9,那麼在某些狀況下,每一個線程督導的值都是9,接着執行遞增操做,而且都將計數器的值設爲10,那麼命中計數器的值將會誤差1.這種由不恰當的執行時序而出現的不正確的結果是一種競態條件。
當某個計算的正確性取決於多個線程的交替執行時序時,那麼就會發生競態條件。最多見的競態條件類型就是「先檢查後執行」操做:經過一個可能失效的觀測結果決定下一步的動做。
「先檢查後執行」的一種常見狀況就是延遲初始化,即將對象的初始化操做推遲到實際被使用時才進行,同時要確保只備初始化一次。
//原文出處:http://liuxp0827.blog.51cto.com/5013343/1412874 @NotThreadSafe public class LazyInitRace { private ExpensiveObject instance = null; public ExpensiveObject getInstance() { if(instance == null) instance = new ExpensiveObject(); return instance; } }
在LazyInitRace中包含了一個競態條件,它可能會破壞這個類的正確性。
LazyInitRace和UnsafeCountingFactorizer都包含一組須要以原子方式執行的操做。要避免競態條件問題,就必須在某個線程修改該變量時,經過某種方式防止其餘線程使用這個變量,從而確保其餘線程只能在修改操做完成以前或以後讀取和修改狀態,而不是在修改狀態的過程當中。
若是UnsafeCountingFactorizer的遞增操做是原子操做,那麼競態條件就不會發生。爲了確保線程安全性,「先檢查後執行」和「讀取-修改-寫入」等操做必須是原子的。咱們把「先檢查後執行」和「讀取-修改-寫入」等操做統稱爲複合操做:包含了一組必須以原子方式執行的操做以確保線程安全性。
//原文出處:http://liuxp0827.blog.51cto.com/5013343/1412874 @ThreadSafe public class CountingFactorizer implements Servlet { private final AtomicLong counts = new AtomicLong(0); public long getCounts(){return counts.get();} public void service (ServletRequest req,ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); counts.incrementAndGet(); encodeIntoResponse(resp,factors); } }
在實際狀況下,應儘量地使用現有的線程安全對象來管理類的狀態。與非線程安全的對象相比,判斷線程安全對象的可能狀態及其狀態轉換狀況要更爲容易,從而也更容易維護和驗證線程安全性。
加鎖機制
當在Servlet中添加狀態變量時,能夠經過線程安全的對象來管理Servlet的狀態以維護Servlet的線程安全性。但若是想在Servlet中添加更多的狀態,只添加更多線程安全狀態變量是不夠的。咱們但願提高Servlet的性能:將最近的計算結果緩存起來,dangliangge 連續的請求對相同的數值進行因數分解時,能夠直接使用上一次的結果,而無需從新計算。這時,須要保存兩個狀態:最近執行因數分解的數值,以及分解結果。前面經過AtomicLong以線程安全的方式來管理計數器狀態,是否可使用相似的AtomicReference來管理最近執行因數分解的數值以及分解結果?
//原文出處:http://liuxp0827.blog.51cto.com/5013343/1412874 @NotThreadSafe public class UnsafeFactorizer implements Servlet { private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>(); private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>(); public void service(ServletRequest req,ServletResponse resp) { BigInteger i = extractFromRequest(req); if(i.equals(lastNumber.get())) encodeIntoResponse(resp,lastNumber.get()); else{ BigInteger[] factors = factor(i); lastNumber.set(i); lastFactors.set(factors); encodeIntoResponse(resp,factors); } }
儘管這些原子引用自己都是線程安全的,但在UnsafeCachingFactorizer中存在着競態條件,這可能產生錯誤的結果。
在線程安全性的定義中要求,多個線程之間的操做不管採用何種執行時序或交替方式,都要保證不變性不被破壞。UnsafeCachingFactorizer的不變性條件之一是:在lastFactors中的緩存的因數之積應該等於在lastNumber中的緩存值。當在不變性條件中涉及多個變量時,各個變量之間並非彼此獨立的,而是某個變量的值會對其餘變量的值產生約束。所以,當更新某一個變量時,須要在同一個原子操做中對其餘變量同時進行更新。
在上述代碼中,儘管set方法的每次調用都是原子的,但仍然沒法同時更新lastNumber和lastFactors。若是隻修改了其中一個變量,那麼在這兩次修改操做之間,其餘線程將發現不變性條件被破壞了。
因此,要保持狀態的一致性,就須要在單原子操做中更新全部相關的狀態變量。
未完待續... Java併發編程學習筆記(二)線程安全性 2