在多執行緒環境中安全的共享物件
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) 不可變物件的共享
· 不可變類可以安全的在多執行緒壞境下共享