基礎知識
線程或者鎖在並發變成在並發編程中的作用,類似於鉚釘和工字梁在土木工程中的作用。
java中主要的同步機制是關鍵字synchroinzed,它提供了一種獨占的加鎖方式,但“同步”這個術語還包括voliatile類型的變量,顯式鎖(Explicit Lock)以及原子變量。
當多個線程訪問某個類時,這個類始終都能表現出正確的行為,那麽就稱這個類是線程安全的
一個無狀態的Servlet:
@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是無狀態的:它既不包含任何域,也不包含任何對其他類中域的引用。計算過程中的臨時狀態僅存在於線程棧中的局部變量中,並且只能由正在執行的線程訪問。訪問StatelessFactorizer的線程不會影響另一個訪問同一個StatelessFactorizer的線程計算結果,因為這兩個線程並沒有共享狀態,好像它們都在訪問不同的實例。
無狀態對象一定是線程安全的。
原子性
往無狀態對象中增加一個狀態
@NotThreadSafe public class UnsafeCountingFactorizer implements Servlet{ private long count = 0; public long getCount(){return count;} public void service(ServletRequest req, ServletResponse resp){ BigInteger i = extractFromRequest(req); BigInteger[] FACTORS = factor(i); ++count; encodeIntoResponse(resp, factors); } }
UnsafeCountingFactorizer並非線程安全的,盡管它在單線程環境中能正確運行。雖然遞增操作++是一種緊湊的語法,使其看起來是一個操作,但是這個操作是並非原子的,因而它並不會作為一個不可分割的操作來執行。實際上,它包含了三個獨立的操作:讀取count的值,將值 + 1,然後將計算結果寫入count。
你可能認為,基於web的服務中,命中計數器的少量偏差或許是可以接受的,一般情況下是可以的,但是被用作序列等標誌的時候,可能就有影響了。並發變成中,這種由於不恰當的執行時序出現不正確的情況是一種非常重要的情況,它有一個正式的名字:競態條件(Race Condition)
競態條件Race Condition
當某個計算的正確性取決於多個線程的交替執行時序時,那麽就會發生競態條件。最常見的競態條件類型就是“先檢查後執行(Check-Then-Act)”操作,即通過一個可能失效的觀測結果來決定下一步的動作。
使用”先檢查後執行“的一種常見情況就是延遲初始化。例如:
@NotThreadSafe public class LazyInitRace { private ExpensiveObject instance = null; public ExpensiveObject getInstance(){ if(instance == null){ instance = new ExpensiveObject(); } return instance; } }
另一種競態條件。“讀取-修改-寫入”,前面的有狀態例子中就是這樣。
復合操作
LazyInitRace和UnsafeCountingFactorizer都包含一組需要以原子方式執行(或者說不可分割)的操作。要避免競態條件問題,就必須在某個線程修改該變量時,通過某種方式防止其他線程使用這個變量。
原子方式的操作:將設兩個操作A和B,如果從執行A的線程來看,當另一個線程執行B時,要麽將B全部執行完,要麽完全不執行B,那麽A和B對彼此來說是原子的。原子操作是指對於訪問同一個狀態的所有操作(包括操作本身)來說,這個操作是一個以原子方式執行的操作。
我們使用不加鎖的方式修復前面的CountingFactorizer問題:
@ThreadSafe public class CountingFactorizer implements Servlet{ private final AtomicLong count = new AtomicLong(0); public long getCount(){ return count.get();} public void service(ServletRequest req, ServletResponse resp) { BigInteger i = extractFromRequest(req); BigInteger[] factors = factor(i); count.incrementAndGet(); encodeIntoResponse(resp, factors); } }
在實際情況中,應竟可能地使用現有的線程安全對象(例如AcomicLong)來管理類的狀態。與非線程安全的對象相比,判斷線程安全對象的可能狀態以及狀態轉換情況更為容易,從而也更容易維護和驗證線程安全性。
加鎖機制
當在Servlet中添加一個狀態變量時,可以通過多線程安全的對象來管理Servlet的狀態以維護Servlet的線程安全性。但是如果想在Servlet中添加更多的狀態,那麽是否只需要添加更多的線程安全狀態變量就足夠了?
例如:希望將最近的計算結果緩存起來,當兩個連續的請求對相同的數值進行因數分解時,可以直接使用上一次的計算結果,而無需重新計算。
@NotThreadSafe public class UnsafeCachingFactorizer implements Servlet { private final AtomicReference<BigInteger> lastNumber = new AtomicReference<BigInteger>(); private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<BigInteger[]>(); public void service(ServlerRequest req, ServletResponse resp){ BigInteger i = extractFormRequest(req); if(i.equals(lastNumber.get())){ encodeIntoResponse(resp, lastFactors.get()); }else{ BigInteger[] factors = factor(i); lastNumber.set(i); lastFactors.set(factors); encodeIntoResponse(resp, factors); } } }
這樣並不安全,雖然這些原子引用本身都是現成安全的,但在UnsafeCachingFactorizer中存在著競態條件,這可能產生錯誤的結果。
內置鎖
java提供了一種內置的鎖機制來支持原子性:同步代碼塊(Synchronized Block)
同步代碼塊包括兩個部分:一個作為鎖的對象引用,一個座位由這個鎖保護的代碼塊。以關鍵字synchronized來修飾的方法就是一種橫跨整個方法體的同步代碼塊,其中該同步代碼塊的鎖就是方法調用所在的對象。靜態的synchronized方法以Class對象作為鎖。
synchronized (lock) {
//訪問或修改由鎖保護的共享狀態
}
每個java對象都可以用作一個實現同步的鎖,這些鎖被稱為內置鎖(Instrinsic Lock)或監視器(Monitor Lock).線程在進入同步代碼塊之前會自動獲得鎖,並且在退出同步代碼塊時自動釋放鎖。無論何種方式退出,獲得內置鎖的唯一途徑就是進入由這個鎖保護的同步代碼塊或方法。
java的內置所相當於一種互斥體(或互斥鎖),這意味著最多只能由一個縣城能持有這種鎖。
由於每次只能由一個縣城執行內置鎖保護的代碼塊,因此,由這個鎖保護的同步代碼塊會以原子方式執行。
這種同步機制使得確保因數分解Servlet的線程安全性變得更簡單。然而卻過於極端,因為多個客戶端無法同時使用因數分解Servlet,服務的響應性非常低,無法令人接受。
@ThreadSafe public class SynchronizedFactorizer implements Servlet { @GuardedBy("this") private BigInteger lastNumber; @GuardedBy("this") private BigInteger[] lastFactors; public synchronized void service(ServletRequest req, ServletResponse resp){ BigInteger i = extractFormRequest(req); if(i.equals(lastNumber)){ encodeIntoResponse(resp, lastFactors); }else{ BigInteger[] factors = factor(i); lastNumber = i; lastFactors = factors; encodeIntoResponse(resp, factors); } } }
重入
當某個線程請求一個由其他線程持有的鎖時,發出請求的線程就會阻塞。然而,由於內置鎖是可重入的,因此如果某個線程試圖獲得一個已經由他自己持有的鎖,那麽這個請求就會成功。“重入”意味著獲取鎖的操作的粒度是"線程",而不是“調用”。
重入的一種實現方法為:為每個鎖關聯一個獲取計數器和一個所有者線程。當計數值為0時,這個所被認為是沒有被任何線程持有。當線程請求一個未被持有的鎖時,JVM將記下鎖的持有者,並且將獲取的計數值置為1.如果同一個線程再次獲取這個所,計數值將遞增,而當線程退出同步代碼塊時,計數器會對應的遞減。當計數器的值為0時,這個鎖將被釋放。
如果內置鎖不是可重入的,那麽這段代碼將發生死鎖:
public class Widget{ public synchronized void doSomething() { ... } } public class LoggingWidget extends Widget { public synchronized void doSomething() { System.out.println(toString() + ": calling doSomething"); super.doSomething(); } }
用鎖來保護狀態
由於鎖能使其保護的代碼路徑以串行形式來訪問,因此可以通過鎖來構造一些協議以實現對共享狀態的獨立訪問。
對象的內置鎖與其狀態之間沒有內在的關聯。雖然大多數類都將內置鎖用做一種有效的加鎖機制,單對象的域並不一定要通過內置鎖來保護。當獲取與對象關聯的鎖時,並不能阻止其他線程訪問該對象,某個線程在獲得對象的鎖之後,只能阻止其他線程獲得同一個鎖。之所以每個對象都有一個內置鎖,只是為了免去顯式樣的創建鎖對象。你需要自行後遭加鎖協議或者同步協議來實現對共享狀態的安全訪問,並且在程序中自始至終地使用它們。
每個共享的和可變的變量都應該只由一個鎖來保護,從而使維護人員知道是哪一個鎖。
一種常見的加鎖約定是,將所有的可變狀態都封裝在對象內部,並通過對象的內置鎖對所有訪問可變狀態的代碼路徑進行同步,使得該對象上不會發生並發訪問。
當類的不變性條件涉及多個狀態變量時,那麽還有另外一個需求:在不變性條件中的每一個變量都必須由同一個鎖來保護。
活躍性與性能
在UnsafeCachingFactorizer中,我們通過在因數分解Servlet中引入緩存機制來提升性能。在緩存中需要使用共享狀態,因此需要通過同步來維護狀態的完整性。然而,如果使用SynchronizedFactorizer中的同步方式,那麽代碼的執行性能將非常糟糕。
我們將這種Web應用程序稱之為不良並發(Poor Concurrency)應用程序:同時可調用的數量,不僅搜到可用處理資源的限制,還搜到應用本身結構的限制。幸運的是,通過縮小同步代碼塊的作用範圍,我們很容易做到既確保Servlet的並發性,同時又維護線程安全性。要確保同步代碼塊不要過小,並且不要講本應是原子的操作拆分到多個同步代碼塊中。應該盡量將不影響共享狀態切執行時間較長的操作從同步代碼塊中分離出去,從而在這些操作的執行過程中,其他線程可以訪問共享狀態。
修改後代碼:
@ThreadSafe public class CachedFactorizer implements Servlet { @GuardedBy("this") private BigInteger lastNumber; @GuardedBy("this") private BigInteger[] lastFactors; @GuardedBy("this") private long hits; @GuardedBy("this") private long cacheHits; public synchronized long getHits(){ return hits; } public synchronized double getCacheHitRatio(){ return (double) cacheHits / (double) hits; } public void service(ServletRequest req, ServletResponse resp) { BigInteger[] i = extractFormRequest(req); 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(resp, factors); } }
當執行時間較長的計算或者可能無法快速完成的操作時(例如,網絡I/O或控制臺I/O),一定不要持有鎖
基礎知識