第三章-對象的共享

3.1 可見性

  • 重排序
3.1.1 失效數(shù)據(jù)
只要在某個線程中無法檢測到重排序情況,無法確保線程中的操作按照程序中指定的順序來執(zhí)行
  • 只要數(shù)據(jù)在多個線程中共享,那么就要使用正確的同步
public class NoVisiblity {
    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;
    }
}

上方程序在多線程情況下可能出現(xiàn)N中情況:

  1. 正常輸出42
代碼按預想的順序執(zhí)行
  1. 一直循環(huán),無法輸出數(shù)字
讀線程永遠讀不到ready的值
  1. 輸出數(shù)字0
讀線程看到了ready的值,沒看到之后寫入number的值

上方程序演示了,當讀線程查看ready變量時,可能會得到一個已經(jīng)失效的值。更糟糕的是,失效值可能不會同時出現(xiàn):一個線程可能獲得某個變量的更新值,而獲得另一個變量的失效值

3.1.2 非原子的64位操作
  • 非volatile類型的64位數(shù)值變量(double和long)
JVM允許將64位的讀操作或寫操作分解為兩個32位的操作。顧名思義如果對該變量的讀操作和寫操作在不同的線程中執(zhí)行,那么很可能會讀取到某個值的高32位和另一個值得低32位,到底數(shù)值不準確。因此在多線程程序中使用共享且可變的long和double等類型的變量時,應該使用volatile聲明它們或者用鎖保護起來。
3.1.3 加鎖與可見性
  • 加鎖的含義不僅僅局限于互斥行為,還包括可見性,為了確保所有線程都能看到共享變量的最新值,所有執(zhí)行讀操作或寫操作的線程都必須在同一個鎖上同步
3.1.4 Volatile變量
  • 加鎖機制即可保證可見性還可以保證原子性,volatile只能保證可見性
  • 把變量聲明為volatile后,不會將該變量上的操作與其他內(nèi)存操作一起重排序。volatile變量不會緩存在寄存器或者其他對處理器不可見的地方,因此在讀取volatile變量時總會返回最新寫入的值。
  • volatile正確使用方式
- 確保它們自身狀態(tài)的可見性
- 確保它們所引用對象的狀態(tài)的可見性
- 標識一些重要程序的生命周期事件的發(fā)生(例如:初始化或關閉)
  • 當且僅當滿足下列條件,才可以使用volatile變量:
- 對變量的寫入操作不依賴于變量的當前值,或者確保只有單個線程更新變量的值
- 該變量不會與其他狀態(tài)變量一起納入不變性條件中
- 在訪問變量時不需要加鎖

3.2 發(fā)布與逸出

  • 發(fā)布
使對象能在當前作用域之外的代碼中使用
  • 逸出
當某個不應被發(fā)布的對象被發(fā)布了
  • 不要在構造過程中使this引用逸出,否則認為是不正確構造

  • 錯誤做法

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

當從對象構造函數(shù)中發(fā)布對象時,只是發(fā)布了一個尚未構造完成的對象。如果是構造過程中創(chuàng)建一個線程,并且使this引用逸出,然后創(chuàng)建的線程在對象構造完成前就啟動了,就可能使用了尚未構造完全的對象,會導致預想不到的錯誤。

  • 正確構造
public class SafeListener{
    private final EventListener listener;
    
    private SafeListener(){
        listener=new EventListener(){
           public void onEvent(Event e){
               doSomething(e);
           } 
        };
    }
    
    public static SafeListener newInstance(EventSource source){
        SafeListenter safe=new SafeListener();
        source.registerListener(safe.listener);
        return safe;
    }
}

3.3 線程封閉

  • 僅在單線程內(nèi)部訪問數(shù)據(jù),實現(xiàn)線程安全性最簡單的方式之一
    • 例:JDBC的Connection對象
3.3.1 Ad-hoc線程封閉
  • 維護線程封閉性的職責完全由程序實現(xiàn)來承擔(非常脆弱)
- 沒有一種語言特性,能將對象封閉到目標線程上。
- 盡量少使用Ad-hoc線程封閉,應該使用棧封閉或者ThreadLocal類等更強的線程封閉技術硬
3.3.2 棧封閉
  • 只有通過局部變量才能訪問對象。
- 局部變量的固有屬性之一就是封閉在執(zhí)行線程中。
- 它們位于執(zhí)行線程的棧中,由于棧是線程私有,所以其他線程無法訪問這個棧
  • 示例:
public int loadTheArk(Collection<Animal> candidates){
    SortedSet<Animal> animals;
    int numPairs=0;
    Animal candidate=null;
    
    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.3.3 ThreadLocal類
  • ThreadLocal類能使線程中的某個值與保存值得對象關聯(lián)起來。
  • ThreadLocal提供了get和set等訪問接口或方法,這些方法為每個使用該變量的線程都存有一份獨立的副本,因此get總是返回由當前執(zhí)行線程在調(diào)用set時設置的最新值。
-當某個線程初次調(diào)用ThreadLocal.get方法時,就會調(diào)用initialValue來獲取初始值
- 可以將ThreadLocal<T>視為包含了Map<Thread,T>對象,其中保存了特定于該線程的值,當線程終止后,這些值會作為垃圾回收
  • ThreadLocal變量類似于全局變量,能降低代碼的可重用性,并在類之間引入隱含的耦合性,使用時要格外小心

3.4 不變性

  • 不可變對象(一定是線程安全的)
某個對象被創(chuàng)建后其狀態(tài)就不能被修改
  • 不可變性并不等同于將對象中所有的域聲明為final
即使對象中所有的域都是final類型的,這個對象也仍然是可變的,因為在final類型的域中可以保存對可變對象的引用
  • 對象不可變的條件
- 對象創(chuàng)建后其狀態(tài)不能被修改
- 對象的所有域都是final類型
- 對象是正確創(chuàng)建的(在對象構造期間,this引用沒有逸出)
  • 在可變對象基礎上構建不可變類
@Immutable
public final class ThreeStooges{
    private final Set<String> stooges=new HashSet<String>();
    
    public ThreeStooges(){
        stooges.add("Moe");                stooges.add("Larry");
        stooges.add("Curly");
    }
    
    public boolean isStooge(String name){
        return stooges.contains(name);
    }
}
  • 不可變對象與不可變引用
保存在不可變對象中的程序狀態(tài)仍可被更新,即通過將一個保存新狀態(tài)的實例來“替換”原有的不可變對象
3.4.1 Final域
  • 除非需要更高的可見性,否則應將所有的域聲明為私有域
  • 除非需要某個域是可變的,否則應將其聲明為final域
3.4.2 使用volatile發(fā)布不可變對象
  • 不可變對象額外的性能優(yōu)勢
降低了對鎖和防御性拷貝的需要,減少了后續(xù)垃圾回收產(chǎn)生的沖突
  • 示例
@ThreadSafe
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);
    }
}

@Immutable
public class OneValueCache{
    private final BigInteger lastNumber;
    private final BigInteger[] lastFactors;
    
    public OneValueCache(BigInteger i,
                        BigInteger[] factors){
        lastNumber=i;
        lastFactors=Arrays.copyOf(factors,factors.length);
    }
    
    public BigInteger[] getFactors(BigInteger i){
        if(lastNumber==null||!lastNumber.equals(i)){
            return null;
        }else{
            return Arrays.copyOf(lastFactors,lastFactors.length);
        }   
    }
}

VolatileCachedFactorizer利用OneValueCache存儲緩存的數(shù)字及其因數(shù)。當一個線程設置volatile類型的cache域并引用到一個新的OneValueCache后,新數(shù)據(jù)會立即對其他線程可見。
與cache相關的操作不會互相干擾,因為OneValueCache是不可變的,而且每次只有一條相應的代碼路徑訪問它。

  • 以下兩個前提保證了即使VolatileCachedFactorizer沒有顯示地用到鎖,但仍然是線程安全的。
    • 不可變的容器對象(OneValueCache對象)持有與不變約束相關的多個狀態(tài)變量(lastNumber,lastFactors)
    • 利用volatile引用確保及時的可見性

3.5 安全發(fā)布

3.5.1 不正確的發(fā)布:正確的對象被破壞
  • 示例(由于未被正確發(fā)布,這個類可能出現(xiàn)故障)
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.");
        }
    }
}
  • 除了發(fā)布對象的線程外,其他線程可以看到的Holder域是一個失效值,因此將看到一個空引用或者之前的舊值
  • 更糟的情況是,線程看到Holer引用的值是最新的,但Holder狀態(tài)的值卻是失效的
3.5.2 不可變對象與初始化安全性
  • 任何線程都可以在不需要額外同步的情況下安全地訪問不可變對象,即使在發(fā)布這些對象時沒有使用同步
  • 任何線程在不需要額外同步的情況下安全地訪問final類型的域,然而,如果final類型的域指向的是可變對象,那么在訪問這些域所指向的對象的狀態(tài)時仍然需要同步
3.5.3 安全發(fā)布的常用模式
  • 一個正確構造的對象可以通過以下方式來安全地發(fā)布
    • 在靜態(tài)初始化函數(shù)中初始化一個對象的引用
    • 將對象的引用保存到volatile類型的域或者AtomicReference對象中
    • 將對象的引用保存到某個正確構造的對象的final類型域中
    • 將對象的引用保存到一個由鎖保護的域中
3.5.4 事實不可變對象
  • 事實不可變對象
對象從技術上來看是可變的,但其狀態(tài)在發(fā)布后不會再改變
  • 在沒有額外同步的情況下,任何線程都可以安全地使用被安全發(fā)布的事實不可變對象
3.5.5 可變對象
  • 如果對象在構造后可以修改,那么安全發(fā)布只能確保“發(fā)布當時”狀態(tài)的可見性
  • 對象的發(fā)布需求取決于它的可變性
    • 不可變對象可以通過任意機制來發(fā)布
    • 事實不可變對象必須通過安全方式來發(fā)布
    • 可變對象必須通過安全方式來發(fā)布,并且必須是線程安全的或者由某個鎖保護起來
3.5.6 安全地共享對象
  • 當發(fā)布一個對象時,必須明確說明對象的訪問方式
  • 在并發(fā)程序中使用和共享對象時,可以使用一些實用的策略,包括:
- 線程封閉
    - 線程封閉的對象只能由一個線程擁有,對象被封閉在該線程中,并且只能由這個線程修改
- 只讀共享
    - 在沒有額外同步的情況下,共享的只讀對象可以由多個線程并發(fā)訪問,但任何線程都不能修改他。共享的只讀對象包括不可變對象和事實不可變對象
- 線程安全共享
    - 線程安全的對象在其內(nèi)部實現(xiàn)同步,因此多個線程可以通過對象的公有接口來進行訪問而不需要進一步的同步
- 保護對象
    - 被保護的對象只能通過持有特定的鎖來訪問。保護對象包括封裝在其他線程安全對象中的對象,以及已發(fā)布的并且由某個特定鎖保護的對象
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容