3 Java中的鎖與排隊上廁所。 鎖就是阻止其它進程或線程進行資源訪問的一種方式,即鎖住的資源不能被其它請求訪問。在JAVA中,sychronized關鍵字用來對一個對象加鎖。比如:
public class MyStack { int idx = 0; char [] data = new char[6];
public synchronized void push(char c) { data[idx] = c; idx++; }
public synchronized char pop() { idx--; return data[idx]; }
public static void main(String args[]){ MyStack m = new MyStack(); /** 下面對象m被加鎖。嚴格的說是對象m的所有synchronized塊被加鎖。 如果存在另一個試圖訪問m的線程T,那麼T無法執行m對象的push和 pop方法。 */ m.pop();//對象m被加鎖。 } } Java的加鎖解鎖跟多個人排隊等一個公用廁位完全一樣。第一個人進去後順手把門從裡面鎖住,其它人只好排隊等。第一個人結束後出來時,門才會開啟(解鎖)。輪到第二個人進去,同樣他又會把門從裡面鎖住,其它人繼續排隊等待。 用廁所理論可以很容易明白: 一個人進了一個廁位,這個廁位就會鎖住,但不會導致另一個廁位也被鎖住,因為一個人不能同時蹲在兩個廁位裡。對於Java 就是說:Java中的鎖是針對同一個對象的,不是針對class的。看下例:
MyStatck m1 = new MyStack(); MyStatck m2 = new Mystatck(); m1.pop(); m2.pop(); m1對象的鎖是不會影響m2的鎖的,因為它們不是同一個廁位。就是說,假設有 3線程t1,t2,t3操作m1,那麼這3個線程只可能在m1上排隊等,假設另2個線程 t8,t9在操作m2,那麼t8,t9隻會在m2上等待。而t2和t8則沒有關係,即使m2上的鎖釋放了,t1,t2,t3可能仍要在m1上排隊。原因無它,不是同一個廁位耳。 Java不能同時對一個代碼塊加兩個鎖,這和資料庫鎖機制不同,資料庫可以對一條記錄同時加好幾種不同的鎖,
4 何時釋放鎖。 一般是執行完畢同步代碼塊(鎖住的代碼塊)後就釋放鎖,也可以用wait()方式半路上釋放鎖。wait()方式就好比蹲廁所到一半,突然發現下水 道堵住了,不得已必須出來站在一邊,好讓修下水道師傅(準備執行notify的一個線程)進去疏通馬桶,疏通完畢,師傅大喊一聲: "已經修好了"(notify),剛才出來的同志聽到後就重新排隊。注意啊,必須等師傅出來啊,師傅不出來,誰也進不去。也就是說notify後,不是其 它線程馬上可以進入封鎖地區活動了,而是必須還要等notify代碼所在的封鎖地區執行完畢從而釋放鎖以後,其它線程才可進入。 這裡是wait與notify程式碼範例:
public synchronized char pop() { char c; while (buffer.size() == 0) { try { this.wait(); //從廁位裡出來 } catch (InterruptedException e) { // ignore it... } } c = ((Character)buffer.remove(buffer.size()-1)). charValue(); return c; }
public synchronized void push(char c) { this.notify(); //通知那些wait()的線程重新排隊。注意:僅僅是通知它們重新排隊。 Character charObj = new Character(c); buffer.addElement(charObj); }//執行完畢,釋放鎖。那些排隊的線程就可以進來了。 再深入一些。 由於wait()操作而半路出來的同志沒收到notify訊號前是不會再排隊的,他會在旁邊看著這些排隊的人(其中修水管師傅也在其中)。注意,修 水管的師傅不能插隊,也得跟那些上廁所的人一樣排隊,不是說一個人蹲了一半出來後,修水管師傅就可以突然冒出來然後立刻進去搶修了,他要和原來排隊的那幫 人公平競爭,因為他也是個普通線程。如果修水管師傅排在後面,則前面的人進去後,發現堵了,就wait,然後出來站到一邊,再進去一個,再wait,出 來,站到一邊,只到師傅進去執行notify. 這樣,一會兒功夫,排隊的旁邊就站了一堆人,等著notify. 終於,師傅進去,然後notify了,接下來呢。
1. 有一個wait的人(線程)被通知到。 2. 為什麼被通知到的是他而不是另外一個wait的人。取決於JVM.我們無法預先 判斷出哪一個會被通知到。也就是說,優先順序高的不一定被優先喚醒,等待 時間長的也不一定被優先喚醒,一切不可預知。(當然,如果你瞭解該JVM的 實現,則可以預知)。 3. 他(被通知到的線程)要重新排隊。 4. 他會排在隊伍的第一個位置嗎。回答是:不一定。他會排最後嗎。也不一定。 但如果該線程優先順序設的比較高,那麼他排在前面的機率就比較大。 5. 輪到他重新進入廁位時,他會從上次wait()的地方接著執行,不會重新執行。 噁心點說就是,他會接著拉巴巴,不會重新拉。 6. 如果師傅notifyAll(). 則那一堆半途而廢出來的人全部重新排隊。順序不可知。 Java DOC 上說,The awakened threads will not be able to proceed until the current thread relinquishes the lock on this object(當前線程釋放鎖前,喚醒的線程不能去執行)。 這用廁位理論解釋就是顯而易見的事。
5 Lock的使用 用synchronized關鍵字可以對資源加鎖。用Lock關鍵字也可以。它是JDK1.5中新增內容。用法如下:
class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition();
final Object[] items = new Object[100]; int putptr, takeptr, count;
public void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; notEmpty.signal(); } finally { lock.unlock(); } }
public Object take() throws InterruptedException { lock.lock(); try { while (count == 0) notEmpty.await(); Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notFull.signal(); return x; } finally { lock.unlock(); } } } (註:這是JavaDoc裡的例子,是一個阻塞隊列的實現例子。所謂阻塞隊列,就是一個隊列如果滿了或者空了,都會導致線程阻塞等待。Java裡的 ArrayBlockingQueue提供了現成的阻塞隊列,不需要自己專門再寫一個了。) 一個對象的lock.lock()和lock.unlock()之間的代碼將會被鎖住。這種方式比起synchronize好在什麼地方。簡而言 之,就是對wait的線程進行了分類。用廁位理論來描述,則是那些蹲了一半而從廁位裡出來等待的人原因可能不一樣,有的是因為馬桶堵了,有的是因為馬桶沒 水了。通知(notify)的時候,就可以喊:因為馬桶堵了而等待的過來重新排隊(比如馬桶堵塞問題被解決了),或者喊,因為馬桶沒水而等待的過來重新排 隊(比如馬桶沒水問題被解決了)。這樣可以控製得更精細一些。不像synchronize裡的wait和notify,不管是馬桶堵塞還是馬桶沒水都只能 喊:剛才等待的過來排隊。假如排隊的人進來一看,發現原來只是馬桶堵塞問題解決了,而自己渴望解決的問題(馬桶沒水)還沒解決,只好再回去等待 (wait),白進來轉一圈,浪費時間與資源。 Lock方式與synchronized對應關係:
| Lock |
await |
signal |
signalAll |
| synchronized |
wait |
notify |
notifyAll |
注意:不要在Lock方式鎖住的塊裡調用wait、notify、notifyAll