《Java併發編程實戰》學習筆記(1)

第一章:介紹

知識點

  • 進程是資源(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,難以快速完成。執行這些操作期間不要佔有鎖。

發佈了113 篇原創文章 · 獲贊 77 · 訪問量 24萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章