1. 程式人生 > >在多執行緒環境中安全的共享物件

在多執行緒環境中安全的共享物件

1.      可見性

1.1      多執行緒環境中共享變數的可見性問題

(1)    單執行緒環境下,對一個共享變數的修改很自然的是有序的

在t1時刻,修改了一個變數的值

那麼在t2時候,一定會讀到這個變數的最新的值,不會說讀到過期的資料。

(2)    多執行緒環境下,對一個執行緒中對一個共享變數的修改,對其他執行緒來說不一定是可見的。

public class NoVisibility {

    private staticboolean ready;

    private staticint number;

    private staticclass ReaderThread extends Thread {

        public voidrun() {

            while(!ready)

               Thread.yield();

           System.out.println(number);

        }

    }

    public staticvoid main(String[] args) {

        newReaderThread().start();

        number =42;

        ready =true;

    }

}

在主執行緒中對ready設定true,不一定在ReaderThread中一定是馬上可見的。所以ReaderThread執行緒中loop會一直執行。以下幾個因素可能或導致這種情況的發生。

· 因為有快取存在,主執行緒對ready設定true,,不一定會store到主記憶體中去,所以ReaderThread執行緒還是讀到的過期資料

· 主執行緒對ready的修改儲存到主記憶體中去了,但是ReaderThread執行緒因為其快取了過期值,導致讀到的還是false

· 主執行緒中原始碼在編譯成機器指令的時候,可能reorder了機器指令,ready=true先執行,然後是number=42。所以ReaderThread可能直接列印number=0;

· 執行緒之間的執行是亂序的,並不能保證主執行緒一定先執行,然後才是ReaderThread執行緒,而ready又是對ReaderThread執行緒是不可見的,所以ReaderThread執行緒會一直執行。

可見,在缺乏適當同步的情況下,去分析機器是如何執行是很困難的。

單執行緒環境下,編譯器、cpu、快取可能都會優化程式碼的執行;前提條件只要不影響最終結果就行了。

而在多執行緒環境下,如果缺乏同步,那麼正是這些優化使得最後很難預測程式碼是如何執行的。

(3)過期資料

@NotThreadSafe
public class MutableInteger {
    private int value;
    public int  get() { return value; }
    public void set(int value) { this.value = value; }
}
缺乏適當同步的情況下,會導致線上程中讀到的資料是過期資料。
程式碼分析:
·  原來value值是0
·  Thread B呼叫set方法把value值改為2
·  Thread A呼叫get方法讀value的值
這時候Thread A讀到的值可能是0,也可能是2。這樣是Thread A是不是先與Thread B執行;Thread B對value值的修改是不是store到主記憶體中去了;Thread A是不是讀的是工作記憶體中的初始值,還是Thread B修改後的值。這些情況多會發生。
1.2  用lock來保證可見性。
(1)Lock的性質
·  原子性,由鎖保護的程式碼塊中所有操作可以視作是一個不可分割的unit
·  執行緒執行的有序性,由鎖保護的程式碼塊某個時間段只能由一個執行緒執行,執行緒執行完釋放lock,其他執行緒才能獲取lock執行由這個lock保護的程式碼塊。
·  可見性,執行緒開始執行的時候要獲取lock並且強制load主記憶體中的共享變數的最新值;執行緒釋放鎖的時候,必須要把工作記憶體中對共享變數的修改重新整理到主記憶體中去。
(2)Lock在多執行緒中的作用
在多執行緒中,不管在哪裡對共享變數的讀和寫,都需要由同一個鎖來保護。
1.3  volatile 變數
(1)volatile變數的性質
·  只能保證可見性
·  不能保證原子性
意味著複合操作需要用lock
·  不能保證有序性
(2)Volatile 與happens-before
·  對volatile變數的寫一定是發生在讀之間。
·  新的JMM強化了volatile變數的語意
對非volatile變數的寫,如果發生在對某個volatile變數的寫之前;那麼隨後在其他執行緒中讀volatile變數,也能讀到非volatile變數的最新值。所以volatile變數可以這麼用:

MapconfigOptions;

char[]configText;

volatileboolean initialized = false;

以上是一些共享變數

// In Thread A
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;
// In Thread B
while (!initialized) 
  sleep();
// use configOptions 
執行緒A中對configOptions初始化後,對於執行緒B是可見的。
(3)  Volatile變數的使用場景
·   寫一個volatile變數的值,不會依賴於當前值
·   Volatile變數不會參與物件的約束

2.      物件的釋出和逸出

2.1      釋出一個物件的引用

(1)    釋出物件應用意味著,把一個物件從當前的scope釋出到其他地方,比如:

·  把它儲存到hashMap這個資料結構中去,以便以後的程式碼能迭代hashMap訪問到hashMap中對物件

Public static Map<Secret> maps;

Public void init() {

         Maps= new HashMap<Secret>();

}

這樣其他方法中能拿到這個maps,然後迭代它,拿到每個Secret物件做操作。

·  通過一個public 的方法把一個原本私有的域釋出出去。

Private String[] names = {“hua”,”zhang”,”liu”};

Public String[] getNames() {

         Returnnames;

}

·  在構造器中,把this逸出

public class ThisEscape {

    publicThisEscape(EventSource source) {

       source.registerListener(

            newEventListener() {

               public void onEvent(Event e) {

                   doSomething(e);

                }

            });

    }

}

(2)    帶來的問題。

·  無法預測物件的狀態空間,不安全的釋出了物件的引用。意味了程式碼對於物件的狀態失去了控制;只要物件被不安全的釋出出去,那麼在任何執行緒中都可能會修改物件的內部狀態。如果一個物件狀態只有滿足某種約束才是合理的話,那麼不安全的釋出物件可能會破壞物件的內在約束。比如:

Public classNumberRange {

         Private int lower;

         Private int upper;

         Public NumberRange(int lower,int upper){

                   If(low > upper)

throw new IllegalArgumentException("lower: " +lower + " > " +"upper: "+upper);

                   This.lower = lower;

                   This.upper = upper;

}

Public void setLower(int lower) {

         This.lower = lower;

}

Public void setUpper(int upper){

         This.upper = upper;

}

}

如果NumberRange初始化完成後是(0,5),然後這個物件被不安全的釋出了,那麼其他執行緒就可能呼叫setLower,setUpper使得這個物件違反物件的約束。

2.2      如何正確的釋出一個物件的引用

2.2.1         多使用執行緒安全的不變類

·  不變類的物件的域,一旦呼叫構造器完成初始化工作後,其狀態就能在整個生命週期內保持不變。也就是不變類的狀態空間只有一個。所以,不變類的物件可以線上程中安全的共享。

·  另外不變類的物件可以很好的作為雜湊儲存結構的鍵值。因為不變類的狀態只有一個,所以其hashcode也就只有一個值,在不變類物件的整個生命週期中都不可以改變。所以可以很好的作為key值。(不會破壞HashMap的內在約束)。

2.2.2         對於可變物件,用鎖釋出可變物件的引用

在多執行緒壞境下,去分析可變物件的狀態空間比較複雜。因為只要不正確的釋出了物件的引用,那麼誰知道別人會怎麼使用這個物件呢?所以,為了保護可變物件內部的約束,必須恰當使用lock。例如上面的NumberRange

Public classNumberRange {

         Private int lower;

         Private int upper;

         Public NumberRange(int lower,int upper){

                   If(low > upper)

throw new IllegalArgumentException("lower: " +lower + " > " +"upper: "+upper);

                   This.lower = lower;

                   This.upper = upper;

}

Public synchronized void setLower(int lower) {

         If(low > upper)

throw new IllegalArgumentException("lower: " +lower + " > " +"upper: "+upper);

         This.lower = lower;

}

Public synchronized void setUpper(int upper){

If(low > upper)

throw new IllegalArgumentException("lower: " +lower + " > " +"upper: "+upper);

         This.upper = upper;

}

}

                   這樣,執行緒A呼叫setLower(4)時候看到的最新的NumberRange的範圍例如(0,5),由於某個時間段只能有一個執行緒進入同步塊,那麼線上程A執行的時候,執行緒B不能呼叫setUpper(3)使得NumberRange的狀態處於違反約束的情況下。(鎖性質:排斥性,可見性)。

2.2.3         在構造物件的時候,不要釋出物件的引用

3.      執行緒限制

可以用執行緒限制來避免不安全的共享可變物件。可變物件如果想在多執行緒壞境中共享,必須使用lock。

如果物件只在一個執行緒中使用,就不需要用鎖來保護物件。這樣的一種方式稱為執行緒限制;也就說,不能再執行緒中對外發布這個物件的引用。

實際上執行緒限制是一種用記憶體空間來換取執行緒安全性的方式,請看下面兩種執行緒限制。

(1)    棧限制

把對可變物件的訪問限制在單個限制中,並且不對外發布這個物件的引用,我們稱為棧限制。

public int loadTheArk(Collection<Animal> candidates) {
    SortedSet<Animal> animals;
    int numPairs = 0;
    Animal candidate = null;
    // animals confined to method, don't let them escape!
    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;
}

Animals這個引用指向的物件只能在loadTheArk這個方法中訪問到,並且animals引用不會對外發布,所以其他物件是不能拿到animals引用的。每個執行緒進入到這個方法中來的時候,都為得到一個新的animals物件;也就是說animals限制線上程的工作記憶體中是不對外共享的(主記憶體中沒有animals)。

為每一個使用該變數執行緒都提供一個變數值的副本,使每一個執行緒都可以獨立地改變自己的副本,而不會和其它執行緒的副本衝突。從執行緒的角度看,就好像每一個執行緒都完全擁有該變數。

對於ThreadLocal的說明請看:

4.      不可變類和volatile

當需要對一組相關的變數做一個原子性操作的時候,考慮把這些相關變數封裝在一個不可變類中。


@Immutable
class OneValueCache {
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;
    public OneValueCache(BigInteger i,
                         BigInteger[] factors) {
        lastNumber  = i;
        //對於可變域做保護性copy
        lastFactors = Arrays.copyOf(factors, factors.length);
    }
    public BigInteger[] getFactors(BigInteger i) {
        if (lastNumber == null || !lastNumber.equals(i))
            return null;
        else
            //可變域做保護性copy
            return Arrays.copyOf(lastFactors, lastFactors.length);
    }
}

使用不可變類構建的新類是執行緒安全的。

@ThreadSafe
//因為OneValueCache是不可變類,而不可變類是執行緒安全的
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);
        }
        encodeIntoResponse(resp, factors);
    }
}

用volatile來保證對cache指向的不可變類的物件,一旦有變化,那麼對於其他執行緒就是可見的。

5.      安全釋出共享物件

(1)    可變物件的共享

一旦要共享,必須用鎖來保證物件狀態的一致性。

(2)    不可變物件的共享

·  不可變類可以安全的在多執行緒壞境下共享