天天看點

對象的共享1 可見性2 釋出與逸出3 線程封閉4 不變性5 安全釋出

1 可見性

通常,我們無法保證執行讀操作的線程能看到其他線程寫入的值,因為每個線程都由自己的緩存機制。為了確定多個線程之間對記憶體寫入操作的可見性,必須使用同步機制。

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的值;更奇怪的是可能輸出0,因為讀線程看到了寫入ready的值,卻沒有看到之後寫入number的值,這種現象稱為“重排序”(Reordering).

在沒有同步的情況下,編譯器、處理器以及運作時等都可能對操作的執行順序進行一些意想不到的調整.

有種簡單方法避免這些複雜的問題:隻要有資料在多個線程之間共享,就該使用正确的同步.

1.1 失效資料

除非在每次通路變量時使用同步,否則很可能獲得變量的一個失效值。失效值可能不會同時出現:一個線程可能獲得一個變量的最新值,而獲得另一個變量的失效值。

失效資料還可能導緻一些令人困惑的故障,如:意料之外的異常、被破壞的資料結構、不精确的計算、無限循環等.

//非線程安全的可變整數類
@NotThreadSafe
public class MutableInteger {
    private int value;

    public int get() {
        return value;
    }

    public void set(int value) {
        this.value = value;
    }
}      
  • 此類是非線程安全的,因為get和set方法都是在沒有同步的情況下通路value的.

    失效值很容易出現:若某個線程調用了set,那麼另一個正在調用get的線程可能會看到更新後的value值,也可能看不到.

//線程安全的可變整數類
@ThreadSafe
public class SynchronizedInteger {
    @GuardedBy("this") private int value;

    public synchronized int get() {
        return value;
    }

    public synchronized void set(int value) {
        this.value = value;
    }
}      
  • 通過對set,get進行同步,可以使此類成為一個線程安全的類.僅對set同步時不夠的,調用get的線程仍可能看見失效值.

1.2 非原子的64位操作

對于非volatile類型的long和double變量,JVM允許将64位的讀操作或寫操作分解為兩個32位的操作。是以,當讀取該類變量的操作在不同的線程時,很可能會讀取到某個值的高32位和另一個值的低32位,造成讀取到是一個随機值。除非用關鍵字volatile來聲明它們,或者用鎖保護起來.

1.3 加鎖和可見性

當某線程執行由鎖保護的同步代碼塊時,可以看到其他線程之前在同一同步代碼塊中的所有操作結果。如果沒有同步,将無法實作上述保證。

對象的共享1 可見性2 釋出與逸出3 線程封閉4 不變性5 安全釋出

加鎖的含義不僅僅局限于互斥行為,還包括記憶體可見性.為了確定所有線程都能看到共享變量的最新值,所有執行讀操作或寫操作的線程都必須在同一個鎖上同步.

1.4 volatile變量

用于確定将變量的更新操作通知到其他線程,通路volatile變量時不會執行加鎖操作,也就不會使執行線程阻塞,是一種比sychronized更輕量級的同步機制.

編譯器與運作時都會注意到此變量是共享的,是以不會将該變量上的操作與其他記憶體操作一起重排序.

volatile變量不會被緩存在寄存器或其他處理器不可見的地方,是以在讀取volatile變量時總會傳回最新寫入的值.

從記憶體可見性來看:寫入volatile變量相當于退出同步代碼塊,讀取則相當于進入同步代碼塊(并不建議過度依賴此特性,通常比使用鎖的代碼還複雜)

僅當能簡化代碼的實作及對同步政策的驗證時,才該用.若在驗證正确性時需要複雜判斷可見性,就不要使用!正确使用方式包括:

  • 確定它們自身狀态的可見性
  • 確定它們所引用對象的狀态的可見性
  • 辨別一些重要的程式周期事件的發生(如初始化或關閉)
// 數綿羊
volatile boolean asleep;
...
while(!asleep){
   countSomeSheep();
}      
  • 一種典型用法:檢查某個狀态标記判斷是否退出循環.示例中,線程試圖通過數綿羊方法進入休眠狀态.為了使此示例能正确執行,asleep必須為volatile型.否則,當asleep被另一個線程修改時,執行判斷的線程卻發現不了.亦可使用加鎖保證,但代碼會很複雜.

雖然友善,但也存在局限性.常用做某個操作完成,發生中斷或狀态的标志,如上例的asleep标志..但語義不足以確定遞增操作的原子性,除非確定隻有一個線程對變量執行寫操作(後文的原子變量常做一種”更好的volatile變量”).

加鎖機制既可以確定可見性又可以確定原子性,而volatile變量隻能確定可見性

當且僅當滿足以下所有條件時,才該用volatile變量

  • 對變量的寫入操作不依賴變量的目前值,或能確定隻有單個線程更新變量的值
  • 該變量不會與其他狀态變量一起納入不變性條件中
  • 在通路變量時不需要加鎖

2 釋出與逸出

釋出:使對象能夠在目前作用域之外的代碼中使用.

釋出方式:

  • 将一個指向該對象的引用儲存到其他代碼可以通路的地方(最簡單的就是儲存到公有的靜态變量)
  • 非私有方法中傳回該引用
  • 将引用傳遞到其他類的方法中

當某個不應該釋出的對象被釋出時,就被稱為逸出.

//使内部的可變狀态逸出(不要這樣做!!!)
class UnsafeStates {
    private String[] states = new String[]{
            "AK", "AL" /*...*/
    };

    public String[] getStates() {
        return states;
    }
}      
  • 如此釋出states有問題,因為任何調用者都能修改這個數組的内容.states已經逸出了它所在的作用域,因為這個本應是private的變量已經被釋出了.
//this引用隐式地在構造函數中逸出
public class ThisEscape {
    public ThisEscape(EventSource source) {
        source.registerListener(new EventListener() {
            public void onEvent(Event e) {
                doSomething(e);
            }
        });
    }
}      
  • 當ThisEscape釋出EventListener時,也隐含釋出了ThisEscape執行個體本身,因為内部類的執行個體包含了對外部類執行個體的隐含引用.

構造過程中,另一個常見錯誤是,在構造器啟動一個線程.此時,無論是顯式建立(傳給構造器)或隐式(内部類),this引用都會被建立的線程共享.在對象尚未完全構造之前,新的線程就可以看見它.在構造器建立線程并無錯誤,但最好不要立即啟動,而是通過start或initialize方法啟動.在構造器調用一個可改寫的執行個體方法時,也會導緻this引用逸出.

想在構造器注冊一個監聽器或啟動線程,可使用一個私有的構造器和一個公共的工廠方法.如下示例:

public class SafeListener {
    private final EventListener listener;

    private SafeListener() {
        listener = e -> doSomething(e);
    }

    public static SafeListener newInstance(EventSource source) {
        SafeListener safe = new SafeListener();
        source.registerListener(safe.listener);
        return safe;
    }
}      

3 線程封閉

一種避免使用同步的方式就是不共享資料.

如果僅在單線程内通路資料,就不需要同步,這就被稱為線程封閉.線程封閉是程式設計中的考慮因素,必須在程式中實作.Java也提供了一些機制幫助維護線程封閉性,比如局部變量和ThreadLocal類.

3.1 Ad-hoc線程封閉

維護線程封閉性的職責完全由程式實作來承擔.

使用volatile變量是實作Ad-hoc線程封閉的一種方式,隻要能保證隻有單個線程對共享的volatile變量執行寫操作,就可以安全地在這些變量上進行“讀-改-寫”操作,volatile變量的可見性又保證了其他線程能夠看到最新的值。

Ad-hoc線程封閉是非常脆弱的,沒有語言特性可使對象直接封閉到目标線程.是以在程式中盡量少使用.

在可能的情況下,使用其他更強的線程封閉技術.

3.2 棧封閉

在棧封閉中,隻能通過局部變量才能通路對象.

局部變量的固有屬性之一就是封閉在執行線程中

它們位于執行線程的棧中,其他線程無法通路此棧.

即使使用了非線程安全的對象,該對象仍然是線程安全的.

3.3 ThreadLocal類

使用ThreadLocal是一種更規範的線程封閉方式,它能使線程中的某個值與儲存值的對象關聯起來。提供了get與set等通路接口方法,這些方法為每個使用該變量的線程都存有一份獨立的副本,是以get總是傳回由目前執行線程在調用set時設定的最新值.

常用于防止對可變的單執行個體變量或全局變量進行共享.

如下示例,通過将JDBC的連接配接儲存到ThreadLocal對象中,每個線程都會擁有屬于自己的連接配接:

//使用TheadLocal來維持線程封閉性
public class ConnectionDispenser {
    static String DB_URL = "jdbc:mysql://localhost/mydatabase";

    private ThreadLocal<Connection> connectionHolder
        = new ThreadLocal<Connection>() {
            public Connection initialValue() {
                try {
                    return DriverManager.getConnection(DB_URL);
                } catch (SQLException e) {
                    throw new RuntimeException("Unable to acquire Connection, e");
                }
        };
    };

    public Connection getConnection() {
        return connectionHolder.get();
    }
}      

當某個頻繁執行的操作需要一個臨時對象,如一個緩沖區,而同時又希望避免在每次執行時都重新配置設定該臨時對象,就可以使用該技術.

當某個線程初次調用get方法時,就會調用initialValue來擷取初始值.可将ThreadLocal < T >看作包含了Map< Thread,T>對象,儲存了特定于該線程的值,但ThreadLocal的實作并非如此.這些特定于線程的值存在Thread對象中,當線程終止後,這些值會作為垃圾被回收.

ThreadLocal 變量類似于全局變量,它能降低代碼的可重用性,并在類之間引入隐含的耦合性,使用時需要格外小心.

4 不變性

不可變對象:

滿足以下條件:

  • 對象建立以後其狀态就不能修改
  • 對象的所有域都是final類型(final類型域是不能被修改的)
  • 對象是正确建立的(在對象的建立期間,this引用沒有逸出)

在被建立後其狀态就不能被修改,且必線程安全.

在JMM中,final域能確定初始化過程的安全性,進而可以無限制地通路不可變對象,并在共享這些對象時無須同步.

5 安全釋出

任何線程都可在無額外同步情況下安全通路不可變對象,即使在釋出時沒有使用同步.

然而,若final域所指向為可變對象,通路這些可變對象的狀态時仍需同步.

安全釋出常用模式

可變對象必須通過安全方式釋出,常意味着釋出和使用該對象的線程都需同步.

為安全釋出,對象的引用以及對象的狀态必須同時對其他線程可見.

一個正确構造的對象可以通過以下方式來安全釋出

  • 在靜态初始化函數裡初始化一個對象引用
  • 将對象的引用儲存到volatile類型的域或者AtomicReference對象中
  • 将對象的引用儲存到某個正确構造對象的final類型域中
  • 将對象的引用儲存到一個由鎖保護的域中

線程安全庫中的容器類提供了以下的安全釋出保證:

  • 通過将一個鍵或者值放入Hashtable、synchronizedMap或者ConcurrentMap中,可以安全地将它釋出給任何從這些容器中通路它的線程
  • 通過将某個對象放入Vector、CopyOnWriteArrayList、CopyOnWriteArraySet、synchronizedList或者synchronizedSet中,可以将該對象安全地釋出到任何從這些容器中通路該對象的線程

通過将某個對象放入BlockingQueue或者ConcurrentLinkedQueue中,可以将該對象安全地釋出到任何從這些隊列中通路該對象的線程

通常釋出一個靜态構造的對象,最簡單安全的方式就是使用靜态的初始化器:

public static Holder holder = new Holder(42);      

由JVM在類的初始化階段執行,且由于JVM内部存在着同步機制,是以這樣初始化的任何對象都能被安全釋出.

事實不可變對象:對象從技術上來看是可變的,但其狀态在釋出後不會再改變.

在沒有額外的同步的情況下,任何線程都可以安全地使用被安全釋出的事實不可變對象.

對于可變對象,不僅在釋出對象時需要同步,而且在每次對象通路時同樣需要使用同步來確定後續修改操作的可見性.

對象的釋出需求取決于它的可變性:

  • 不可變對象可以通過任意機制來釋出。
  • 事實不可變對象必須通過安全方式來釋出。
  • 可變對象必須通過安全方式來釋出,而且必須是線程安全的或者用某個鎖保護起來。

安全的共享對象

實用政策:

  • 線程封閉 線程封閉的對象隻能由一個線程擁有,對象被封閉在該線程中,并且隻能由這個線程修改
  • 隻讀共享 在沒有額外同步的情況下,共享的隻讀對象可以由多個線程并發通路,但任何線程都不能修改它.共享的隻讀對象包括不可變對象和事實不可變對象
  • 線程安全共享 線程安全的對象在其内部實作同步,是以多個線程可以通過對象的公共接口來進行通路而不需要進一步的同步
  • 保護對象 被保護的對象隻能通過持有特定的鎖來通路.保護對象包括封裝在其他線程安全對象中的對象,以及已釋出的并且由某個特定鎖保護的對象