從0學習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();
            new ReaderThread().start();
            new ReaderThread().start();
            number = 42;
            ready = true;
    }
}
  • 這段代碼可能出現的結果java

    • 輸出0: 由於ReaderThread可能看到了ready的值,但卻沒看到number的值。
    • 持續的循環下去:由於可能很長時間內,ReaderThread都沒法看到ready的值。
    • 輸出42: ReaderThread同時看到了number和ready的值。
在沒有同步的狀況下,編譯器,處理器以及運行時等均可能對操做的執行順序進行一些意想不到的調整,在缺少足夠同步的多線程程序中,要想對內存操做執行順序進行判斷,幾乎沒法得出正確的結論。

失效數據

當ReaderThread查看ready變量時,可能會獲得一個已經失效的值,並且失效值可能不會同時出現:一個線程可能得到了某個變量的最新值,而得到了另外一個變量的失效值。
  • 在SynchronizedInteger中,經過對get和set方法進行同步,使其成爲一個線程安全類(須要將對象getter/setter方法都進行同步)。

非原子的64位操做

最低安全性:在沒有進行同步時讀取某個變量,可能會獲得一個失效值,但這個值至少是由以前某個線程設置的,而非隨機值。這種安全性保證也被稱爲最低安全性。
  • 不符合最低安全性的變量:非volatile類型的64位數值變量(double和long)
因爲Java內存模型要求,變量的讀取操做和寫入操做必須都是原子操做,但對於非volatile類型的long和double變量,JVM容許將64位的讀操做或寫操做分解爲兩個32位操做。
  • 因此當讀取一個非volatile類型的long變量時,若是對該變量的讀操做和寫操做在不一樣的線程中進行,那麼可能會只讀取到這個變量的高32位或者低32位。
  • 因此在多線程中,double和long須要用volatile聲明,或者用鎖保護起來。

加鎖和可見性

  • 加鎖的含義不只僅侷限於互斥行爲,還包括內存的可見性。爲了確保全部線程都能看到共享變量的最新值,全部執行讀操做或者寫操做的線程都必須在同一個鎖上同步。

volatile變量

  • java提供了一種稍弱的同步機制:volatile變量,用來確保將變量的更新操做通知到其餘線程。當把變量聲明位volatile類型後,編譯器與運行時都會足以到這個變量是共享的。所以不會將該變量上的操做與其餘內存操做一塊兒重排序。volatile變量不會被緩存在寄存器或者其餘對處理器不可見的地方,所以在讀取volatile類型的變量時總會返回最新寫入的值。
  • 從內存的可見性來看,讀取volatile至關於進入同步代碼塊,寫入volatile變量至關於退出同步代碼塊。
  • 可是不建議過分使用volatile提供的可見性,若是代碼中依賴volatile變量來控制可見性,一般比使用鎖的代碼更脆弱,也更難理解。
  • volatile正確的使用方式:確保它們自身狀態的可見性確保它們所引用對象狀態的可見性,以及表示一些重要的聲明週期事件的發生(例如初始化,關閉,循環退出條件等。)
/**
* 數綿羊
*/
volatile boolean asleep;
while(!asleep){
    countSomeSheep();
}
  • 加鎖機制既能保證可見性,又能夠確保原子性。而volatile變量只能保證可見性
  • 當且僅當知足如下全部條件時,才應該使用volatile變量:編程

    • 對變量的寫入操做不依賴變量的當前值,或者你能確保只有單個線程更新變量的值。
    • 該變量不會與其餘狀態變量一塊兒歸入不變性條件中。
    • 在訪問變量時不須要加鎖。

發佈與溢出

  • 發佈(publish)一個對象,指的是對象可以在當前做用域以外的代碼中使用。數組

    • 例如,將一個指向該對象的引用緩存

      • 保存到其餘代碼能夠訪問的地方
      • 或者在某一個非私有的方法中返回該引用。
      • 或者即哪一個引用傳遞到其餘類的方法中。
  • 但若是在發佈時要確保線程安全性,則可能須要同步。發佈內部狀態可能會破壞封裝性,並使程序難以維持不變性條件。安全

    • 例如在對象構造完成以前就發佈該對象,就會破壞線程安全性。
    • 當某個不該該發佈的對象被髮布時,這種狀況就被稱爲逸出(Escape)
public class UnsafeState {

    private String[] states = new String[]{
            "A","B","C","D","E"
    };
    public String[] getStates(){
        return states;
    }
}
  • 任何調用者均可以修改states裏的內容,數組states已經逸出了它所在的做用域:由於做爲私有變量的內容已經被髮布了。
  • 當發佈一個對象時,在該對象的非私有域中的對象一樣會被髮布。
  • 若是一個已經發布的對象可以經過非私有的變量引用和方法調用到達其餘對象,那麼這些對象也會被髮布。
  • 當某個對象逸出後,你必須假設有某個類或者線程在誤用該對象。這正是使用封裝最主要的緣由:封裝可以使得對程序的正確性進行分析變得可能,並使得無心中破壞設計約束條件變得更難。

安全的對象構造過程

  • 不要在構造過程當中使this引用逸出。
  • 當且僅當對象的構造函數返回時,對象才處於可預測和一致的狀態。

線程封閉

  • 當訪問共享的可變數據時,一般須要使用同步。
  • 避免使用同步的方式就是不共享數據,若是僅在當線程內訪問數據,就不須要同步。這種技術就被稱爲線程封閉(Thread Confinement)
  • JDBC的Connection對象就使用了線程封閉技術。在典型的服務器應用程序中,線程從JDBC鏈接池中得到一個Connection對象,而且用該對象來處理請求,使用完後再將對象返回給鏈接池。服務器

    • 因爲大多數請求(如Servlet)都是由單線程採用同步的方式來處理,而且在Connection對象返回以前,鏈接池不會再將它分給其餘線程。所以這種鏈接管理模式實際上是隱式的將Connection對象封閉在線程中。

Ad-hoc線程封閉

Ad-hoc線程封閉是指,維護線程封閉性的職責所有由程序實現來承擔。
  • Ad-hoc線程封閉是很是脆弱的,由於沒有一種語言特性,例如可見性修飾符或局部變量,可以將對象封閉到目標線程上。
  • 當決定使用線程封閉技術時,一般是由於要將某個特定的子系統實現爲一個單線程子系統。在某些狀況下,單線程子系統提供的簡便性要賽過Ad-hoc線程封閉技術的脆弱性。
  • 因爲Ad-hoc的脆弱性,所以在程序裏儘可能少使用,儘量的使用更強的線程封閉技術(如棧封閉和ThreadLocal類)

棧封閉

棧封閉是線程封閉的一種特例,在棧封閉中,只能經過局部變量才能訪問對象。局部變量的特性之一就是封閉在執行線程中。它們位於執行線程的棧中,其餘線程沒法訪問這個棧。

ThreadLocal類

維護線程封閉性的一種更規範方法是使用ThreadLocal,這個類能使線程中的某個值與保存值的對象關聯起來。ThreadLocal提供了get/set等訪問接口和方法,

ThreadLocal是什麼

ThreadLocal是一個建立線程局部變量的類。多線程

使用了ThreadLocal建立的變量只能被當且線程訪問,其餘線程沒法訪問和修改。併發

ThreadLocal的使用

private void testThreadLocal() {
    Thread t = new Thread() {
        ThreadLocal<String> mStringThreadLocal = new ThreadLocal<>();

        @Override
        public void run() {
            super.run();
            mStringThreadLocal.set("123");
            mStringThreadLocal.get();
        }
    };

    t.start();
}

爲ThreadLocal設置初始值的話,則須要重寫initialValue方法:框架

ThreadLocal<String> mThreadLocal = new ThreadLocal<String>() {
    @Override
    protected String initialValue() {
      return Thread.currentThread().getName();
    }
};

對象存放

本質上ThreadLocal是在堆上建立對象,可是將對象引用持有在線程的棧內存上。ide

許多事務性的框架功能,經過將事務的上下文保存在靜態的ThreadLocal對象中,當須要判斷是哪個事務時,只須要從ThreadLocal對象中讀取事務上下文便可。

不變性

知足同步需求的另外一種方法是使用不可變對象(Immutable Object),以前的例如獲得失效數據,丟失更新操做或者觀察到某個對象處於不一致的狀態等問題,都與多線程試圖同時訪問一個可變變量有關,若是這個變量是不可變的,那麼這些問題也就天然消失了。

不可變對象必定是線程安全的

對象不可變的條件

當知足如下條件時,對象纔是不可變的:

  • 對象建立後其狀態就不可修改。
  • 對象的全部域都是final類型。
  • 對象是正確建立的(對象建立期間,this引用沒有逸出)

final域

關鍵字final用於構造不可變對象。final類型的域是不可修改的,但若是final域所引用的對象是可變的,那麼這些被引用的對象是能夠修改的。

在java的內存模型中,final域還有特殊的語義:final域能確保初始化過程的安全性,從而不受限制的訪問不可變對象,並在共享這些對象時無需同步。

  • 正如除非須要更高的可見性,不然應將全部的域都聲明爲私有域是個優秀的編程習慣同樣,除非須要某個域是可變的,不然都應該聲明爲final域也是一個良好的編程習慣。

安全發佈

在某些狀況下,咱們須要在多個線程之間共享對象,此時必須確保安全地進行共享

不正確的發佈

不能期望一個未被徹底建立的對象擁有完整性。

public class Holder {
    private int n;

    public Holder(int n) {
        this.n = n;
    }

    public void assertSanity() {
        if (n != n) {
            throw new AssertionError("this statement is false");
        }
    }
}

在發佈Holder的線程發佈完成以前,Holder域是個失效值,此時的n多是空引用。

不可變對象與初始化安全性

Java內存模型對不可變對象的共享提供了一種特殊的初始化安全性保證。

任何線程均可以在不須要額外同步的狀況下安全的訪問不可變對象,即便在發佈這些對象的時候沒有使用同步

若是final類型的域所指向的是可變對象,那麼在訪問這些域所指向的對象的狀態時仍需同步。

安全發佈的經常使用模式

要安全的發佈一個對象,對象的引用以及對象的狀態必須同時對其餘線程可見。一個正確構造的對象能夠經過如下方式來安全的發佈:

  • 在靜態初始化函數中初始化一個對象引用。
  • 將對象的引用保存到volatile類型的域或者atomicReferance對象中。
  • 將對象的引用保存到某個正確構造對象的final類型域中。
  • 將對象的引用保存到一個由鎖保護的域中。

線程安全庫中的容器提供的安全發佈保證:

  • 經過將一個鍵或一個值放入HashtablesynchonizedMap,ConcurrentMap中,能夠安全的將它發佈給任何從這些容器訪問它的線程(不論直接訪問仍是迭代器訪問)
  • 經過將某個元素放入Vector,CopyOnWriteArrayList,CopyOnWriteArraySet,SynchonizedList,SynchonizedSet中,能夠將元素安全地發佈到任何從這些容器中訪問該元素的線程。
  • 經過將某個元素放入BlockingQueue或者ConcurrentLinkedQueue中,能夠將元素安全地發佈到任何從這些隊列中訪問該元素的線程。

一般,要發佈一個靜態構造的對象,最簡單和最安全的方式就是使用靜態的初始化構造器。

public static Holder holder = new Holder(1);
因爲靜態初始化構造器由JVM在類的初始化階段執行,在JVM內部存在着同步機制,所以經過這種方式初始化的任何對象均可以被安全的發佈。

事實不可變對象

  • 即使對象從技術上來看是可變的,可是其狀態在發佈後不會被改變,就是事實不可變對象(Effectively Immutable Object)
  • 在沒有額外同步的狀況下,任何線程均可以安全地使用被安全發佈的事實不可變對象。

可變對象

  • 若是對象在構造後能夠被修改,那麼安全發佈只能保證發佈當時的可見性。對象的發佈需求取決於它的可變性:

    • 不可變對象能夠經過任意機制發佈。
    • 事實不可變對象必須經過安全方式來發布。
    • 可變對象必須經過安全方式來發布,而且必須是線程安全的,或者由某個鎖保護起來。

安全的共享對象

在併發程序中使用和共享對象的時候,可使用一些實用的策略,包括:

  • 線程封閉:線程封閉的對象只能由一個線程擁有,對象封閉在該線程中,而且只能由這個線程修改。
  • 只讀共享:在沒有額外同步的狀況下,共享的只讀對象能夠由多個線程併發訪問,但任何線程都不能修改它。共享的只讀對象包括不可變對象和事實不可變對象。
  • 線程安全共享:線程安全的對象在其內部實現同步,所以多個線程能夠經過對象的公有接口來進行訪問而不須要進一步的同步。
  • 保護對象:被保護的對象只有經過持有特定的鎖來訪問。保護對象包括封裝在其餘線程安全對象中的對象,以及發佈的而且由某個特定鎖保護的對象。
相關文章
相關標籤/搜索