原文:http://blog.sina.com.cn/s/blog_4b6047bc010009dc.html
線程除要對共享數據保證互斥性訪問外,往往還需保證線程的操作按照特定順序進行。解決多線程按照特定順序訪問共享數據的技術稱作同步。同步技術最常見的編程範式是同步保護塊。這種編程範式在操作前先檢測某種條件是否成立,如成立則繼續操作;如不成立則有兩種選擇,一種是簡單的循環檢測,直至此條件條件成立:
public void guardedOperation(){
while(!condition_expression){
System.out.println("Not ready yet, I have to wait again!");
}
}
這種方法非常消耗CPU資源,任何情況下都不應該使用這種方法。另種更好的方式是條件不成立時調用Object.wait方法掛起當前線程,使它一直等待,直至另一個線程發出激活事件。當然該事件不一定是當前線程希望等待的事件。
public synchronized guardedOperation() {
while(!condition_expression) {
try {
wait();
} catch (InterruptedException e) {}
}
System.out.println("Now, condition met and it is ready!");
}
這兒有兩點需要特別注意:
1.要在循環檢測中等待條件滿足,這是因爲中斷事件並不一定是當前線程所期望的事件。線程等待被中斷後應該繼續檢測條件,以便決定是否進入下一輪等待。
2.當前線程在對wait方法調用時,必須是已經獲得wait方法所屬對象的內部鎖。也就是說,wait方法必須在互斥塊或者互斥方法體內調用,否則就會發生NotOwnerException錯誤。這種限制和前面所說的同步前提是互斥的說法是一致的。
上面代碼更通用的寫法是:
...
synchronized(lock){
while(!condition_expression){
try{
lock.wait();
}catch(InterruptedException ie){}
}
System.out.println("Now, condition met and it is ready!");
}
...
線程在synchronized語句獲取對象的內部鎖之後,在synchronized代碼塊期間就擁有了內部鎖。當判斷條件不成立時,可以調用該對象的wait方法進入等待狀態。
注意持有鎖的線程在調用wait方法進入等待狀態之後,會自動釋放持有的鎖。這樣做的目的是允許其他的線程進入臨界區繼續操作,以防止死鎖的發生。
舉生產者和消費者的例子。如果消費者在檢查時發現沒有產品生成,則調用wait方法等待生產者生產。如果此時消費者不釋放該鎖,生產者就會因爲獲取不到該鎖而處於阻塞狀態。而此時消費者卻在等待生產者生產出產品來,這樣雙方就進入死鎖狀態。因此wait方法需要在掛起線程後釋放該線程所擁有的鎖。
當wait方法調用後,線程進入等待狀態,直至未來某刻其他線程獲得該鎖並調用其invokeAll(或invoke)方法將其喚醒。該線程通過如下類似的代碼激活等待在此鎖上的線程:
public synchronized notifyOperation(){
condition_expression=true;
notifyAll();
}
假設線程C因檢測到某種條件不滿足而進入等待狀態,激活C線程的P線程往往需要和C線程建立“發生過”關係。也就是說程序期望線程P和C之間按照先P後C的順序執行。對於生產者和消費者例子來說,P就是生產者,C就是消費者,它們之間存在從P到C的“發生過”關係。
線程P在調用notify或者notifyAll方法時需要首先獲得該對象的鎖,因此這些代碼也需要放在synchronized代碼體內。上面的激活方法更通用的寫法是:
...
synchronized(lock){
condition_expression=true;
lock.notifyAll();
}
...
現舉生產者和消費者之間同步的例子。爲了簡化,假設生產者和消費者之間只共享一個容器。生產者生產出對象後放在在該容器中,而消費者從該容器中獲取該對象進行消費。消費者和生者之間往往需要建立雙向的“發生過”關係,即消費者只有在有東西才能消費,而生產者只有在有存放空間時才能生產。這兒爲了簡化,只假定保證消費者有東西可消費,生產者不管是否有空間可存放,只是將對象生產出來放在容器中。下面是這個例子的代碼:
public class TankContainer{
private Tank tank;
public synchronized void putTank(Tank tank){
//Dont bother to check whether it has room.
this.tank=tank;
notifyAll();
}
public synchronized Tank getTank(){
//Check whether there's tank to consume
while(tank==null){
//No tank yet, let's wait.
try{
wait();
}catch(InterruptedException e){}
}
Tank retValue=tank.
tank=null; //Clear tank.
return retValue;
}
}
public ProducerThread extends Thread{
//Shared TankContainer
private TankContainer container;
public ProducerThread(TankContainer container){
this.container=container;
}
...
public void run(){
while(true){
Tank tank=produceTank();
container.putTank(tank);
}
}
...
}
public ConsumerThread extends Thread{
//Shared TankContainer
private TankContainer container;
public ConsumerThread(TankContainer container){
this.container=container;
}
...
public void run(){
while(true){
Tank tank=container.getTank();
consumeTank(tank);
}
}
...
}
public class ProducerConsumer{
public static void main(String[]args){
TankContainer container=new TankContainer();//Shared TankContainer
new ProducerThread(container).start(); //Start to produce goods in its own thread.
new ConsumerThread(container).start(); //Start to consume goods in its own thread.
}
}
總結一下,同步編程時應該要記住下面幾條:
1.兩個線程應該獲取同一個對象的鎖。這是獲取同步的互斥性前提。
2.消費者線程應在循環體內檢測條件是否成立。
3.消費者線程在條件沒有滿足時應調用鎖對象的wait方法等待。
4.wait方法被中斷後應進入下一輪條件檢測循環。
5.生產者線程應該在其操作或結束返回之前調用鎖對象的notify或notifyAll方法激活等待線程。
補充一下notify和notifyAll方法的區別。notify激活等待隊列上的下一個線程。而notifyAll則激活所有等待線程。在生產者釋放鎖之後,這些被激活線程競爭獲取該鎖。獲得該鎖的線程只有一個,它從wait中返回,進入下一輪條件檢測。沒有獲得鎖的線程繼續進入等待狀態,等待下一次激活事件。
Java中除了通過互斥和同步技術來獲得代碼線程安全共性以外,還通過所謂恆量對象(immutable objects)的模式獲取線程安全性。其基本原理是恆量對象在創建完畢後就只能讀取,就像final對象一樣。後面的文章將對immuable對象技術進行詳細描述。