【學習筆記】【Java併發編程實戰】第二章 線程安全性

三種方式修復“多個線程訪問同一個可變的狀態變量時沒有使用合適的同步”:

  • 不在線程之間共享該狀態變量
  • 將狀態變量修改爲不可變的變量
  • 在訪問狀態變量時使用同步

什麼是線程安全性

  • 線程安全類: 當多個線程訪問某個類時,不管運行時環境採用何種調度方式或者這些線程將如何交替執行,並且在主調代碼中不需要任何額外的同步或協同,這個類都能表現出正確的行爲。
  • 在線程安全類中封裝了必要的同步機制,因此客戶端無須進一步採取同步措施。
  • 無狀態(既不包含任何域,又不包含任何對其他類中域的引用)對象一定是線程安全的。

原子性

  • 原子性:一組語句作爲一個不可分割的單元被執行。
  • 競態條件(race condition):某個計算的正確性取決於多個線程的交替執行時序。最常見的競態條件類型就是“先檢查後執行”操作,通過一個可能失效的觀測結果來決定下一步的動作。
  • 要避免競態條件問題,就必須在某個線程修改該變量時,通過某種方式防止其它線程使用這個變量。
  • 複合操作:包含了一組必須以原子方式執行的操作以確保線程安全性,如“先檢查後執行”或“讀取-修改-寫入”。
  • 應儘可能地使用現有的線程安全對象來管理類的狀態。

加鎖機制

public class UnsafeCachingFactorizer implements Servlet {
    private final AtomicReference<BigInteger> lastNumber = new AtomicReference<>();
    private final AtomicReference<BigInteger[]> lastFactors = new AtomicReference<>();
    
    public void service(ServletRequest req, ServletResponse resp) {
        BigInteger i = extractFromRequest(req);
        if (i.equals(lastNumber.get()))
            encodeIntoResponse(resp, lastFactors.get());
        else {
            BigInteger[] factors = factor(i);
            lastNumber.set(i); // 此時其它線程可能會讀取lastNumber和lastFactors,出現lastNumber和lastFactors不對等的情況。
            lastFactors.set(factors);
            encodeIntoResponse(resp, factors);
        }
    }
}
  • 要保持狀態的一致性,就需要在單個原子操作中更新所有相關的狀態變量。
  • 以synchronized來修飾的方法就是一種橫跨整個方法體的同步代碼塊,以方法調用所在的對象爲鎖,線程進入同步代碼塊後會自動獲得鎖,並在退出(正常退出或拋出異常)時自動釋放鎖。
  • 內置鎖(監視器鎖):每個Java對象都可以用做一個實現同步的鎖。內置鎖相當於一種互斥體,最多隻有一個線程能持有這種鎖。
  • 重入:爲每個鎖關聯一個獲取計數值和一個所有者線程。當計數值爲0時,這個鎖就被認爲是沒有被任何線程持有,當線程請求一個未被持有的鎖時,JVM將記下鎖的持有者並且將獲取計數值置爲1。如果同一線程再次獲取這個鎖,計數值將遞增,當線程退出同步代碼塊時,計數器將遞減。當計數值爲0是,這個鎖將被釋放。
public class Widget {
	public synchronized void doSomething() {
	}
}

public class LoggingWidget extends Widget {
	public synchronized void doSomething() { // 獲取LoggingWidget類的某對象的鎖。
	System.out.println(toString() + ":calling doSomething");
	super.doSomething(); // synchronized方法,需要再次獲取LoggingWidget類的該對象的鎖,如果內置鎖不是可重入的那麼會一直等待,發生死鎖。
	}
}

用鎖來保護狀態

  • 對於可能被多個線程同時訪問的可變狀態變量,在訪問它時都需要持有同一個鎖。
  • 每個共享和可變的變量都應該只由一個鎖來保護,只有被多個線程同時訪問的可變數據才需要通過鎖來保護。
  • 在不變性條件中的每個變量都必須由同一個鎖來保護。
  • 不加區別的濫用synchronized不足以保證複合操作是原子的,還可能導致活躍性問題或性能問題。

活躍性與性能

public class CachedFactorizer implements Servlet {
    @GuardeBy("this") private BigInteger lastNumber;
    @GuardeBy("this") private BigInteger[] lastFactors;
    @GuardeBy("this") private long hits;
    @GuardeBy("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 = extractFromRequest(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);
    }
}
  • 通過縮小同步代碼塊的作用範圍,可以確保併發性並維護線程安全線。將不影響共享狀態且執行時間較長的操作從同步代碼塊中分離出去。
  • 獲取與釋放鎖等操作都需要一定的開銷,但是不要盲目地爲了性能而犧牲簡單性,這可能會破壞安全性。
  • 當執行時間較長的計算或者可能無法快速完成的操作時,一定不要持有鎖。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章