java併發編程(二): 對象的共享

對象的共享:

  • 要編寫併發程序,關鍵在於:在訪問共享可變狀態時須要進行正確的管理

可見性:     

/**
 * 可見性問題致使,程序運行結果不正確
 * 有可能因爲編譯器,處理器及運行時作一些重排序
 */
public class Novisibility {
	private static boolean ready;
	private static int number;
	
	private static class ReaderThread extends Thread{
		@Override
		public void run() {
			while (!ready){
				Thread.yield(); //主動讓出cpu, 進入就緒隊列
			}
			System.out.println(number);
		}
	}
	
	public static void main(String[] args) {
		new ReaderThread().start();
		number = 42;
		ready = true;
	}
}
  • 失效數據:就如同上面的代碼,沒有同步的狀況下可能產生錯誤的結果。

又如: java

/**
 * get操做可能與最近set值不一致,產生數據失效
 */
@NotThreadSafe
public class MutableInteger {
	private int value;

	public int getValue() {
		return value;
	}

	public void setValue(int value) {
		this.value = value;
	}
}
可作以下修改:
/**
 * 將get, set同步化,可防止數據失效
 */
@ThreadSafe
public class MutableInteger {
	private int value;

	public synchronized int getValue() {
		return value;
	}

	public synchronized void setValue(int value) {
		this.value = value;
	}
}
  • 非原子的64位操做:對於非volatile類型的long和double, jvm會將64位的讀寫操做分解爲2個32位操做(java虛擬機棧中的操做數棧每一個slot爲32位),對於這種變量在多線程讀寫操做下,有可能讀取到某個值的高32位或某個值的低32位,建議在多線程環境下,對共享可變的long或double變量進行volatile聲明。
  • 加鎖與可見性:加鎖的含義不只侷限於互斥行爲,還包括內存可見性。爲了確保全部線程能看到共享可變變量的最新值,全部執行讀寫操做的線程必須在同一個鎖上同步

      

  • Volatile變量:以前轉載過一篇相關文章(http://my.oschina.net/indestiny/blog/208541), 當變量聲明爲volatile後,那麼編譯器或運行時會主要這個變量是共享的不會將該變量上的操做與其餘內存操做一塊兒重排序。volatile變量不會被緩存在寄存器或其餘處理器不可見的地方,所以讀取volatile變量總會返回最新的值

      比較典型的用法: 數據庫

private volatile boolean asleep;
...
while (!asleep){
    // do sth.
}
  • 加鎖機制既能夠確保可見性又能夠確保原子性,而volatile變量只可確保可見性,因此說volatile是一種輕量級同步機制;
  • 知足如下全部條件時,可用volatile:

       1. 對變量的寫入操做不依賴變量的當前值,或者能確保只有一個線程更新變量的值; 緩存

       2. 該變量不會與其餘狀態變量一塊兒歸入不變性條件; 安全

       3. 訪問該變量不須要加鎖。 多線程

發佈與逸出:

  • 發佈對象:使對象可以在當前做用域以外被使用, 如:
//對共有靜態變量的發佈,集合內部的變量也會被髮布
public static Set<Object> publishedObject;
public void init(){
    publishedObject = new HashSet<>();
}
//經過共有方法發佈
private Object publishedObject;
public Object get(){
	return publishedObject;
}
/**
 * 經過發佈類的內部實例
 * this引用被逸出
 */
public class ThisEscape {
	public ThisEscape(EventSource source){
		source.registerListener(new EventListener() {
			@Override
			public void onEvent(Event e) {
				// ThisEscape.this實例逸出,但此時該實例並無構造完成
			}
		});
	}
}

安全的對象構造過程:

  • 不要在構造過程當中使this引用逸出,如上面的ThisEscape;

      可經過工廠方法避免this逸出: 併發

/**
 * 經過工廠方法防止this逸出
 */
public class SafeListener {
	private final EventListener listener;
	private SafeListener(){
		listener = new EventListener() {
			@Override
			public void onEvent(Event e) {
				//do sth.
			}
		};
	}
	
	public static SafeListener newInstance(EventSource source){
		SafeListener safe = new SafeListener();
		source.registerListener(safe.listener);
		return safe;
	}
}

線程封閉:

  • 線程封閉:在單線程內訪問數據,不須要同步,這是實現線程安全最簡單的方式。
  • Ad-hoc線程封閉:維護線程封閉性的職責徹底由程序實現來承擔,它很脆弱,由於沒有一種語言特性能將對象封閉到目標線程上。
  • 棧封閉:是線程封閉的一種特例,在棧封閉中,只能經過局部變量才能訪問對象。比Ad-hoc線程封閉更易維護,強壯。
  • ThreadLocal類:一種維持線程封閉更規範的方法,它會爲每一個使用ThreadLocal變量的線程存放一份獨立的副本,所以對於該變量線程之間不會相互干擾,你能夠想ThreadLocal<T>想成Map<Thread, T>, 當線程終止時 ,這些值會做爲垃圾回收。好比,多線程環境下,咱們能夠這樣獲取數據庫鏈接:

private static ThreadLocal<Connection> connectionHolder = 
	new ThreadLocal<Connection>(){
		@Override
		protected Connection initialValue() {
			try {
				return DriverManager.getConnection("DB_URL");
			} catch (SQLException e) {
				e.printStackTrace();
			} 
			return null;
		}
	};
	
public static Connection getConnection(){
	//不一樣線程每次獲得的Connection, 都是獨立的備份
	return connectionHolder.get();
}

不變性:

  • 不可變對象必定是線程安全的;
  • 對象不可變應知足:

      1.對象建立後其狀態不能修改; jvm

      2.對象的全部域都是final類型; ide

      3.對象是正確建立的(在對象建立期間,this未逸出)。 函數

  • Final域:final類型的域不能修改(但其指向的引用對象可修改),從新改造以前文章的CachedFactorizer:
/**
 * 不可變類:
 * 全部域都是final
 */
public class OneValueCache {
	private final BigInteger lastNumber;
	private final BigInteger[] lastFactors;
	
	public OneValueCache(BigInteger lastNumber, BigInteger[] lastFactors) {
		this.lastNumber = lastNumber;
		this.lastFactors = lastFactors;
	}
	
	public BigInteger[] getFactors(BigInteger i){
		if (lastNumber == null || ! lastNumber.equals(i)){
			return null;
		} else{
			return Arrays.copyOf(lastFactors, lastFactors.length);
		}
	}
}
/**
 * 使用Volatile類型發佈不可變對象
 */
@ThreadSafe
public class VolatileCachedFactorizer implements Servlet {
	private volatile OneValueCache cache = new OneValueCache(null, null); //volatile保證每次寫後最新值對其餘線程可見
	@Override
	public void service(ServletRequest req, ServletResponse repo) {
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = cache.getFactors(i);
		if (factors == null) {
			factors = factor(i);
			cache = new OneValueCache(i, factors);
		}
		reponseTo(i, factors);
	}
}

安全發佈:

  • 不正確的發佈:正確的對象被破壞:
/**
 * 多線程訪問下,有可能出錯,問題不在Holder自己,而在於未正確地發佈,可將n聲明爲final,避免不正確發佈
 */
public class Holder {
	private int n;
	
	public Holder(int n){
		this.n = n;
	}
	
	public void assertSanity(){
		if (n != n){
			throw new AssertionError("");
		}
	}
}

不可變對象與初始化安全性:

  • 任何線程均可以在不須要額外同步地狀況下安全地訪問不可變對象,即便在發佈這些對象時沒有使用同步。

安全發佈地經常使用模式:

  • 一個正確構造的對象能夠經過如下方式來安全地發佈:

      1.在靜態初始化函數中初始化一個對象引用; 性能

      2.將對象的引用保存到volatile類型地域或AtomicReference對象中;

      3.將對象的引用保存到某個正確構造對象地final類型域中;

      4.將對象的引用保存到一個由鎖保護的域中。

事實不可變對象:

  • 若是對象從技術上來看是可變的,但其狀態在發佈後不會再改變,稱這種對象爲"事實不可變對象"。
  • 在沒有額外的同步的狀況下,任何線程均可以安全地使用被安全發佈的事實不可變對象。

可變對象:

  • 可變對象:對象構造後,其狀態能夠發生改變;
  • 對象的發佈需求取決於它的可變性

       1.不可變對象能夠經過任意機制來發布;

       2.事實不可變對象必須經過安全方式來發布;

       3.可變對象必須經過安全方式來發布,而且必須是線程安全的或由某個鎖保護起來。

安全地共享對象:

  • 在併發程序中使用和共享對象時,可使用一些使用的策略,包括:

      1.線程封閉。線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中,只容許這個線程修改;

      2.只讀共享。在沒有同步的狀況下,共享的只讀對象能夠由多個線程併發訪問,但任何線程不能修改它。共享的只讀對象包括不可變對象和事實不可變對象。

      3.線程安全共享。線程安全的對象在其內部實現同步,多個線程能夠經過公有接口對其訪問而不需進一步同步;

      4.保護對象。被保護對象只能經過持有特定鎖來訪問。保護對象包括封裝在其餘線程安全對象中的對象,以及已發佈的而且由某個特定鎖保護的對象。

不吝指正。

相關文章
相關標籤/搜索