java併發編程實戰閱讀筆記(第三章)對象的共享

如何共享和發佈對象,從而使他們能夠安全地由多個線程訪問。

可見性

可見性:
  可見性是一種複雜的屬性,因爲可見性中的錯誤總是會違揹我們的直覺。通常,我們無法確保執行讀操作的線程能適時地看到其他線程寫入的值,有時甚至是根本不可能的事情。爲了確保多個線程之間對內存寫入操作的可見性,必須使用同步機制。

  可見性,是指線程之間的可見性,一個線程修改的狀態對另一個線程是可見的。也就是一個線程修改的結果。另一個線程馬上就能看到。比如:用volatile修飾的變量,就會具有可見性。volatile修飾的變量不允許線程內部緩存和重排序,即直接修改內存。所以對其他線程是可見的。但是這裏需要注意一個問題,volatile只能讓被他修飾內容具有可見性,但不能保證它具有原子性。比如 volatile int a = 0;之後有一個操作 a++;這個變量a具有可見性,但是a++ 依然是一個非原子操作,也就是這個操作同樣存在線程安全問題。

  在 Java 中 volatile、synchronized 和 final 實現可見性。

原子性:

  原子是世界上的最小單位,具有不可分割性。比如 a=0;(a非long和double類型,爲何要強調非long和double類型?下面有說明) 這個操作是不可分割的,那麼我們說這個操作時原子操作。再比如:a++; 這個操作實際是a = a + 1;是可分割的,所以他不是一個原子操作。非原子操作都會存在線程安全問題,需要我們使用同步技術(sychronized)來讓它變成一個原子操作。一個操作是原子操作,那麼我們稱它具有原子性。java的concurrent包下提供了一些原子類,我們可以通過閱讀API來了解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。

  在 Java 中 synchronized 和在 lock、unlock 中操作保證原子性。

有序性:

  Java 語言提供了 volatile 和 synchronized 兩個關鍵字來保證線程之間操作的有序性,volatile 是因爲其本身包含“禁止指令重排序”的語義,synchronized 是由“一個變量在同一個時刻只允許一條線程對其進行 lock 操作”這條規則獲得的,此規則決定了持有同一個對象鎖的兩個同步塊只能串行執行。一個線程安全的可變整數類樣例:

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

最低安全性:
當線程在沒有同步的情況下讀取變量時,可能會得到一個失效值,但至少這個值是由之前某個線程設置的值,而不是一個隨機值,這種安全性保證也被稱爲最低安全性。

非原子的64位操作:
即使不考慮數據失效性,在多線程中使用共享且可變的long 和 double等類型的變量也是不安全的,除非用volatile來聲明他們,或者永鎖保護起來。因爲JVM允許將64位的讀操作或寫操作分解成兩個32位來操作,當讀取一個非volatile類型的long變量時,可能會讀到某個值的高32位和另一個值的低32位。

volatile變量
Java語言提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操作通知到其他線程。**當把變量聲明爲volatile類型後,編譯器與運行時都會注意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序。**volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。

  在訪問volatile變量時不會執行加鎖操作,因此也就不會使執行線程阻塞,因此volatile變量是一種比sychronized關鍵字更輕量級的同步機制。

瞭解一下多線程情況下對變量的操作:
這裏寫圖片描述
當對非 volatile 變量進行讀寫的時候,每個線程先從內存拷貝變量到CPU緩存中。如果計算機有多個CPU,每個線程可能在不同的CPU上被處理,這意味着每個線程可以拷貝到不同的 CPU cache 中。

而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內存中讀,跳過 CPU cache 這一步。
當一個變量定義爲 volatile 之後,將具備兩種特性:

  1.保證此變量對所有的線程的可見性,這裏的“可見性”,如本文開頭所述,當一個線程修改了這個變量的值,volatile 保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新。但普通變量做不到這點,普通變量的值在線程間傳遞均需要通過主內存(詳見:Java內存模型)來完成。

  2.禁止指令重排序優化。有volatile修飾的變量,賦值後多執行了一個“load addl $0x0, (%esp)”操作,這個操作相當於一個內存屏障(指令重排序時不能把後面的指令重排序到內存屏障之前的位置),只有一個CPU訪問內存時,並不需要內存屏障;(什麼是指令重排序:是指CPU採用了允許將多條指令不按程序規定的順序分開發送給各相應電路單元處理)。

volatile 性能:
  volatile 的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因爲它需要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。

發佈與逸出

一、對象的發佈和逸出

發佈(publish)對象意味着其作用域之外的代碼可以訪問操作此對象。例如將對象的引用保存到其他代碼可以訪問的地方,或者在非私有的方法中返回對象的引用,或者將對象的引用傳遞給其他類的方法。
爲了保證對象的線程安全性,很多時候我們要避免發佈對象,但是有時候我們又需要使用同步來安全的發佈某些對象。
逸出即爲發佈了本不該發佈的對象。
使用靜態變量引用對象是發佈對象最直觀和最簡單的方式。例如以下代碼示例,在示例中我們也看到,由於任何代碼都可以遍歷我們發佈persons集合,導致我們間接的發佈了Person實例,自然也就造成可以肆意的訪問操作集合中的Person元素。

public class ObjectPublish {

    public static HashSet<Person> persons ;

    public void init()
    {
        persons = new HashSet<Person>();
    }

}

在非私有的方法內返回一個私有變量的引用會導致私有變量的逸出,例如以下代碼

public class ObjectPublish {    
    private  HashSet<Person> persons=  new HashSet<Person>();
    public HashSet<Person>  getPersons()
    {
        return this.persons;
    }

}

發佈一個對象也會導致此對象的所有非私有的字段對象的發佈,其中也包括方法調用返回的對象。
在構造函數中使用直接初始化或者調用可改寫的實例方法都會導致隱式的this逸出也是經常發生的事情,例如以下代碼,在EventListener的實例中也通過this隱含的發佈了尚未構造完成的ConstructorEscape實例,可能會造成無法預知的結果。

public class ConstructorEscape {
    public ConstructorEscape(EventSource eventSource)
    {
        eventSource.registerListener(
                    new EventListener(){
                        public void OnEvent(Event e)
                        {
                            doSomeThing(e);
                        }
                    }
                );
    }
}

我們可以使用工廠方法防止隱式的this逸出問題,例如以下代碼


public class ConstructorEscape {
    private final EventListener listener;

    private  ConstructorEscape()
    {
        this.listener=    new EventListener(){
            public void OnEvent(Event e)
            {
                doSomeThing(e);
            }
        };        
    }

    public static ConstructorEscape getInstance(EventSource eventSource)
    {
        ConstructorEscape  instance = new ConstructorEscape();
        eventSource.registerListener(instance.listener);
        return instance;
    }    
}

二、避免同步之線程封閉

線程封閉可以使數據的訪問限制在單個線程之內,相對鎖定同步來說,其實實現線程安全比較簡單的方式。
java提供了ThreadLocal類來實現線程封閉,其可以使針對每個線程存有共享狀態的獨立副本。其通常用於防止對可變的單實例變量和全局變量進行共享,例如每個請求作爲一個邏輯事務需要初始化自己的事務上下文,這個事務上下文應該使用ThreadLocal來實現線程封閉。

public class SynchronizedInteger {
    private ThreadLocal<Long> value;

    public Long get(){
        return this.value.get();
    }

    public void set(Long value){
        this.value.set(value);
    }
}

棧封閉是線程封閉的特例,即數據作爲局部變量封閉在執行線程中,對於值類型的局部變量不存在逸出的問題,如果是引用類型的局部變量,開發人員需要確保其不要作爲返回值或者其他的關聯引用等而被逸出。

三、避免同步之不變性

某個對象創建之後就不能修改其狀態,那麼我們就說這個對象是不可變對象。
由於多線程操作可變狀態會導致原子性、可見性一系列問題,所以線程安全性是不可變對象與生俱來的特性。
不可變對象由構造函數初始化狀態,並可以安全的傳遞給任何不可信代碼使用。
所有字段標記爲final的對象,由於引用字段的對象可能可以直接修改,所以其並不一定是不可變對象,其需要滿足以下條件
1)對象的所有字段都用final標記
2)對象創建之後任何狀態都不能修改
3)對象不存在this隱式構造函數逸出

public class ThreeStooges {

    private final Set<String> stooges = new HashSet<String>();

    public ThreeStooges(){
        stooges.add("Moe");
        stooges.add("Larry");
        stooges.add("Carry");
    }

    public boolean isStooge(String name){
        return stooges.contains(name);
    }
}

四、對象的安全發佈

很多時候我們是希望在多線程之間共享數據的,此時我們就必須確保安全的發佈共享對象。
要安全的發佈一個對象,對象的引用以及對象的狀態對其他線程都是可見的,一個正確構造的對象可以通過以下方式安全的發佈
1)在靜態構造函數中初始化對象引用
2)使用volatile和AtomicReferance限定對象引用
3)使用final限定對象引用
4)將對象引用保存到有鎖保護的字段中

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