最近寫關於並發的小應用,才發現真的該好好的正視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進入方法塊,死鎖就這樣被創造出來了。
以上的例子不直觀的話,再看一個實例順便看看wait()的缺陷:
Java代碼
import java.util.LinkedList;
import java.util.List;
/**
* User: yanxuxin
* Date: Dec 9, 2009
* Time: 5:58:39 PM
*/
public class DeadLockSample {
public static void main(String[] args) {
final WaitAndNotify wan = new WaitAndNotify();
Thread t1 = new Thread(new Runnable(){
public void run() {
wan.pop();
}
});
Thread t2 = new Thread(new Runnable(){
public void run() {
wan.push("a");
}
});
t1.start();
t2.start();
}
}
class WaitAndNotify {
final List<String> list = new LinkedList<String>();
public synchronized void push(String x) {
synchronized(list) {
list.add(x);
notify();
}
}
public synchronized Object pop() {
synchronized(list) {
if(list.size() <= 0) {
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return list.size();
}
}
}
上面的這個例子也會出現死鎖,為什麼呢?首先看WaitAndNotify這個類,在push和pop方法上有synchronized關鍵字,方法內部也有 synchronized,那麼當WaitAndNotify實例化時會有兩個對象的隱式鎖,一個是WaitAndNotify對象自身的,作用在方法上;另一個就是方法 內部同步用到的list的。主線程開啟兩個線程t1和t2,t1進入pop方法此時list為空,它先後獲得了wan和 list的隱式鎖,接著就被wait扔 進wait set等待去了。注意這個wait()方法是誰的?答案是wan的,所以它釋放了wan的隱式鎖,但是把list的死死的抓著不放。此時t2終於 得到了 wan的隱式鎖進入push方法,但是不幸的是list的隱式鎖它這輩子也得不到了。。。
就是由於wait的設計是針對對象管理線程的,而又沒有其他的可以類似棧的方式層層釋放鎖,導致死鎖的杯具了。關於synchronized和 wait(),notify(),notifyAll()的故事,我想我能瞎編的暫時這麼多了,不然滔滔江水就又杯具了,哈哈。下面簡單的講講 java.lang.Thread的特色小故事。(待續...)
PS:"多線程基礎總結六"算是本文的補遺了,呵呵。