java併發編程(一): 線程安全性

線程安全性:

  • 要編寫線程安全的代碼,其核心就是要對狀態訪問操做進行管理,特別是共享的(Shared)可變的(Mutable)狀態的訪問;
  • 「共享」:多個線程可訪問同一變量;
  • 「可變」:變量值在聲明週期內可變化;
  • java的同步機制:獨佔鎖synchronized, volatile, 顯示鎖Lock原子變量;
  • 編寫併發程序的原則:1.代碼正確運行;2.提升代碼速度(需求所需時);

什麼是線程安全性:

  • 其核心概念就是正確性:即某個類的行爲與其規範徹底一致:
  • 線程安全性:當多個線程訪問某個類時,這個類始終都能表現出正確的行爲,這個類就是線程安全的;
  • 無狀態對象必定是線程安全的,如:
/**
 * 該類無任何屬性域,且不包含任何其餘類中域的引用
 * 即計算中的狀態都爲臨時狀態,保存在線程棧中
 * 由JMM知線程棧線程私有,僅由當前線程可訪問
 * 所以線程安全
 */
@ThreadSafe
public class StatelessFactorizer implements Servlet {
	@Override
	public void service(ServletRequest req, ServletResponse repo) {
		Map<String, Object> params = extractFromRequest(req);
		Map<String, Object> res = doBussiness(params);
		reponseTo(res);
	}
        ...
}

原子性:

看一個非線程安全的版本: java

/**
 * 競態條件(因爲不恰當的執行時序而出現不正確的結果)致使非線程安全
 */
@NotThreadSafe
public class UnsafeCountingFactorizer implements Servlet {
	private long count = 0;
	
	@Override
	public void service(ServletRequest req, ServletResponse repo) {
		count++; //該操做並不是原子(讀取--自增--寫入), 致使非線程安全
		Map<String, Object> params = extractFromRequest(req);
		Map<String, Object> res = doBussiness(params);
		reponseTo(res);
	}
}
例如這種狀況,預計值爲11,結果爲10:

  • 用例:延遲初始化的競態條件:

       看個實例: 緩存

/**
 * 延遲初始化的競態條件
 * 因爲多個線程可能同時執行到instance == null,
 * 或者因爲實例初始化過程比較耗費時間,
 * 這樣有可能所謂的"單例"再也不單例
 */
@NotThreadSafe
public class LazyInitRace {
	private ExpensiveObject instance;
	
	private LazyInitRace(){}
	
	public ExpensiveObject getInstance() {
		if (instance == null) {
			instance = new ExpensiveObject();
		}
		return instance;
	}
}

複合操做:

  • 原子操做:對於訪問同一個狀態的全部操做(包括該操做自己)來講,這個操做是一個以原子方式執行的操做(要麼所有執行,或所有不執行);
  • 複合操做:包含一組以原子方式執行的操做,如(檢查再初始化, 讀取-修改-寫入);
  • 可經過java提供的原子變量實現原子操做,修改上面的計數器:
/**
 * 可經過java提供的原子變量(Atomic*)
 * 來解決競態條件引發的非線程安全
 */
@ThreadSafe
public class CountingFactorizer implements Servlet {
	private AtomicLong count = new AtomicLong(0);
	
	@Override
	public void service(ServletRequest req, ServletResponse repo) {
		count.incrementAndGet(); //原子增長,該方法最後會調用Unsafe.compareAndSwapLong本地方法
		Map<String, Object> params = extractFromRequest(req);
		Map<String, Object> res = doBussiness(params);
		reponseTo(res);
	}
       ...
}
  • 當無狀態的類中,添加一個狀態時,若該狀態由線程安全的對象管理,那麼這個無狀態的類也是線程安全的,如上面這段代碼,可是當添加多個狀態時就不必定了。

加鎖機制:

  • 對於多個由線程安全的對象管理起來的狀態組合起來,且幾個狀態有關聯關係時,這時就可能不線程安全了,如:
/**
 * 多個關聯線程安全的狀態對象致使線程安全
 * 這裏咱們對lastNumber進行緩存,若請求的值與其相同,直接返回,反之計算
 */
@NotThreadSafe
public class UnsafeCachingFactorizer implements Servlet {
	private final AtomicReference<BigInteger> lastNumber = 
			new AtomicReference<>();
	private final AtomicReference<BigInteger[]> lastFactors = 
			new AtomicReference<>();
	
	@Override
	public void service(ServletRequest req, ServletResponse repo) {
	   BigInteger i = extractFromRequest(req);
	   //A線程發現不等,從新計算;(A未計算完)
	   //B線程進入也發現不等,也從新計算(但其實有可能通過A計算後是相等的)
	   //從而達不到緩存效果
	   if (i.equals(lastNumber.get())){
		   reponseTo(i, lastFactors);
	   } else{ 
		   BigInteger[] factors = factor(i);
		   lastNumber.set(i);
		   lastFactors.set(factors);
		   reponseTo(i, factors);
	   }
	}
        ...
}
  • 因此要保持狀態的一致性,就須要再單個原子操做中更新全部相關的狀態變量

內置鎖:

  • java提供的內置鎖機制實現: synchronized;
  • 經過synchronized咱們能夠簡單使上面的代碼線程安全,但這樣性能極低,每次只容許一個請求獲得響應,凡人難以接受:
/**
 * 經過synchronized實現線程安全,但性能低下
 */
@ThreadSafe
public class CachingFactorizer implements Servlet {
	private BigInteger lastNumber = new BigInteger("");
	private BigInteger[] lastFactors = new BigInteger[]{};
	
	@Override
	public synchronized void service(ServletRequest req, ServletResponse repo) {
	   BigInteger i = extractFromRequest(req);
	   if (i.equals(lastNumber)){
		   reponseTo(i,lastFactors);
	   } else{ 
		   BigInteger[] factors = factor(i);
		   lastNumber = i;
		   lastFactors = factors;
		   reponseTo(i, factors);
	   }
	}
}

重入:

  • 重入:一個已經持有某對象鎖的線程再次請求該該對象鎖時,這個請求會成功(synchronized容許重入);
  • 「重入」:意味着獲取鎖的粒度是「線程」而不是「調用」;

用鎖來保護狀態:

  • 對於可能被多個線程同時訪問的可變狀態變量,在訪問時須要持有同一個鎖,在這種狀況下,咱們稱狀態變量是由這個鎖保護的;
  • 每一個共享的和可變的變量都應該只由一個鎖來保護,從而使維護人員知道是哪個鎖

活躍性與性能:

  • 爲了提高上面Servlet處理請求吞吐量,明顯不能對整個方法synchronized, 下面對這個實現進行分段synchronized, 但仍必須保證多個狀態變化是原子操做;
/**
 * 經過分段synchronized提高性能
 */
@ThreadSafe
public class CachedFactorizer implements Servlet {
	private BigInteger lastNumber = new BigInteger("");
	private BigInteger[] lastFactors = new BigInteger[] {};
	private long hits;
	private long cacheHits;

	@Override
	public void service(ServletRequest req, ServletResponse repo) {
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = null;
		synchronized (this) {
			++hits;
			if (i.equals(lastNumber)) {
				++cacheHits;
				factors = lastFactors;
			}
		}
		if (factors == null) {
			factors = factor(i);
			synchronized (this) {
				lastNumber = i;
				lastFactors = factors;
			}
		}
		reponseTo(i, lastFactors);
	}
       ...
}
  • 一般,在簡單性與性能之間存在着相互制約的因素。當實現某個同步策略時,必定盲目地爲了性能而犧牲簡單性(這可能破壞安全性);
  • 當執行時間較長的計算或者可能沒法快速完成的操做時(若網絡I/O,控制檯I/O), 必定不要持有鎖。

不吝指正。 安全

相關文章
相關標籤/搜索