Java併發程式和共享物件實用策略
在併發程式中使用和共享物件時,可以使用一些實用的策略,包括:
- 執行緒封閉
- 只讀共享。共享的只讀物件可以由多個執行緒併發訪問,但任何執行緒都不能修改它。共享的只讀物件包括不可變物件和事實不可變物件
- 執行緒安全共享。執行緒安全地物件在器內部實現同步。
- 保護物件。被保護的物件只能通過持有特定的鎖來方訪問。
1、執行緒封閉
當訪問共享的可變資料時,通常需要使用同步。一種避免使用同步的方式就是不共享資料。如果僅在單執行緒內訪問資料,就不需要同步。這種技術被稱為執行緒封閉,它是實現執行緒安全性的最簡單方式之一。
Ad-hoc執行緒封閉指的是,維護執行緒封閉性的職責完全由程式實現來承擔。
當決定使用執行緒封閉技術時,通常是因為要將某個特定的子系統實現為一個單執行緒子系統。在volatile變數上存在一種特殊的執行緒封閉,只要你能確保只有單個執行緒對共享的volatile變數執行寫入操作,那麼就可以安全地在這些共享的volatile變數上執行“讀取-修改-寫入”操作。在這種情況下,相當於將修改操作封閉在單個執行緒中以防止發生競態條件,並且volatile變數的可見性保證還確保了其他執行緒能看到的最新值。
棧封閉是一種執行緒封閉的特例,在棧封閉中,只能通過區域性變數才能訪問物件。區域性變數的固有屬性之一就是封閉在執行執行緒中。它們位於執行執行緒的棧中,其他執行緒無法訪問這個棧。在下面的例子中,由於任何方法都無法獲得對基本型別的引用,因此Java語言的這種語義就確保了基本型別的區域性變數始終封閉線上程內。
public int loadTheArk(Collection<Animal> candidates){
SortedSet<Animal> animals;
int numPirs = 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類通常用於防止對可變的單例項變數(Singleton)或全域性變數進行共享。例如通過將JDBC的連線儲存到ThreaLocal物件中,每個執行緒都會擁有屬於自己的連線
private static ThreadLocal<Connection> connectionHolder
=new ThreadLocal<Connection>(){
public Connection initialValue(){
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection(){
return connectionHolder.get();
}
當某個頻繁執行的操作需要一個臨時物件,例如一個緩衝區,而同時又希望避免在每次執行時都重新分配該臨時物件,就可以用這個技術。但是,ThreadLocal變數類似於全域性變數,它能降低程式碼的可重用性,並在類之間引入隱含的耦合性,因此在使用時要格外小心。
2、不變性
滿足同步需求的另一種方法是使用不可變物件。執行緒安全性是不可變物件的固有屬性之一,它們的不變性條件是由建構函式建立的,只要它們的狀態不改變,那麼這些不變條件就能得以維持。另一方面,不可變物件不會像這樣被惡意程式碼或者有問題的程式碼破壞,因此可以安全地共享和釋出這些物件,而無須建立保護性的副本。
當滿以下條件時,物件才是不可變的:
- 物件建立以後其狀態就不能修改。
- 物件的所有域都是final型別。
- 物件是正確建立的(在物件的建立期間,this引用沒有逸出)。
@Immutable
public final class ThreeStooges{
private final Set<String> stooges = new HashSet<String>();
public ThreeStooges(){
stooges.add("Moe");
stooges.add("Larry");
stooges.add("Curly");
}
public boolean isStooge(String name){
return stooges.contains(name);
}
}
使用Volatile型別來發布不可變物件
``` java
@Immutable
public class OneValueCache {
private final BigInteger lastNumber;
private final BigInteger[] lastFactors;
public OneValueCache(BigInteger lastNumber, BigInteger[] lastFactors) {
this.lastNumber = lastNumber;
this.lastFactors = Arrays.copyOf(lastFactors, lastFactors.length);
}
public BigInteger[] getFactors(BigInteger integer) {
if (lastNumber == null || !lastNumber.equals(integer)) {
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型別的引用來確保可見性,
* 使得Volatile Cache Factorizer 在沒有顯式地使用鎖的情況下仍然是執行緒安全的
* @param servletRequest
* @param servletResponse
* @throws ServletException
* @throws IOException
*/
@Override
public void service(ServletRequest servletRequest, ServletResponse servletResponse)
throws ServletException, IOException {
BigInteger i = extractFromRequest(servletRequest);
BigInteger[] factors = cache.getFactors(i);
if (factors == null) {
factors = factor(i);
cache = new OneValueCache(i, factors);
}
encodeIntoResponse(servletRequest, servletResponse);
}
}
3、安全釋出
要安全地釋出一個物件,物件的引用以及物件的狀態必須同時對其他執行緒可見。一個正確構造的物件可以通過以下方式來安全地釋出:
- 在靜態初始化函式中初始化一個物件引用。
- 將物件的引用儲存到volatile型別的域或者AtomicReferance物件中。
- 將物件的引用儲存到某個正確構造物件的final型別域中。
- 將物件的引用儲存到一個由鎖保護的域中
儘管javadoc在這個主題上沒有給出很清晰的說明,但執行緒安全庫中的容器類提供了以下的安全釋出保證:
- 通過將一個鍵或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地將它釋出給任何從這些容器中訪問它的執行緒
- 通過將某個元素放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或synchronizedSet中,可以將該元素安全地釋出到任何從這些容器中訪問該元素的執行緒。
- 通過將某個元素放入BlockingQueue或者ConcurrentLinkedQueue中,可以將該元素安全地釋出到任何從這些佇列中訪問該元素的執行緒
通常,要釋出一個靜態構造的物件,最簡單和最安全的方式是使用靜態的初始化器:
public static Holder hodler= new Holder(42);
靜態初始化器由JVM在類的初始化階段執行。由於在JVM內部存在著同步機制,因此通過這種方式初始化的任何物件都可以被安全地釋出。
事實不可變物件指的是如果物件從技術上看是可變的,但其狀態在釋出後不會再改變,那麼把這種物件稱為事實不可變物件。在這些物件釋出後,程式只需要將它們視為不可變物件即可。
轉自:有夢想的鹹魚