最近寫關於並發的小應用,才發現真的該好好的正視java的多線程了。之前沒有深入的掌握,用起來也是那麼的吃力。作為J2SE裡面為 數不多的重要痛點之一,多線程應用一直是我以敬畏的心態去盡量避開的,只是通過一些執行個體掌握一些簡單的應用。這段時間會多用點時間 去掌握,有需要寫下來的我也會通過這種方式既分享又加深理解。
首先這篇只涉及基礎的知識整理,對於並發包java.util.concurrent內的線程池和鎖我會看情況在之後的總結中寫點東西。對於進程的 概念我們都很熟悉,它是應用程式級的隔離,不同的應用程式之間的進程幾乎不共用任何資源。而線程則可以說是應用程式內的隔離,一種 相對低層級的隔離。一個進程可以有多個線程,它們之間隔離的內容大致包括:a.自身的堆棧,b.程式計數器,c.局部變數;共用應用的內 容大致包括:a.記憶體,b.檔案控制代碼,c.進程狀態等。線程不是Java自身的概念,它是作業系統底層的概念。Java作為一種應用語言把線程的 操作通過API提升到應用開發的支援,但是在並發性的支援上並不是那麼美好。
Java在設計時,每個對象都有一個隱式的鎖,這個鎖的使用則是通過synchronized關鍵字來顯式的使用。在JDK5.0以後引用了 java.util.concurrent.ReentrantLock作為synchronized之外的選擇,配和Condition可以以一種條件鎖的機制來管理並發的線程,之後的 總結再介紹。提到synchronized,多數的初學者都知道Object的 wait(),notify(),notifyAll()是配和其使用的,但是為什麼要在同步內 才能用對象的這些方法呢(不然拋 IllegalMonitorStateException)?
我想因為沒有synchronized讓對象的隱式鎖發揮作用,那麼方法或者方法塊內的線程在同一時間可能存在多個,假設wait()可用,它會 把這些線程統統的加到wait set中等待被喚醒,這樣永遠沒有多餘的線程去喚醒它們。每個對象管理調用其wait(),notify()的線程,使得 別的對象即使想幫忙也幫不上忙。這樣的結果就是多線程永遠完成不了多任務,基於此Java在設計時使其必須與synchronized一起使用,這 樣獲得隱式鎖的線程同一時間只有一個,當此線程被對象的wait()扔到wait set中時,線程會釋放這個對象的隱式鎖等待被喚醒的機會,這 樣的設計會大大降低死結。另外同一個對象隱式鎖作用下的多個方法或者方法塊在沒有鎖的限制下可以同時允許多個線程在不同的方法內 wait和notify,嚴重的競爭條件使得死結輕而易舉。所以Java設計者試圖通過Monitor Object模式解決這些問題,每個對象都是Monitor用 於監視擁有其使用權的線程。
但是synchronized這種獲得隱式鎖的方式本身也是有隱患問題的:a.不能中斷正在試圖獲得鎖的線程,b.試圖獲得鎖時不能設定逾時, c.每個鎖只有一個條件太少。對於最後一項的設計前面提到的JDK5的方案是可以彌補的,一個ReentrantLock可以有多個Condition,每個條 件管理獲得對象鎖滿足條件的線程,通過await(),signalAll()使只關於Condition自己放倒的線程繼續運行,或者放倒一些線程,而不是全 部喚醒等等。但對於前兩者的極端情況會出現死結。下面的這個例子:
Java代碼
class DeadLockSample{
public final Object lock1 = new Object();
public final Object lock2 = new Object();
public void methodOne(){
synchronized(lock1){
...
synchronized(lock2){...}
}
}
public void methodTwo(){
synchronized(lock2){
...
synchronized(lock1){...}
}
}
}
假設情境:線程A調用methodOne(),獲得lock1的隱式鎖後,在獲得lock2的隱式鎖之前線程B進入運行,調用 methodTwo(),搶先獲得了 lock2的隱式鎖,此時線程A等著線程B交出lock2,線程B等著lock1進入方法塊,死結就這樣被創造出來了。