第一章:介紹
知識點:
- 進程是資源(CPU、內存等)分配的基本單位
- 線程是CPU調度和分派的基本單位
- 一個進程包括一個或多個線程
1、爲什麼應用程序需要從單線程發展爲多線程(從同步到異步)?
- 資源利用。程序有時候需要等待外部的操作,比如輸入和輸出,並且在等待的時候不可能進行有價值的工作。在等待的時候,讓其他的程序運行會提高效率。
- 公平。多個用戶或程序可能對系統資源有平等的優先級別。讓他們通過更好的時間片方式來共享計算機,這要比結束一個程序後纔開始下一個程序更可取。
- 方便。寫一些程序,讓它們各自執行一個單獨任務並進行必要的相互協調,這要比編寫一個程序來執行所有的任務更容易,更讓人滿意。
第二章:線程安全
一個對象的狀態就是它的數據。編寫線程安全的代碼,本質上就是管理對狀態(state)的訪問,而且通常都是共享的、可變的狀態。
所謂共享,是指一個變量可以被多個線程訪問;所謂可變,是指變量的值在其生命週期內可以改變。我們討論的線程安全性好像是關於代碼的,但是我們真正要做的,是在不可控制的併發訪問中保護數據。
在沒有正確同步的情況下,如果多個線程訪問了同一個變量,你的程序就存在隱患。有3種方法修復它:
- 不要跨線程共享變量;
- 使狀態變量爲不可變的;或者
- 在任何訪問狀態變量的時候使用同步。
線程安全的定義:
當多個線程訪問一個類時,如果不用考慮這些線程在運行時環境下的調度和交替執行,並且不需要額外的同步及在調用方代碼不必作其他的協調,這個類的行爲仍然是正確的,那麼稱這個類是線程安全的。
無狀態對象永遠是線程安全的。
線程不安全之複合操作
讀-改-寫(read-modify-write)
@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);
}
}
檢查再運行(check-act)
@NotThreadsafe
public class LazyInitRace {
private Expensive0bject instance = null;
public ExpensiveObject getInstance() {
if ( instance == null ) {
instance = new Expensive0bject();
}
return instance;
}
}
爲了確保線程安全,“檢查再運行” 操作(如惰性初始化)和讀-改-寫操作(如自增)必須是原子操作。我們將“檢查再運行”和讀-改-寫操作的全部執行過程看作是複合操作:爲了保證線程安全,操作必須原子地執行。
我們會在下一節考慮用Java內置的原子性機制——鎖。現在,我們先用其他方法修復這個問題——使用已有的線程安全類。
@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);
}
}
java.util.concurrent.atomic
包中包括了原子變量(atomic variable) 類,這些類用來實現數字和對象引用的原子狀態轉換。把long
類型的計數器替換爲AtomicLong
類型的,我們可以確保所有訪問計數器狀態的操作都是原子的。計數器是線程安全的了。
內部鎖
Java提供了強制原子性的內置鎖機制:synchronized
。一個synchronized
塊有兩部分
:鎖對象的引用,以及這個鎖保護的代碼塊。
每個Java對象都可以隱式地扮演一個用於同步的鎖的角色;這些內置的鎖被稱作內部鎖(intrinsic locks)或監視器鎖(monitor locks)。
執行線程進入synchronized
塊之前會自動獲得鎖;而無論通過正常控制路徑退出,還是從塊中拋出異常,線程都在放棄對synchronized
塊的控制時自動釋放鎖。
獲得內部鎖的唯一途徑是:進入這個內部鎖保護的同步塊或方法。
內部鎖在Java中扮演了互斥鎖(mutual exclusion lock,也稱作mutex)的角色,意味着至多隻有一個線程可以擁有鎖,當線程A嘗試請求一個被線程B佔有的鎖時,線程A必須等待或者阻塞,直到B釋放它。如果B永遠不釋放鎖,A將永遠等下去。
重進入(Reentrancy)
當一個線程請求其他線程已經佔有的鎖時,請求線程將被阻塞。然而內部鎖是可重進入的,因此線程在試圖獲得它自己佔有的鎖時,請求會成功。
重進入意味着所的請求是基於“每線程(per-thread)”,而不是基於“每調用(per-invocation) ”的。
重進入的實現是通過爲每個鎖關聯一個請求計數(acquisition count)和一個佔有它的線程。當計數爲0時,認爲鎖是未被佔有的。
線程請求一個未被佔有的鎖時,JVM將記錄鎖的佔有者,並且將請求計數置爲1。如果同一線程再次請求這個鎖,計數將遞增;每次佔用線程退出同步塊,計數器值將遞減。直到計數器達到0時,鎖被釋放。
/**
* 如果該鎖不是可重入的,代碼將死鎖
*/
public class Widget {
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
@Override
public synchronized void doSomething() {
super.doSomething();
}
}
對於每個可被多個線程訪問的可變狀態變量,如果所有訪問它的線程在執行時都佔有同一個鎖,這種情況下,我們稱這個變量是由這個鎖保護的。
每個共享的可變變量都需要由唯一一個確定的鎖保護。
適當調整
synchronized
塊的大小,使其在安全和性能之間達到平衡。
有些耗時的計算或操作,比如網絡或控制檯I/O,難以快速完成。執行這些操作期間不要佔有鎖。