1. 程式人生 > 實用技巧 >Java併發程式和共享物件實用策略

Java併發程式和共享物件實用策略

在併發程式中使用和共享物件時,可以使用一些實用的策略,包括:

  1. 執行緒封閉
  2. 只讀共享。共享的只讀物件可以由多個執行緒併發訪問,但任何執行緒都不能修改它。共享的只讀物件包括不可變物件和事實不可變物件
  3. 執行緒安全共享。執行緒安全地物件在器內部實現同步。
  4. 保護物件。被保護的物件只能通過持有特定的鎖來方訪問。

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、不變性

滿足同步需求的另一種方法是使用不可變物件。執行緒安全性是不可變物件的固有屬性之一,它們的不變性條件是由建構函式建立的,只要它們的狀態不改變,那麼這些不變條件就能得以維持。另一方面,不可變物件不會像這樣被惡意程式碼或者有問題的程式碼破壞,因此可以安全地共享和釋出這些物件,而無須建立保護性的副本。

當滿以下條件時,物件才是不可變的:

  1. 物件建立以後其狀態就不能修改。
  2. 物件的所有域都是final型別。
  3. 物件是正確建立的(在物件的建立期間,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內部存在著同步機制,因此通過這種方式初始化的任何物件都可以被安全地釋出。

事實不可變物件指的是如果物件從技術上看是可變的,但其狀態在釋出後不會再改變,那麼把這種物件稱為事實不可變物件。在這些物件釋出後,程式只需要將它們視為不可變物件即可。

轉自:有夢想的鹹魚