如果一個線程因為cpu時間全部被其他線程搶走而得不到cpu運作時間,這種狀态被稱之為“饑餓”。而該線程被“饑餓緻死”正是因為它得不到cpu運作時間的機會。解決饑餓的方案被稱之為“公平性” – 即所有線程均能公平地獲得運作機會。
下面是本文讨論的主題:
1. java中導緻饑餓的原因:
高優先級線程吞噬所有的低優先級線程的cpu時間。
線程被永久堵塞在一個等待進入同步塊的狀态。
線程在等待一個本身也處于永久等待完成的對象(比如調用這個對象的wait方法)。
2. 在java中實作公平性方案,需要:
使用鎖,而不是同步塊。
公平鎖。
注意性能方面。
java中導緻饑餓的原因
在java中,下面三個常見的原因會導緻線程饑餓:
線程被永久堵塞在一個等待進入同步塊的狀态,因為其他線程總是能在它之前持續地對該同步塊進行通路。
線程在等待一個本身(在其上調用wait())也處于永久等待完成的對象,因為其他線程總是被持續地獲得喚醒。
高優先級線程吞噬所有的低優先級線程的cpu時間
你能為每個線程設定獨自的線程優先級,優先級越高的線程獲得的cpu時間越多,線程優先級值設定在1到10之間,而這些優先級值所表示行為的準确解釋則依賴于你的應用運作平台。對大多數應用來說,你最好是不要改變其優先級值。
線程被永久堵塞在一個等待進入同步塊的狀态
java的同步代碼區也是一個導緻饑餓的因素。java的同步代碼區對哪個線程允許進入的次序沒有任何保障。這就意味着理論上存在一個試圖進入該同
步區的線程處于被永久堵塞的風險,因為其他線程總是能持續地先于它獲得通路,這即是“饑餓”問題,而一個線程被“饑餓緻死”正是因為它得不到cpu運作時
間的機會。
線程在等待一個本身(在其上調用wait())也處于永久等待完成的對象
如果多個線程處在wait()方法執行上,而對其調用notify()不會保證哪一個線程會獲得喚醒,任何線程都有可能處于繼續等待的狀态。是以存在這樣一個風險:一個等待線程從來得不到喚醒,因為其他等待線程總是能被獲得喚醒。
在java中實作公平性
雖java不可能實作100%的公平性,我們依然可以通過同步結構線上程間實作公平性的提高。
首先來學習一段簡單的同步态代碼:
如果有一個以上的線程調用dosynchronized()方法,在第一個獲得通路的線程未完成前,其他線程将一直處于阻塞狀态,而且在這種多線程被阻塞的場景下,接下來将是哪個線程獲得通路是沒有保障的。
使用鎖方式替代同步塊
為了提高等待線程的公平性,我們使用鎖方式來替代同步塊。
注意到dosynchronized()不再聲明為synchronized,而是用lock.lock()和lock.unlock()來替代。
下面是用lock類做的一個實作:
注意到上面對lock的實作,如果存在多線程并發通路lock(),這些線程将阻塞在對lock()方法的通路上。另外,如果鎖已經鎖上(校對注:
這裡指的是islocked等于true時),這些線程将阻塞在while(islocked)循環的wait()調用裡面。要記住的是,當線程正在等待
進入lock() 時,可以調用wait()釋放其鎖執行個體對應的同步鎖,使得其他多個線程可以進入lock()方法,并調用wait()方法。
這回看下dosynchronized(),你會注意到在lock()和unlock()之間的注釋:在這兩個調用之間的代碼将運作很長一段時間。
進一步設想,這段代碼将長時間運作,和進入lock()并調用wait()來比較的話。這意味着大部分時間用在等待進入鎖和進入臨界區的過程是用在
wait()的等待中,而不是被阻塞在試圖進入lock()方法中。
但我們能改變這種情況。目前的lock類版本調用自己的wait()方法,如果每個線程在不同的對象上調用wait(),那麼隻有一個線程會在該對象上調用wait(),lock類可以決定哪個對象能對其調用notify(),是以能做到有效的選擇喚醒哪個線程。
公平鎖
下面來講述将上面lock類轉變為公平鎖fairlock。你會注意到新的實作和之前的lock類中的同步和wait()/notify()稍有不同。
準确地說如何從之前的lock類做到公平鎖的設計是一個漸進設計的過程,每一步都是在解決上一步的問題而前進的:nested monitor
lockout, slipped conditions和missed
signals。這些本身的讨論雖已超出本文的範圍,但其中每一步的内容都将會專題進行讨論。重要的是,每一個調用lock()的線程都會進入一個隊列,
當解鎖後,隻有隊列裡的第一個線程被允許鎖住farlock執行個體,所有其它的線程都将處于等待狀态,直到他們處于隊列頭部。
首先注意到lock()方法不在聲明為synchronized,取而代之的是對必需同步的代碼,在synchronized中進行嵌套。
fairlock新建立了一個queueobject的執行個體,并對每個調用lock()的線程進行入隊列。調用unlock()的線程将從隊列頭部
擷取queueobject,并對其調用donotify(),以喚醒在該對象上等待的線程。通過這種方式,在同一時間僅有一個等待線程獲得喚醒,而不是
所有的等待線程。這也是實作fairlock公平性的核心所在。
請注意,在同一個同步塊中,鎖狀态依然被檢查和設定,以避免出現滑漏條件。
還需注意到,queueobject實際是一個semaphore。dowait()和donotify()方法在queueobject中儲存着
信号。這樣做以避免一個線程在調用queueobject.dowait()之前被另一個調用unlock()并随之調用
queueobject.donotify()的線程重入,進而導緻信号丢失。queueobject.dowait()調用放置在
synchronized(this)塊之外,以避免被monitor嵌套鎖死,是以另外的線程可以解鎖,隻要當沒有線程在lock方法的
synchronized(this)塊中執行即可。
最後,注意到queueobject.dowait()在try – catch塊中是怎樣調用的。在interruptedexception抛出的情況下,線程得以離開lock(),并需讓它從隊列中移除。
性能考慮
如果比較lock和fairlock類,你會注意到在fairlock類中lock()和unlock()還有更多需要深入的地方。這些額外的代碼
會導緻fairlock的同步機制實作比lock要稍微慢些。究竟存在多少影響,還依賴于應用在fairlock臨界區執行的時長。執行時長越
大,fairlock帶來的負擔影響就越小,當然這也和代碼執行的頻繁度相關。