Java程式設計思想讀書筆記一:併發

衣舞晨風發表於2017-01-31

1. Thread.yield( )方法

當呼叫yield()時,即在建議具有相同優先順序的其他執行緒可以執行了,但是注意的是,僅僅是建議,沒有任何機制保證你這個建議會被採納 。一般情況下,對於任何重要的控制或者呼叫應用時,都不能依賴於yield()。這個方法經常被誤用。

2.Runnable介面

當從Runnable匯出一個類時,它必須具有run()方法,但是這個方法並無特殊之處——它並不會產生任何內在的執行緒能力。要實現執行緒的行為必須顯式的講一個任務附著到執行緒上。

3.Join()方法

一個執行緒可以在其他執行緒之上呼叫Join()方法,其效果是等待一段時間直到第二個執行緒結束才繼續執行。如果某個執行緒在另一個執行緒t上呼叫t.join(),此執行緒將被掛起,直到目標執行緒t結束才恢復(即t.isAlive()返回為假)。也可以在呼叫join()時帶上一個超時引數(單位可以是毫秒,或者毫秒和納秒),這樣如果目標執行緒在這段時間到期時還沒有結束的話,join()方法總能返回。對join()方法的呼叫可以被中斷,做法實是在呼叫執行緒上呼叫interrupt()方法,這時需要呼叫try-catch子句。

4.互斥量(mutex)

基本上所有的併發模式在解決執行緒安全問題時,都採用“序列化訪問臨界資源”的方案,即在同一時刻,只能有一個執行緒訪問臨界資源,也稱作同步互斥訪問。通常這是在程式碼前加上一條鎖語句,使得在一段時間內只有一個執行緒可以執行這段程式碼。因為鎖語句產生了一種能夠互相排斥的效果,所以這種機制被稱為互斥量(mutex)。

5.共享資源競爭

一個任務可以多次獲得物件的鎖。如果一個方法在同一個物件上呼叫了第二個方法,後者又呼叫了同一個物件上的另一個方法,就會發生這種情況。JVM負責跟蹤物件被加鎖的次數,如果一個物件被解鎖,計數變為0。在任務第一次給物件加鎖的時候,計數變為1。每當這個相同的任務在這個物件上獲得鎖,計數都會遞增。顯然,只有首先獲得了鎖的任務才能允許繼續獲取多個鎖。每當任務離開一個synchronized 方法,計數遞減,當計數為0的時候,鎖被完全釋放,其他任務可以使用此資源。

考慮一下屋子裡的浴室:多個人(即多個執行緒)都希望能單獨使用浴室(即共享資源)。為了使用浴室,一個人先敲門,看看是否能使用。如果沒人的話,他就進入浴室並且鎖上門。這時其它人要使用浴室的話,就會被“阻擋”,所以他們要在浴室門口等待,直到浴室可以使用。

當浴室使用完畢,就該把浴室給其他人使用了,這個比喻就有點不太準確了。事實上,人們並沒有排隊,我們也不能確定誰將是下一個使用浴室的人,因為執行緒排程機制並不是確定性的。實際情況是:等待使用浴室的人們簇擁在浴室門口,當鎖住浴室門的那個人開啟鎖準備離開的時候,離門最近的那個人可能進入浴室。如前所述,可以通過yield( )和setPriority( )來給執行緒排程機制一些建議,但這些建議未必會有多大效果,這取決於你的具體平臺和JVM實現。

Java以提供關鍵字synchronized的形式,為防止資源衝突提供了內建支援。它的行為很像Semaphore類:當執行緒要執行被synchronized關鍵字守護的程式碼片斷的時候,它將檢查訊號量是否存在,然後獲取訊號量,執行程式碼,釋放訊號量。不同的是,synchronized內建於語言,所以這種防護始終存在,不像Semaphore那樣要明確使用才能工作。

典型的共享資源是以物件形式存在的記憶體片斷,但也可以是檔案,輸入/輸出埠,或者是印表機。要控制對共享資源的訪問,你得先把它包裝進一個物件。然後把所有要訪問這個資源的方法標記為synchronized。也就是說,一旦某個執行緒處於一個標記為synchronized的方法中,那麼在這個執行緒從該方法返回之前,其它要呼叫類中任何標記為synchronized方法的執行緒都會被阻塞。

一般來說類的資料成員都被宣告為私有的,只能通過方法來訪問這些資料。所以你可以把方法標記為synchronized來防止資源衝突

6.原子性與可見性(volatile)

當你定義long或double變數時,如果使用 volatile關鍵字,就會獲到(簡單的賦值與返回操作的)原子性(注意,在Java SE5之前,volatile一直不能正確的工作)。

volatile關鍵字還確保了應用中的可視性。如果講一個域宣告為volatile的,那麼只要對這個域產生了寫操作,那麼所有的讀操作就都可以看到這個修改。即便使用了本地快取,情況也確實如此,volatile域會立即被寫入到主存中,而讀取操作就發生在主存中。

在非volatile域上的原子操作不必重新整理到主存中去,因此其他讀取該域的任務也不必看到這個新值。如果多個任務在同時訪問這個域,那麼該域也應該是volatile,否則,這個域就應該只能經由同步來訪問。同步也會導致向主存中重新整理,因此如果一個域完全有synchronized方法或語句塊來保護。那就不必將其設定為是volatile。

一個任務所做的任何寫入操作對這個任務來說都是可視的,因此如果它只需要在這個任務內部可視,那麼你就不需要將其設定為volatile的。
當一個域的值依賴於他之前的值時(例如遞增一個計數器),volatile就無法工作了。如果這個域的值受到其他的域的值的限制,那麼volatile也無法工作,例如Range類的lower和upper邊界就必須遵循lower<=upper的限制

基本上,如果一個域可能會被多個任務同時訪問,或者這些任務中至少有一個是寫入任務,那麼就應該將這個域設定為volatile的。如果將一個域定義為volatile,那麼它就會告訴編譯器不執行任何移除讀取和寫入操作的優化。這些操作的目的是用執行緒中的區域性變數維護對這個域的精確同步。實際上,讀取和寫入都是直接針對記憶體的,而卻沒有被快取。但是,volatile並不能對遞增不是原子性操作這一事實產生影響。

使用volatile而不是synchronized的唯一安全的情況是類中只有一個可變的域。再次提醒,你的第一選擇應該使用synchronized關鍵字,這是最安全的方式,而其他所有方式都是有風險的

7.原子類

Java SE5引入了諸如AtomicInteger、AtomicLong、AtomicReference等特殊的原子性變數類,他們提供下面形式的原子性條件更新操作:

boolean compareAndSet(expectedValue, updateValue);

這些類調整為可以使用在某些現在處理器上的可獲得的,並且是在機器級別的原子性,因此在使用它們時,通常不需要擔心。對於常規程式設計來說,他們很少會派上用場,但是在涉及效能調優時,他們就大有用武之地了。
應該強調的是,Atomic類被設計用來構建java.util.concurrent中的類,因此只有在特殊情況下才在自己的程式碼中使用它們,即便使用了也需要確保不存在其他可能出現的問題。通常依賴於鎖要更安全一些。

8.臨界區

有時,我們只是希望防止多個執行緒同時訪問方法內部的部分程式碼而不是防止訪問整個方法,通過這個方式分離出來的程式碼被稱為臨界區(critical section),他也使用synchronized關鍵字建立,這裡,synchronized被用來指定某個物件,此物件的鎖被用來對花括號內的程式碼進行同步控制:

synchronized(syncObject) {
    // This code can be accessed
    // by only one task at a time
}

這也被成為同步控制塊;在進入此段程式碼前,必須得到syncObject物件的鎖。如果其他執行緒已經得到這個鎖,那麼就得等到鎖被釋放後,才能進入臨界區。通過使用同步控制塊,而不是對整個方法進行同步控制,可以使多個任務訪問物件的時間效能得到顯著提高。

9.執行緒狀態

下圖來自:http://lavasoft.blog.51cto.com/62575/99153/
這裡寫圖片描述

1、新狀態(new):執行緒物件已經建立,還沒有在其上呼叫start()方法。

2、可執行狀態(Runnable):當執行緒有資格執行,但排程程式還沒有把它選定為執行執行緒時執行緒所處的狀態。當start()方法呼叫時,執行緒首先進入可執行狀態。線上程執行之後或者從阻塞、等待或睡眠狀態回來後,也返回到可執行狀態。

3、執行狀態(RUNNING):執行緒排程程式從可執行池中選擇一個執行緒作為當前執行緒時執行緒所處的狀態。這也是執行緒進入執行狀態的唯一一種方式。

4、等待/阻塞/睡眠狀態(Blocked):這是執行緒有資格執行時它所處的狀態。實際上這個三狀態組合為一種,其共同點是:執行緒仍舊是活的,但是當前沒有條件執行。換句話說,它是可執行的,但是如果某件事件出現,他可能返回到可執行狀態。

5、死亡態(Dead):當執行緒的run()方法完成時就認為它死去。這個執行緒物件也許是活的,但是,它已經不是一個單獨執行的執行緒。執行緒一旦死亡,就不能復生。 如果在一個死去的執行緒上呼叫start()方法,會丟擲java.lang.IllegalThreadStateException異常。

10.進入阻塞狀態

一個任務進入阻塞狀態,可能有如下的原因:

1.通過呼叫sleep(milliseconds)使任務進入休眠狀態,在這種情況下,任務在指定的時間內不會執行。

2.你通過wait使執行緒掛起。直到執行緒得到了notify()或notifyAll()訊息,執行緒才會進入就緒狀態

3.任務在等待某個輸入/輸出完成。

4.任務試圖在某個物件上呼叫其同步控制方法,但物件鎖不可用,因為另一個任務已經獲取了這個鎖。

在較早的程式碼中,也可能會看到用suspend()和resume()方法來阻塞和喚醒執行緒,但是在Java新版本中這些方法被廢棄了,因為它們可能導致死鎖。stop()方法也已經被廢棄了,因為它不釋放執行緒獲得的鎖,並且如果執行緒處於不一致的狀態,其他任務可以在這種狀態下瀏覽並修改它們。

現在我們需要檢視的問題是:有時你希望能夠終止處於阻塞狀態的任務。如果對於阻塞狀態的任務,你不能等待其到達程式碼中可以檢查其狀態值的某一點,因而決定讓它主動終止,那麼你就必須強制這個任務跳出阻塞狀態。

11.中斷

Thread類包含了interrupt()方法,因此你可以終止被阻塞的任務,這個方法將設定執行緒的中斷狀態。如果一個執行緒已經被阻塞,或者試圖執行一個阻塞操作,那麼設定這個執行緒的中斷狀態將丟擲InterruptedException。當丟擲該異常或者該任務呼叫Thread.interrupted()時,中斷狀態將被複位。正如你將看到的,Thread.interrupted()提供了離開run()迴圈而不丟擲異常的第二種方式。

為了呼叫interrupt(),你必須持有Thread物件。你可能已經注意到了,新的concurrent類庫似乎在避免對Thread物件上的直接操作,轉而儘量的通過Executor來執行所有操作。如果你在Executor上呼叫shutdownNow(),那麼它將傳送一個interrupt()呼叫給它啟動的所有執行緒。這麼做是有意義的,因為當你完成工程中的某個部分或者整個程式時,通常會希望同時關閉某個特定Executor的所有任務。然而,你有時也會希望只中斷某個單一任務。如果使用Executor,那麼通過呼叫submit()方法而不是execute()方法來啟動任務,就可以持有該任務的上下文。submit()將返回一個泛型Future<?>,其中有一個未修飾的引數,因為你永遠都不會在其上呼叫get()——持有這種Future的關鍵在於你可以在其上呼叫cancel(),並因此可以使用它來中斷某個特定任務。如果你將true傳遞給cancel(),那麼它就會擁有在該執行緒上呼叫interrupt()以停止這個執行緒的能力。因此,cancel是一種中斷由Executor啟動的單個執行緒的方式。

12.wait()與notifyAll()

wait()使你可以等待某個條件發生變化,而改變這個條件超出了當前方法的控制能力。通常,這種條件將由另一個任務來改變。你肯定不想在你的任務測試這個條件的同時,不斷地進行空迴圈,這杯稱為忙等待,通常是一種不良的CPU週期使用方式。因此wait()會在等待外部世界產生變化的時候將任務掛起,並且只有在notify()或notifyAll()發生時,即表示發生了某些感興趣的事物,這個任務才被喚醒並去檢查所發生的變化。因此wait()提供了一種在任務之間對活動同步的方式。

呼叫sleep()的時候鎖並沒有被釋放,呼叫yield()也屬於這種情況,理解這一點很重要。另一方面,當一個任務在方法裡遇到了對wait()的呼叫的時候,執行緒的執行被掛起,物件上的鎖被釋放。因為wait()將釋放鎖,這就意味著另一個任務可以獲得這個鎖,因此在該物件(現在是未鎖定的)中的其他synchronized方法可以在wait()期間被呼叫。因為這些其他的方法通常將會產生改變,而這種改變正是使被掛起的任務重新喚醒所感興趣的變化。因此,當你呼叫wait()時,就是在宣告:“我已經剛剛做完所有能做的事情,因此我要在這裡等待,但是我希望其他的synchronized操作在條件適合的情況下能夠執行。”

有兩種形式的wait(),分別是sleep(long millis)和sleep(),
第一種方式接受好描述作為引數,含義與sleep()方法裡引數的意思相同,都是指“在此期間暫停”。但與sleep()不同的是,對於wait()而言:
在wait()期間,物件鎖是釋放的可以通過notify()、notifyAll(),或者令時間到期,從wait()中恢復執行
第二種,也是更常用形式,它不接受任何引數,這種wait()將無線等待下去,直到執行緒接受到notify()或者notifyAll()訊息。
wait()、notify()、notifyAll()有一個比較特殊的方面,那就是這些方法的基類是Object的一部分,而不是Thread類的一部分。儘管開始看起來有點奇怪——僅僅針對執行緒的功能卻作為通用基類的一部分而實現,不過這是有道理的,因為這些方法操作的鎖也是所有物件的一部分。

這個涉及到wait什麼的問題?
等的是某個物件上的鎖,大家(多個執行緒)競爭這個鎖,是吧,
wait和notify方法都放到目標物件上,那這個物件上可以維護執行緒的佇列,可以對相關執行緒進行排程。(方法和方法所操縱的資料要在一起)

實際上,只能在同步控制方法或者同步控制塊裡呼叫wait()、notify()、和notifyAll()(因為不用操作鎖,所以sleep()可以在非同步控制方法裡呼叫)。

如果在非同步控制方法裡呼叫這些方法,程式能通過編譯,但在執行的時候,將得到IllegalMonitorStateException異常,並伴隨著一些模糊的訊息,比如:當前執行緒不是鎖的擁有者。訊息的意思是,呼叫wait()、notify()和notifyAll()的任務在呼叫這些方法之前必須“擁有”(獲取)物件的鎖。

可以讓另一個物件執行某種操作以維護其自己的鎖。要這麼做的話,必須首先得到物件的鎖。比如,如果要向物件x傳送notifyAll(),那麼就必須在能夠得到x的鎖的同步塊中這麼做:

synchronized(x) {
    x.notifyAll();
}

使用notify()而不是notifyAll()是一種優化,使用notify()時,在眾多等待的執行緒中,只有一個會被喚醒,因此,如果你希望使用notify(),就必須保證被喚醒的是恰當的任務。另外,使用notify()所有任務必須等待相同的條件,因為如果你有多個任務在等待不同的條件,那麼你就不會知道是否喚醒了恰當的任務。如果使用notify(),當條件發生時,必須只有一個任務從中受益。最後,這些限制對所有可能存在的子類都必須總是起作用。如果這些規則中有任何一條不滿足,那麼你就必須使用notifyAll()而不是notify()。

notifyAll()將喚醒“所有正在等待的任務”並不意味著在程式中的任何地方、任何處於等待狀態的任務都將被喚醒,而僅僅只是等待這個鎖的任務才會被喚醒。

13.免鎖容器

這些免鎖容器背後的通用策略是:對容器的修改可以與讀取操作同時發生,只要讀取者只能看到完成修改的結果即可。修改時在容器資料結構的某個部分的一個單獨副本(有時是完整的資料結構的副本)上執行的,並且這個副本在修改過程是不可視的。只有當修改完成後,被修改的結構才會自動的與主資料結構進行交換,之後讀取者就可以看到這個修改了。

在CopyOnWriteArrayList中,寫入操作將導致建立整個底層陣列的副本,而原陣列將保留在原地,使得複製的陣列在被修改時,讀取操作可以安全的執行。當修改完成後,一個原子性的操作將把新的陣列換入,使得新的讀取操作可以看到這個新的修改。CopyOnWriteArrayList的好處之一是當多個迭代器同時遍歷和修改這個列表時,不會丟擲ConcurrentModificationException,因此你不必編寫特殊的程式碼區防範這種異常。

ConcurrenthashMap和ConcurrentLinkedQueue使用了類似的技術,允許併發的讀取和寫入,但是容器中只有部分內容而不是整個容器可以被複制和修改。然而,任何修改在完成之前,讀取者仍然不能看到他們。ConcurrentHashMap不會丟擲ConcurrentModificationException異常。

14.讀寫鎖

讀寫鎖可以用於 “多讀少寫” 的場景,讀寫鎖支援多個讀操作併發執行,寫操作只能由一個執行緒來操作。

ReadWriteLock對向資料結構相對不頻繁地寫入,但是有多個任務要經常讀取這個資料結構的這類情況進行了優化。ReadWriteLock使得你可以同事有多個讀取者,只要它們都不試圖寫入即可。如果寫鎖已經被其他任務持有,那麼任何讀取者都不能訪問,直至這個寫鎖被釋放為止。

ReadWriteLock 對程式心效能的提高受制於如下幾個因素也還有其他等等的因素。
1)資料被讀取的頻率與被修改的頻率相比較的結果。
2)讀取和寫入的時間
3)有多少執行緒競爭
4)是否在多處理機器上執行

15.內部類

一般來說,內部類繼承自某個類或實現某個介面,內部類的程式碼操作建立它的外圍類的物件。所以可以認為內部類提供了某種進入其外圍類的視窗。

但是,如果只是需要一個對介面的引用,為什麼不通過外圍類實現那個介面呢?原因在於:後者不是總能享用到介面帶來的方便,有時需要用到介面的實現。所以,使用內部類最吸引人的原因是:

每個內部類都能獨立地繼承自一個(介面的)實現,所以無論外圍類是否已經繼承了某個(介面的)實現,對於內部類都沒有影響。

如果沒有內部類提供的,可以繼承多個具體的或抽象的類的能力,一些設計與程式設計問題就很難解決。從這個角度看,內部類時得多重繼承的解決方案變的完整。介面解決了部分問題,而內部類有效地實現了“多重繼承”。也就是說,內部類允許繼承多個非介面型別(類或者抽象類)。

Java程式設計思想(第4版)PDF版下載:
http://download.csdn.net/detail/xunzaosiyecao/9745699

作者:jiankunking 出處:http://blog.csdn.net/jiankunking

相關文章