《Java多線程設計模式》讀書筆記2
目錄:
1 Java的記憶體模型
2 Single Threaded Execution Pattern
3 Guarded Suspension Pattern
4 Balking Pattern
5 Producer-Consumer Pattern
6 Read-Write Lock Pattern
=============Java的記憶體模型======================
Java的記憶體模型分為主儲存空間與工作儲存空間兩種。
主儲存空間(與硬體上的主存無關,僅為抽象概念)就是執行個體位置所在的地區,所有的執行個體都存在於主儲存空間內。尤其是執行個體所擁有的欄位即位於主儲存空間內的地區。主儲存空間為所有線程所共有。工作儲存空間為各個線程所擁有的作業區,所有的線程都有其專用的工作儲存空間。在工作儲存空間中存有主儲存空間中必要的拷貝,稱之為工作拷貝。
線程無法直接對主儲存空間直接進行操作,因此也無法直接引用欄位的值,當線程需要引用欄位的值時,需要把它從主儲存空間上拷貝到自己的工作儲存空間中。至於要修改欄位的值時,也要先修改工作拷貝,然後把修改從工作儲存空間拷貝回主儲存空間中。至於何時完成資料在工作儲存空間和主儲存空間之間的映像,則有Java執行處理系統決定。
對於多個線程共用的欄位,一般由synchronized或者volatile來保護。
synchronized用來完成線程的共用互斥。而volatile不負責線程的共用互斥,僅負責記憶體的同步。
synchronized一方面製作臨界區,使得該地區只能同時允許一個線程進行操作;另一方面,當進入和退出臨界區時,執行記憶體的同步操作,即把當前工作區的修改映像到主儲存空間上。在進入synchronized時,除了工作拷貝到主儲存空間的映像之外,還會釋放工作儲存空間的工作拷貝,如果之後再引用主儲存空間上值,則必須從主儲存空間拷貝到工作儲存空間。
雖然在進入和離開synchronized時,會發生記憶體的同步,但是如果是“在synchronized內”或者“在synchronized外”時,記憶體何時同步是不確定的,取決於Java運行處理系統。
在單線程中,可以不考慮記憶體的同步問題,當然線程的同步也不需考慮。
volatile記憶體同步:當線程欲引用volatile欄位的值時,通常都會發生從主儲存空間到工作儲存空間的拷貝操作,反之,將值指定給寫著volatile的欄位後,工作儲存空間的內容通常會映像到主儲存空間。另外,對於long和double類型,預設的賦值操作不是atomic的,如要以atomic方式指定這些類型的值,則需使用volatile關鍵字,此時,volatile實現原子操作,而非記憶體同步。問:如果要指定long,double類型欄位的記憶體同步,怎麼辦?難道只能用synchronized來實現?
=============Single Threaded Execution Pattern=============
當我們修改由多個線程共用的變數,變數就會失去安全性。所以我們應該仔細找出變數狀態不安定的範圍,將這個範圍設定為臨界區,並對臨界區施加保護,限制同時只能有一個線程執行它。在java中,可以使用synchronized關鍵字定義臨界區,保護共用欄位。
適用性:資料可被多個線程訪問,並且狀態可能變化的時候。
===================Immutable Pattern===============
當一個類的執行個體聲明之後,狀態就完全不再改變,此時就算用多個線程同時調用這個類的方法也無所謂,所以方法沒有synchronized的必要,這樣一來,可以在不喪失安全性與生命性的前提下,提高程式的效能。而要實作類別的immutable,則在類的實現中必須小心防備,不要提供能夠改變執行個體狀態的方法或者破綻。
Java final的用法
final類:final類無法被繼承
final方法:如果是執行個體方法,則無法被子類所覆蓋,若是類方法,則無法被子類隱藏。
final欄位:其值只能指定一次,要麼在聲明時初始化,要麼在建構函式中賦值(靜態欄位則是在static塊中初始化)。
final變數與參數:final變數只能在定義時指定一次,final參數則是僅在調用時傳值指定。
===================Guarded Suspension Pattern===============
當滿足某條件時,直接執行某動作;如果不滿足條件,等待,直到條件滿足時被喚醒。
比如請求隊列,如果隊列已經為空白,則此時要執行擷取請求操作則必須等待;當添加新請求時喚醒等待的線程。
public synchronized Request getRequest(){
while(queue.size() <= 0){
try{
wait();
}catch(InterruptedException e){}
return (Request)queue.removeFirst();
}
public synchronized void putRequest(Request req){
queue.addLast(req);
notifyAll();
}
===================Balking Pattern===============
當現在不適合進行某個操作,或者沒有必要進行某個操作時,就直接放棄進行這個操作而返回。
public synchronized void save(){
if(!changed){
return;
}
dosave();
changed = false;
}
使用情境:(1)不需要刻意去執行的時候(提高執行效能);(2)不想等待警戒條件時(提高響應性);(3)警戒條件只有第一次成立時(比如初始化)。
===================Producer-Consumer Pattern===============
Producer-Consumer模式假設所生產的產品放置在一個長度有限制的緩衝區,如果緩衝區滿了,則生產者必須停止繼續將產品放到緩衝區中,直到消費者取走了產品而有了空間,而如果緩衝區中沒有產品,當然消費者必須等待,直到有新的產品放到緩衝區中。與Guarded Suspension 模式類似的,只不過Guarded Suspension模式並不限制緩衝區的長度。
可能有多個生產者和多個消費者,當只有一個生產者和一個消費者的時候,又叫管道Pipe Pattern。
Java主要通過synchronized,wait與notify/notifyAll方法來實現producer與consumer的協調。
===================Read-Write Lock Pattern===============
Read-Write Lock Pattern將讀取與寫入分開處理。在讀取資料之前,必須擷取用來讀取的鎖定。而要寫入的時候,則必須擷取用來寫入的鎖定。
因為讀取的時候,執行個體的狀態不會改變,所以就算有多個線程在同時讀取也沒有關係。但有人在讀取的時候就不可做寫入的操作。
寫入的時候,執行個體的狀態就會改變,當有一個線程在寫入的時候,其他的線程不可以進行讀取或寫入。一般來說,進行共用互斥會使程式效能變差,但將寫入的共用互斥與讀取的共用互斥分開思考,就可以提升程式的效能。
Java語言中,使用finally可以避免忘記解除鎖定。比如:
擷取鎖定
try{
實際的操作
}finally{
解除鎖定
}