《Java併發編程實戰》第三章筆記

對象的共享

  • 要編寫正確的併發程序,關鍵問題在於:在訪問共享的可變狀態時需要進行正確的管理。
  • 我們已經知道了同步代碼塊和同步方法可以確保以原子的方式執行操作,但一種常見的誤解是,認爲關健字synchronized只能用於實現原子性或者確定“臨界區(Critical Section)”。同步還有另一個重要的方面:內存可見性(Memory Visibility)。我們不僅希望防止某個線程正在使用對象狀態而另一個線程在同時修改該狀態,而且希望確保當一個線程修改了對象狀態後,其他線程能夠看到發生的狀態化。如果沒有同步,那麼這種情況就無法實現。你可以通過顯式的同步或者類庫中內置的同步來保證對象被安全地發佈。

可見性

  • 通常,我們無法確保執行讀操作的線程能適時地看到其他線程寫入的值,有時甚至是根本不可能的事情。爲了確保多個線程之間對內存寫入操作的可見性,必須使用同步機制。
public class Novisibility {
	private static boolean ready;
	private static int number;
	
	private static class ReaderThread extends Thread {
		public void run () {
			while (!ready) {
				Thread.yield();
			}
			System.out.println(number);
		}
	}
	
	public static void main (String[] args) {
		new ReaderThread().start();
		number = 42;
		ready = true;
	}
}
  • 在沒有同步的情況下,編譯器、處理器以及運行時等都可能對操作的執行順序進行一些意想不到的調整。在缺乏足夠同步的多線程程序中,要想對內存操作的執行順序進行判斷,幾乎無法得出正確的結論。

失效數據

非原子的64位操作

  • 當線程在沒有同步的情況下讀取變量時,可能會得到一個失效值,但至少這個值是由之前某個線程設置的值,而不是一個隨機值。這種安全性保證也被稱爲最低安全性(out-of-thin-airsafety)

加鎖與可見性

  • 內置鎖可以用於確保某個線程以一種可預測的方式來查看另一個線程的執行結果。
    在這裏插入圖片描述
  • 加鎖的含義不僅僅侷限於互斥行爲,還包括內存的可見性。爲了確保所有線程都能看到共享變量的最新值,所有執行讀操作或者寫操作的線程都必須在同一個鎖上同步。

Volatile變量

發佈與逸出

  • “發佈(Publish)”一個對象的意思是指,使對象能夠在當前作用域之外的代碼中使用。例如,將一個指向該對象的引用保存到其他代碼可以訪問的地方,或者在某一個非私有的方法中返回該引用,或者將引用傳遞到其他類的方法中。
public static Set<Secret> knownSecrets;

public void initialize () {
	knowSecrets = new HashSet<Secret> ();
}

當發佈某個對象時,可能會間接地發佈其他對象。

安全的對象構造過程

  • 不要在構造過程中使this引用逸出
  • 在構造過程中使this引用逸出的一個常見錯誤是,在構造函致中啓動一個線程。當對象在其構造函數中創建一個線程時,無論是顯式創建(通過將它傳給構造函數)還是隱式創建(由於Thread或Runnable是該對象的一個內部類),this引用都會被新創建的線程共享。在對象尚未完全構造之前,新的線程就可以看見它。在構造函數中創建線程並沒有錯誤,但最好不要立即啓動它,而是通過一個start或initialize方法來啓動(請參見第7章瞭解更多關於服務生命週期的內容)。在構造函數中調用一個可改寫的實例方法時(既不是私有方法,也不是終結方法),同樣會導致this引用在構造過程中逸出。
  • 如果想在構造函數中註冊一個事件監聽器或啓動線程,那麼可以使用一個私有的構造函
    數和一個公共的工廠方法(Factory Method),從而避免不正確的構造過程,如程序清單3・8中
    SafeListener 所示.
// 隱式地使this引用逸出
public class ThisEscape {
	public ThisEscape(EventSource source) {
		source.registerListenner(
			new EventListener() {
				public void onEvent(Event e) {
					doSomething(e);
				}
			}
		)
	}
}
  • 不要在構造過程中使this引用逸出。
// 使用工廠方法來防止this引用在構造過程中逸出
public class SafeListener {
	private final EventListener listener;
	
	private SafeListener() {
		listener = new EventListener() {
			public void onEvent(Event e) {
				doSomething(e);
			}
		}
	}
	
	public static SafeListener newInstance(EventSource source) {
		SafeListener safe = new SafeListener();
		source.registerListener(safe.listener);
		return safe;
	}
}
  • 具體來說,只有當構造函數返回時,this引用才應該從線程中逸出。構造函數可以將this引用保存到某個地方,只要其他線程不會在構造函數完成之前使用它。

線程封閉

  • 當訪問共享的可變數據時,通常需要使用同步。一種避免使用同步的方式就是不共享數據。如果僅在單線程內訪問數據,就不需要同步。這種技術被稱爲線程封閉(Thread Confinement),他是實現線程安全性的最簡單方式之一。當某個對象封閉在一個線程中時,這種用法將自動實現線程安全性,即使被封閉的對象本身不是線程安全的。
  • 在Java語言中並沒有強制規定某個變量必須由鎖來保護,同樣在Java語言中也無法強制將對象封閉在某個線程中。線程封閉是在程序設計中的一個考慮因素,必須在程序中實現。Java語言及其核心庫提供了一些機制來幫助維持線程封閉性,例如局部變量和ThreadLocal類,但即便如此,程序員仍然需要負責確保封閉在線程中的對象不會從線程中逸出。

Ad-hoc線程封閉

  • Ad-hoc線程封閉是指,維護線程封閉性的職責完全由程序實現來承擔。Ad-hoc線程封閉式非常脆弱的,因爲沒有任何一種語言特性,例如可見性修飾符或局部變量,能將對象封閉到目標線程上。
  • 由於Ad-hoc線程封閉技術的脆弱性,因此在程序中儘量少用它,在可能的情況下,應該使用更強的線程封閉技術。

棧封閉

  • 棧封閉式線程封閉的一種特例,在棧封閉中,只能通過局部變量才能訪問對象。正如封裝能使得代碼更容易維持不變性條件那樣,同步變量也能使對象更易於封閉在線程中。局部變量的固有屬性之一就是封閉在執行線程中。他們位於執行線程的棧中,其他線程無法訪問這個棧。棧封閉(也被稱爲線程內部使用或者線程局部使用,不要與核心類庫中的ThreadLocal混淆)比Ad-hoc線程封閉更易於維護,也更加健壯。
// 基本類型的局部變量與引用變量的線程封閉性
public int loadTheArk(Collection<Animal> candidates) {
	SortedSet<Animal> animals;
	int numPairs = 0;
	Animal candidate = null;
	
	// animals 被封閉在方法中,不要使他們逸出!
	animals = new TreeSet<Animal>(new SpeciesGenderComparator());
	animals.addAll(candidates);
	for (Animal a : animals) {
		if (candidate == null || !candidate.isPotentialMate(a)) {
			candidate = a;
		} else {
			ark.load(new AnimalPair(candidate, a));
			++numPairs;
			candidate = null;
		}
		return numPairs;
	}
}

ThreadLocal類

  • 維持線程封閉性的一種更規範方法是使用ThreadLocal,這個類能使線程中的某個值與保存值的對象關聯起來。ThreadLocal提供了get與set等訪問接口或方法,這些方法爲每個使用該變量的線程都存有一份獨立的副本,因此get總是返回由當前執行線程在調用set時設置的最新值。
private static ThreadLocal<Connection> connectionHolder 
	= new ThreadLocal<Connection> () {
	public Connection initialValue () {
		return DriverManager.getConnection(DB_URL);
	}	
};
public static Connection getConnection () {
	return connectionHolder.get();
}

不變性

  • 如果某個對象在被創建後其狀態就不能被修改,那麼這個對象就稱爲不可變對象。
  • 不可變對象一定是線程安全的。
  • 當滿足以下條件時,對象纔是不可變的:
    • 對象創建以後其狀態就不能修改。
    • 對象的所有域都是final類型。
    • 對象時正確創建的(在對象的創建期間,this引用沒有逸出)。
@Immutable
public final class ThreeStooges {
	private final Set<String> stooges = new HashSet<String>();
	
	public ThreeStooges () {
		stooges.add("More");
		stooges.add("Larry");
		stooges.add("Curly");
	}
	
	public boolean isStooge (String name) {
		return stooges.contains(name);
	}
}

Final域

示例:使用Volatile類型來發布不可變對象

// 對數值及其因數分解結果進行緩存的不可變容器類
@Immutable
class OneValueCache {
	private final BigInteger lastNumber;
	private final BigInteger[] lastFactors;
	
	public OneValueCache (BigInteger i, BigInteger[] factors) {
		lastNumber = i;
		lastFactors = Arrays.copyOf(factors, factors.length);
	}
	
	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);
	
	public void service (ServletRequest req, ServletResponse resp) {
		BigInteger i = extractFromRequest(req);
		BigInteger[] factors = cache.getFactors(i);
		if (factors == null) {
			factors = factor(i);
			cache == new OneValueCache(i, factors);
		}
		encodeIntResponse(resp, factors);
	}
}

安全發佈

// 在沒有足夠同步的情況下發布對象

// 不安全的發佈
public Holder hlder;

public void initialize () {
	holder = new Holder(42);
}

不正確的發佈:正確的對象被破壞

  • 你不能指望一個尚未被完全創建的對象擁有完整性。
// 由於未被正確發佈,因此這個類可能出現故障

public class Holder {
	private int n;
	
	public Holder (int n) {
		this.n = n;
	}
	
	public void assertSanity () {
		if (n != n) {
			throw new AssertionError("This statement is false");
		}
	}
}
  • 由於沒有使用同步來確保Holder對象對其他線程可見,因此將Holder稱爲“未被正確發佈”。

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

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

安全發佈的常用模式

  • 可變對象必須通過安全的方式來發布,這通常意味着在發佈和使用該對象的線程時都必須使用同步。

  • 要安全地發佈一個對象,對象的引用以及對象的狀態必須同時對其他線程可見。一個正確構造的對象可以通過以下方式來安全地發佈:

    • 在靜態初始化函數中初始化一個對象引用。
    • 將對象的引用保存到 volatile 類型的域或者 AtomicReferance 對象中。
    • 將對象的引用保存到某個正確構造對象的final類型域中。
    • 將對象的引用保存到一個由鎖保護的域中。
  • 在線程安全容器內部的同步意味着,在將對象放入到某個容器,例如 Vector 或 synchronizedList時,將滿足上述最後一條需求。

    • 通過將一個鍵或者值放入 Hashtable、synchronizedMap 或者 ConcurrentMap 中,可以安全地將它發佈給任何從這些容器中訪問它的線程(無論是直接訪問還是通過迭代器訪問)。
    • 通過將某個元素放入 Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList 或 synchronizedSet 中,可以將該元素安全地發佈到任何從這些容器中訪問該元素的線程。
    • 通過將某個元素放入BlockingQueue 或者 ConcurrentLinkedQueue 中,可以將該元素安全地發佈到任何從這些隊列中訪問該元素的線程。
  • 靜態初始化器由JVM在類的初始化階段執行。

事實不可變對象

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

可變對象

  • 對象的發佈需求取決於它的可變性:
    • 不可變對象可以通過任意機制來發布。
    • 事實不可變對象必須通過安全方式來發布。
    • 可變對象必須通過安全方式來發布,並且必須是線程安全的或者由某個鎖保護起來。

安全地共享對象

  • 在併發程序中使用和共享對象時,可以使用一些實用的策略,包括:
    • 線程封閉。線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中,並且只能由這個線程修改。
    • 只讀共享。在沒有額外同步的情況下,共享的只讀對象可以由多個線程併發訪問,但任何線程都不能修改它。共享的只讀對象包括不可變對象和事實不可變對象。
    • 線程安全共享。線程安全的對象在其內部實現同步,因此多個線程可以通過對象的共有接口來進行訪問而不需要進一步同步。
    • 保護對象。被保護的對象只能通過持有特定的鎖來訪問。保護對象包括封裝在其他線程安全對象中的對象,以及已發佈的並且由某個特定鎖保護的對象。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章