快速鳥瞰併發編程, 嘔心瀝血整理的架構技術【2】

做者:享學課堂James老師java

轉載請聲明出處!編程

接着第1篇後,咱們繼續來跟進一下併發編程的其它內容,以下:安全

第5節 安全發佈

要安全的發佈一個對象,對象的引用和對象的狀態必須同時對其餘線程可見。通常一個正確構造的對象(構造函數不發生this操做),能夠經過以下方式來正確發佈:bash

  • 在靜態初始化函數中初始化一個對象引用。只有一個線程能夠初始化靜態變量,由於類的初始化是在獨佔鎖下完成的。
class  JamesStaticInitializer {
	// 在不進行其餘初始化的狀況下發布不可變對象
	public  static  final  Year year = Year.of(2017);
	public  static  final  Set<String> keywords;
	// 使用靜態static來構造複雜對象
	static {
		// 建立可變集
		Set<String> keywordsSet = new  HashSet<>();
		// 初始化狀態
		keywordsSet.add("james");
		keywordsSet.add("13號技師");
		// 使集合不可修改
		keywords = Collections.unmodifiableSet(keywordsSet);
	}
}
複製代碼
  • 將一個對象引用保存在volatile類型的域或者是AtomicReference對象中。
class  JamesVolatile {
	private  volatile  String state;
	void setState(String state) {
		this.state = state;
	}
	String getState() {
		return state;
	}
}
複製代碼
  • AtomicInteger將值存儲在volatile字段中,所以volatile變量的相同規則適用於此處。
class  JamesAtomics {
	private  final  AtomicInteger state = new  AtomicInteger();
	void initializeState(int state) {
		this.state.compareAndSet(0, state);
	}
	int getState() {
		return state.get();
	}
}
複製代碼
  • final域, 將對象的引用保存到某個正確構造對象的final類型的域中。
class  JamesFinal {
	private  final  String state;
	JamesFinal(String state) {
		this.state = state;
	}
	String getState() {
		return state;
	}
}
複製代碼

確保此引用在構造期間不逃逸。多線程

this引用逃逸("this"escape)是指對象尚未構造完成,它的this引用就被髮布出去了。這是危及到線程安全的,由於其餘線程有可能經過這個逸出的引用訪問到「初始化了一半」的對象(partially-constructed object)。這樣就會出現某些線程中看到該對象的狀態是沒初始化完的狀態,而在另一些線程看到的倒是已經初始化完的狀態,這種不一致性是不肯定的,程序也會所以而產生一些沒法預知的併發錯誤。在說明併發編程中如何避免this引用逸出以前併發

class  JamesThisEscapes {
	private  final  String name;
	ThisEscapes(String name) {
		JamesCache.putIntoCache(this);
		this.name = name;
	}
	String getName() {
		return name;
	}
}
class  JamesCache {
	private  static  final  Map<String, ThisEscapes> CACHE = new  ConcurrentHashMap<>();
	static  void putIntoCache(JamesThisEscapes thisEscapes) {
		//「this」引用在對象徹底構造以前逃逸
		CACHE.putIfAbsent(thisEscapes.getName(), thisEscapes);
	}
}
複製代碼
  • 正確同步成員變量。
class  JamesSynchronization {
	private  String state;
	synchronized  String getState() {
		if (state == null)
		state = "Initial";
		return state;
	}
}
複製代碼

第6節 不可變的對象

不可變對象具有執行安全的特性。此外,相較於可變對象,不可變對象一般也較合理,易於瞭解,並且提供較高的安全性。不可變對象的一個重要特性是它們都是線程安全的,所以不須要同步。固然對象不可變的是有以下要求滴:函數

  • 全部變量都是 final.
  • 全部變量必須是可變對象或不可變對象。
  • this 在構造方法執行期間引用不會逃脫。
  • 該類是final,所以沒法在子類中覆蓋此行爲。

不可變對象的示例:post

// 聲明爲final類
public  final  class  JamesArtist {
	// 不可變對象, 字段爲final
	private  final  String name;
	//用於保存不可變對象, final類型
	private  final  List<JamesTrack> tracks;
	public  JamesArtist(String name, List<JamesTrack> tracks) {
		this.name = name;
		//防止拷貝
		List<JamesTrack> copy = new  ArrayList<>(tracks);
		//標記爲不可更改
		this.tracks = Collections.unmodifiableList(copy);
		// 「this」在構造期間不會傳遞到任何地方。
	}
}
// 同上聲明爲final類
public  final  class  JamesTrack {
	// 不可變對象, 字段爲final
	private  final  String title;
	public  JamesTrack(String title) {
		this.title = title;
	}
}
複製代碼

第7節 線程Thread類

java.lang.Thread類用於表示應用程序或JVM線程。代碼老是在某些Thread類的上下文中執行(用於 Thread#currentThread()獲取本身的Thread)。ui

線程狀態以下this

線程協調方法以下

怎麼處理 InterruptedException異常?

  • 在從新拋出 InterruptedException 以前執行特定於任務的清理工做。
  • 聲明當前方法拋出 InterruptedException.
  • 若是未聲明某個方法拋出 InterruptedException,則應經過調用將中斷的標誌恢復爲true, Thread.currentThread().interrupt()而且應該拋出一個更合適的異常。將標誌設置爲true很是重要,以便有機會處理更高級別的中斷。

不可預知的異常處理

線程能夠指定 UncaughtExceptionHandler將接收任何致使線程忽然終止的未捕獲異常的通知。

Thread thread = new  Thread(runnable);
thread.setUncaughtExceptionHandler((failedThread,exception)->
{
	logger.error("Caught unexpected exception in thread ‘{}’.", failedThread.getName(), exception);
}
);
thread.start();
複製代碼

第8節 線程的活躍度

死鎖

當存在多個線程時會發生死鎖,每一個線程等待另外一個線程持有的資源,從而造成資源循環和獲取線程。

潛在的死鎖示例:

class  JamesAccount {
	private  long amount;
	void plus(long amount) {
		this.amount += amount;
	}
	void minus(long amount) {
		if (this.amount < amount)
		throw  new  IllegalArgumentException(); else
		this.amount -= amount;
	}
	static  void transferWithDeadlock(long amount, JamesAccount first, JamesAccount second) {
		synchronized (first) {
			synchronized (second) {
				first.minus(amount);
				second.plus(amount);
			}
		}
	}
}
複製代碼

若是同時發生死鎖:

  • 一個線程正在嘗試從第一個賬戶轉移到第二個賬戶,而且已經得到了第一個賬戶的鎖。
  • 另外一個線程正在嘗試從第二個賬戶轉移到第一個賬戶,而且已經得到了第二個賬戶的鎖。

避免死鎖的技巧:

  • 鎖定順序 - 始終以相同的順序獲取鎖。
class  JamesAccount {
	private  long id;
	private  long amount;
	// 此處省略了一些方法
	static  void transferWithLockOrdering(long amount, JamesAccount first, JamesAccount second) {
		Boolean lockOnFirstAccountFirst = first.id < second.id;
		Account firstLock = lockOnFirstAccountFirst ? first : second;
		Account secondLock = lockOnFirstAccountFirst ? second : first;
		synchronized (firstLock) {
			synchronized (secondLock) {
				first.minus(amount);
				second.plus(amount);
			}
		}
	}
}
複製代碼
  • 鎖定超時 - 在獲取鎖時不要無限制地阻塞,應該釋放全部鎖後再嘗試。
class  JamesAccount {
	private  long amount;
	//省略了一些方法
	static  void transferWithTimeout(long amount, JamesAccount first, JamesAccount second, int retries, long timeoutMillis)
	throws  InterruptedException {
		for (int attempt = 0; attempt < retries; attempt++) {
			if (first.lock.tryLock(timeoutMillis, TimeUnit.MILLISECONDS)) {
				try {
					if (second.lock.tryLock(timeoutMillis, TimeUnit.MILLISECONDS)) {
						try {
							first.minus(amount);
							second.plus(amount);
						}
						finally {
							second.lock.unlock();
						}
					}
				}
				finally {
					first.lock.unlock();
				}
			}
		}
	}
}
複製代碼

JVM能夠檢測監視器死鎖,並在線程轉儲中打印死鎖信息。

活鎖和線程飢餓

活鎖偏偏與死鎖的概念相反,死鎖的意思是全部線程都拿不到資源且都佔用着對方的資源,而活鎖的意思是拿到資源後各線程卻又相互釋放不執行。

若是多線程中出現了相互謙讓,至關於13號技師得病後你們都不要她了,都主動將資源釋放給其它的線程來使用,那麼這個資源會不斷在多個線程之間跳動但又得不到明確執行,這就是活鎖;並且多線程執行中有線程的優先級,有的線程優先級高它是可以插隊並優先執行的,但若是這些優先級高的線程一直搶佔優先級低線程的資源,最終會致使低優先級線程它是沒法獲得執行的,這就是飢餓。

(未完待續......)

關注我,等待更新,還有更多技術乾貨分享~

相關文章
相關標籤/搜索