java併發編程實踐學習(2)共享對象

一.可見性

當對象在從工作內存同步到主內存之前,那麼它就是不可見的。若有其他線程在存取不可見對象就會引發可見性問題,看下面一個例子:

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;  
}  

}

按照正常邏輯,應該會輸出42,但其實際結果會非常奇怪,可能會永遠沒有輸出(因爲ready爲false)一直循環,可能會輸出0(因爲重排序問題導致ready=true先執行)。
關於重排序:Java存儲模型允許編譯器重排序操作,在寄存器中緩存數值;還允許cpu重排序,並在處理器特有的緩存中緩存數值。跟多細節在後面java存儲模型中講

(1) 過期數據

上面的例子就演示了過期數據,當讀線程檢查ready變量時,可能看到一個過期的值,而且跟壞的情況是:一個線程獲取到了更新後的值,但是另一個線程獲取到的是過期的值。
過期的數據可能導致嚴重的混亂錯誤,比如以外的異常,髒數據結構,錯誤的計算和無線循環。
非線程安全的可變整數訪問器

@NotThreadSafe
public class MutableInteger{
private int value;

public int get(){
    return value;
}
public void set (int value) {
    this.value = value;
}

}
線程安全的可變整數訪問器

@NotThreadSafe
public class MutableInteger{
@GuardedBy private int value;

public synchronized int get(){
    return value;
}
public synchronized void set (int value) {
    this.value = value;
}

}
(2) 非原子的64位操作

java存儲模型要求獲取和存儲操作都是原子的,但在對沒有聲明爲volatile的64位數值變量(double和long),jvm允許將64位的讀或寫操作劃分爲倆個32位的操作,這種情況下在多線程中獲取數據你可能獲取到一個變量值不同時的高32位和一個低32位。
因此,在多線程中共享的long和double變量也是不安全的,你需要用鎖保護起來或者聲明爲volatile類型。

(3) 鎖和可見性

爲了保證所有線程都能看到共享的,可變的最新值,讀取和寫入必須使用公共的鎖進行同步。
這裏寫圖片描述
在上面例子中,線程A在同步塊中或者之前所做的操作都是對b可見的,但是沒有同步塊的話就沒有這樣的保證。

(4) Volatile變量

java提供了一種同步的弱形式:Volatile變量。
當一個域聲明爲Volatile類型之後,編譯器會監視這個變量:它是共享的,而且對它的操作不會與其他的內存操作儀器被重排序。Volatile變量不會緩存到寄存器或者在對於其他處理器隱藏的地方,所以讀Volatile類型的變量時,總會返回由一個線程所寫入的最新值。
Volatile比SynchronizedInteger更加輕量級。
加鎖能保證可見性和原子性;volatile變量只能保證變量可見性。
只有滿足下面標準才能使用volatile變量

寫入變量時並不依賴變量的當前值;或者能夠確保只有單一的線程修改變量的值
變量不需要與其他的狀態變量共同參與不變約束
而且,訪問變量時,沒有其他的原因需要加鎖
二. 發佈和逸出

發佈一個對象的意思是使它能夠被當前範圍之外的代碼使用。
如果發佈對象時他還沒有完成構造,就會威脅到多線程環境下的安全。
一個對象在其尚未準備好時就將他發佈,這種情況稱作逸出。
發佈對象

public static Set knownSecrets;

public void initalize(){
knownSecrets = new HashSet();
}

允許內部可變數據的逸出

class UnsafeStates{
private String[] states = new String[]{
“ak”,”al”,……
};
public String[] getStates(){
return states;
}
}

隱式的允許this引用逸出

public class ThisEscape{
    public  ThisEscape (EventSource source){
        source.registerListener(
              new EventListener(){
                    public void onEvent(Event e){
                        doSomething(e);
                    }
              }
        );
    }       
}

(1) 安全建構建的實踐

對象只有通過構造函數返回後,才處於可預言的、穩定的狀態,所以從構造函數內部發布的對象,只是一個未完成構造的對象。
如果this引用在構造過程中逸出,這樣的對象被認爲“沒有正確構建的”。
一個導致this引用在構造期間逸出的常見錯誤,實在構造函數中啓動一個線程。當對象在構造函數中創建了一個線程時,無論是顯示的(通過將它傳給構造函數)還是隱式的(因爲Thread或Runnable是所屬對象的內部類),this引用幾乎是被新線程共享。
在構造函數中創建線程並沒有錯誤,但是最好不要立即啓動它。取而代之的是發佈一個start或initialize方法來啓動對象擁有的線程 。
如果想在構造函數中註冊監聽器或者啓動線程,可以使用一個私有的構造函數和一個公共的工廠方法,這樣避免了不正確的構建。

public class SafeListener{
private final EventListener listener;

private SafeListener(){
    listener = new EventListener(){
         public void onEvent(){
             doSomething(e);
         }
    }
};
public static SafeListener newInstance(EventSource source){
    SafeListener safe = new SafeListener();
    source.registerListener(safe.listener);
    return safe;
}

}

三.線程封閉

訪問共享的,可變的數據要求使用同步。一個可以避免同步的方式就是不共享數據。線程封閉技術是實現線程安全的最簡單的方式之一。
常用的是swing的線程封閉技術和應用池化的JDBC Connection對象。
線程封閉是指運行在不同事件線程的其他線程中的代碼不應該訪問這些對象。
java自身和其核心庫提供了某些機制(本地變量和ThreadLocal類)有助於維護線程限制,但程序員自己也要負責確保。

(1)Ad-hoc線程限制

Ad-hoc線程限制指維護線程封閉性的職責完全由程序實現來承擔。
Ad-hoc線程封閉是非常脆弱的,因爲沒有任何一種語言特性,例如可見性修飾符或局部變量,能將對象封閉到目標線程上。
當決定使用線程封閉技術時,通常是因爲要將某個特定的子系統實現爲一個單線程子系統。

(2)棧限制

在棧限制中,只能通過本地變量才能觸及對象。本地變量被限制到執行線程中,其他線程無法訪問這個棧。
棧限制比ad-hoc線程更易維護,更健壯。

public int loadTheArk(Collection candidates) {
SortedSet 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;  

}
(3)ThreadLocal

一種維護線程限制的更加規範的方式是使用ThreadLocal.
ThreadLocal提供了get和set訪問器,爲每個使用它的線程維護一份單獨的拷貝,所以get總是返回當前執行線程通過set設置的最新值。而且ThreadLocal變量有自己的初始化方法。 當某個線程初次調用ThreadLocal.get方法時,就會調用initialValue來獲取初始值。
使用ThreadLocal確保線程封閉性

private static ThreadLocal<Connection> connectionHoder = new ThreadLocal<Connection>(){
    public Connection initialValue(){
        return DriverManager.getConnection(DB_URL);
    } 
};
public static Connection getConnection(){
    return connectionHolder.get();
}

上面的例子通過將JDBC的連接保存到ThreadLocal對象中,每個線程都會擁有屬於自己的連接。
這項技術還用於頻繁執行的操作既需要buffer這樣的零時對象,同時還需要避免每次都衝分配該零時對象。

四.不可變性

只有滿足如下狀態一個對象纔是不可變的

它的狀態不能再創建之後在修改
所有的域都是final類型
他被 正確的創建(在創建期間沒有發生this引用的逸出)
(1) final域

final域是不能修改的(儘管如果final域指向的對象是可變的,這個對象任然可以修改)
將所有域設置爲私有的,除非他們需要更高的可見性
將所有域設置爲final,除非他們是可變的

(2) 使用volatile發佈不可變對象

五.安全發佈

我們讓對象限制在線程中或者另一個對象內部,我們又的確希望跨線程共享對象,這時我們必須安全共享它。
————————-安全發佈對象的條件:—————————-
1、通過靜態初始化對象的引用;
2、將引用存儲到volatile變量或AutomaticReference;
3、將引用存儲到final域字段中;
4、將引用存儲到由鎖正確保護的域中;
Java中支持線程安全保證的類:
1、置入HashTable、synchronizedMap、ConcurrentMap中的主鍵以及值,會安全地發佈到可以從Map獲取他們的任意線程中,無論是直接還是通過迭代器(Iterator)獲得。
2、置入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或者synchronizedSet中的元素,會安全地發佈到可以從容器中獲取它的任意線程中。
3、置入BlockingQueue或ConcurrentLinkedQueue的元素,會安全地發佈到可以從容器中獲取它的任意線程中。
發佈對象必要條件依賴於對象的可變性:
1、不可變對象可以依賴任何機制發佈;
2、高效不可變對象必須安全地發佈;
3、可變對象必須要安全發佈,同時必須要線程安全或被鎖保護。
安全的共享對象
線程限制:一個線程限制的對象,通過限制在線程中,而被線程獨佔,且只能被佔有它的線程修改。
共享只讀(read-only):一個共享的只讀對象,在沒有額外同步的情況下,可以被多個線程併發地訪問,但任何線程都不能修改它。共享只讀對象包括可變對象與高效不可變對象。
共享線程安全(thread-safe)————-
一個線程安全的對象在內部進行同步,所以其它線程無額外同步,就可以通過公共接口隨意地訪問它。
被守護(Guarded):
一個被守護的對象只能通過特定的鎖來訪問。被守護的對象包括哪些被線程安全對象封裝的對象,和已知特定的鎖保護起來的已發佈對象。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章