若是一個線程由於其餘線程佔滿了而沒法獲取CPU運行時間,這種狀況咱們稱之爲「飢餓現象」.線程將一直飢餓下去,由於其餘線程總能替代它獲取CPU運行時間.解決這種狀況的措施咱們稱之爲「公平措施」.即讓全部線程都能得到一次執行的機會.html
在Java中,如下三種狀況可以產生飢餓現象:java
你能夠分別對每個線程設置優秀級.高優先級的線程可以獲取到更多的CPU運行時間.你能夠爲線程設置1~10的優先級,但這徹底依賴於應用運行在哪一個操做系統之上.對於大多數應用採用默認優秀級就好.post
Java的同步代碼塊是引發飢餓現象的另外一個緣由.Java同步代碼塊沒辦法保證在等待中的線程可以按照某種序列進入同步代碼塊.這意味着理論上會有一個線程無限期的等待進入同步代碼塊,由於其餘線程老是可以在它以前進入同步代碼塊.這種問題也稱之爲"飢餓現象",該線程將會一直飢餓下去,由於其餘線程總能替代它獲取CPU運行時間.學習
在多個線程同時調用同一個對象的wait()
方法時,notify()
方法沒法保證喚醒哪一個線程.這會致使某些線程一直在等待.這會產生一個線程一直在等待喚醒的風險,由於其餘線程總能在它以前被喚醒.this
雖然在Java中不可能實現百分百的公平措施,但咱們仍然能夠實現本身的同步器結構來增長線程間的公平性.spa
咱們先來學習一個簡單的同步器代碼塊:操作系統
public class Synchronizer{
public synchronized void doSynchronized(){
// 花費至關長的時間來執行工做
}
}
複製代碼
若是有多於一個的線程調用doSynchronized方法,那麼其餘線程都須要等待第一個進入同步代碼塊的線程退出方法.只要有多於一個線程在等待狀態,就不能保證哪一個線程先被容許進入同步代碼塊.線程
爲了增長等待線程的公平性,首先咱們須要使用鎖來代替同步代碼塊來實現同步機制.code
public class Synchronizer{
Lock lock = new Lock();
public void doSynchronized() throws InterruptedException{
this.lock.lock();
// 臨界區代碼, 花費至關長的時間來執行工做
this.lock.unLock();
}
}
複製代碼
咱們注意到doSynchronized再也不使用synchronized
來聲明.取而代之的是將臨界區代碼放置在lock.lock()和lock.unLock()方法調用之間.htm
一個簡單Lock類實現:
public class Lock{
public class Lock {
private boolean isLocked = false;
private Thread lockingThread;
public synchronized void lock() throws InterruptedException {
while (isLocked){
wait();
}
isLocked = true;
lockingThread = Thread.currentThread();
}
public synchronized void unLock(){
if(lockingThread != null && lockingThread == Thread.currentThread()){
throw new IllegalMonitorStateException("Calling thread is not locked this lock");
}
isLocked = false;
lockingThread = null;
notify();
}
}
複製代碼
若是你看了上文的Synchronizer和如今的Lock實現,你會發現若是有多於一個線程同時訪問lock()方法時,會發生阻塞.其次,若是當前的鎖爲鎖住狀態的話,線程會在lock方法中的while循環內部調用wait()方法從而進入等待狀態.記住,一個線程一旦調用wait()完畢,則會釋放當前對象鎖,讓其餘線程能夠進入lock()方法.最後的結果是多個線程進入lock()方法的while循環中調用wait()方法進入等待狀態.
若是你回頭看doSynchronized()方法,你會發現lock()和unlock()狀態切換中間的註釋,這將花費至關長的時間來執行兩個方法調用間的代碼.須要咱們確認的是執行這些代碼須要花費至關長的時間來比較進入lock()方法和在鎖被鎖住的狀況下調用wait()方法.這意味着大部分時間都用來等待鎖的鎖住和在lock()方法內部調用的wait()方法完畢後等待退出wait()方法.而不是等待進入lock()方法.
在以前的同步代碼塊的狀態下,若是有多個線程等待進入同步代碼塊,沒法保證哪一個線程先進入同步代碼塊.一樣的在調用wait()方法進入等待狀態後,沒法保證調用notify()後哪一個線程會先被喚醒.因此當前Lock的實現版本並不比以前的synchronized版本的doSynchronized()公平到哪去.但咱們能夠稍做更改.
當前Lock版本調用的是它本身的wait()方法.若是將每一個線程調用的wait()方法替換成不一樣對象的.即每一個線程對應調用一個對象的wait()方法,即Lock可以決定在日後的時間裏到底調用哪一個對象的notify()方法,這樣就能具體有效的選擇喚醒哪一個線程.
如下內容是將上文說起的Lock.class轉變爲一個公平鎖即FairLock.class.你會發現與以前版本對比,這個實現僅是調整了同步代碼塊和wait()/notify()的調用方式而已.
實際上在獲得當前這個版本的公平鎖以前遇到了許許多多的問題,而每解決這其中的一個問題都須要長篇概論來闡述,解決這些問題的每個步驟都會在日後的主題中說起.這包含Nested Monitor Lockout, 滑動條件和信號丟失問題. 如今重點要知道的是線程以隊列的方式來調用lock()方法中的wait()方法,且每次在公平鎖未鎖住時僅能讓隊列頭部的線程來獲取和鎖住公平鎖實例.其餘線程則處在等待狀態直到進入隊列頭部.
public class FairLock {
private boolean isLocked = false;
private Thread lockingThread;
private List<QueueObject> waitingThreads = new ArrayList<>();
public void lock() throws InterruptedException {
// 1. 爲每一個線程建立一個QueueObject
QueueObject queueObject = new QueueObject();
boolean isLockedForThisThread = true;
synchronized (this) {
// 2. 添加當前線程的QueueObject到隊列中
waitingThreads.add(queueObject);
}
while (isLockedForThisThread) {
synchronized (this) {
isLockedForThisThread = isLocked || waitingThreads.get(0) != queueObject;
if (!isLockedForThisThread) {
// 3. 鎖住當前公平鎖
isLocked = true;
waitingThreads.remove(queueObject);
lockingThread = Thread.currentThread();
return;
}
}
try {
// 4. 調用該線程對應QueueObject的wait()方法進入等待狀態
queueObject.doWait();
} catch (InterruptedException e) {
synchronized (this) {
waitingThreads.remove(queueObject);
}
throw e;
}
}
}
public synchronized void unlock() {
if (this.lockingThread != Thread.currentThread()) {
throw new IllegalMonitorStateException("Calling thread has not locked this lock");
}
// 1. 釋放公平鎖
isLocked = false;
lockingThread = null;
if (waitingThreads.size() > 0) {
// 2. 調用隊列頭部線程一一對應的QueueObject喚醒線程
waitingThreads.get(0).doNotify();
}
}
}
複製代碼
public class QueueObject {
private boolean isNotified = false;
public synchronized void doWait() throws InterruptedException {
while (!isNotified) {
this.wait();
}
this.isNotified = false;
}
public synchronized void doNotify() {
this.isNotified = true;
this.notify();
}
public boolean equals(Object o) {
return this == o;
}
}
複製代碼
首先你會注意到lock再也不聲明爲synchronized
.取而代之的是將須要作同步限制的代碼塊嵌套到synchronized
代碼塊中.
每一個線程調用lock()方法後都會建立與之對應的QueueObject
實例,並進入到隊列中.線程調用unlock()方法後會從隊列的頭部取得QueueObject
對象並調用它的doNotify()方法來喚醒與之對應的線程.對於全部等待的線程來講,這種方式每次僅會喚醒一個線程.這部分就是FairLock用來確保公平的代碼.
咱們注意到lock的鎖住狀態會在同一個代碼塊中不停的檢查和設置來解決滑動條件帶來的問題.
同時咱們注意到QueueObject
就是一個Semaphore
.doWait()和doNotify()調用所產生的狀態會存儲在QueueObject
內部.這用來解決信號丟失問題,即一個線程在調用queueObject().doWait()時,被另外一個線程搶先機會調用了unlock()中的queueObject.doNotify(). queueObject.doWait()調用被放置在synchronized(this)
同步代碼塊以外,用於解決Nested Monitor Lockout問題.這樣當沒有線程在lock()方法的synchronized(this)
代碼塊中執行時,其餘線程能夠正常調用unLock()方法.
最後須要注意的是,爲何須要將queueObject.doWait()調用放置在try-catch中.當線程經過拋出InterruptedException
來終止lock()方法調用時,咱們須要將線程與之對應的QueueObject
踢出隊列.
當你比較Lock.class和FairLock.class的lock()和unLock()實現時,你會發如今FairLock.class中多了許多代碼.這部分代碼會讓FairLock同步機制的運行相較於Lock會慢一些.至於影響多大取決於FairLock所限制的臨界區代碼的運行時長.運行時長越長,FairLock帶來的負面影響越小,固然這還取決於這部分代碼的運行頻率.
該系列博文爲筆者複習基礎所著譯文或理解後的產物,複習原文來自Jakob Jenkov所著Java Concurrency and Multithreading Tutorial