Java的線程編程非常簡(jiǎn)單。但有時(shí)會(huì)看到一些關(guān)于線程的錯(cuò)誤用法。下面列出一些應(yīng)該注意的問題。
1.同步對(duì)象的恒定性All java objects are references.
對(duì)于局部變量和參數(shù)來說,java里面的int, float, double, boolean等基本數(shù)據(jù)類型,都在棧上。這些基本類型是無法同步的;java里面的對(duì)象(根對(duì)象是Object),全都在堆里,指向?qū)ο蟮膔eference在棧上。
java中的同步對(duì)象,實(shí)際上是對(duì)于reference所指的“對(duì)象地址”進(jìn)行同步。
需要注意的問題是,千萬不要對(duì)同步對(duì)象重新賦值。舉個(gè)例子。
class A implements Runnable{
Object lock = new Object();
void run(){
for(...){
synchronized(lock){
// do something
...
lock = new Object(); } } } run函數(shù)里面的這段同步代碼實(shí)際上是毫無意義的。因?yàn)槊恳淮蝜ock都給重新分配了新的對(duì)象的reference,每個(gè)線程都在新的reference同步。
大家可能覺得奇怪,怎么會(huì)舉這么一個(gè)例子。因?yàn)槲乙娺^這樣的代碼,同步對(duì)象在其它的函數(shù)里被重新賦了新值。
這種問題很難查出來。
所以,一般應(yīng)該把同步對(duì)象聲明為final.
final Object lock = new Object();
使用Singleton Pattern 設(shè)計(jì)模式來獲取同步對(duì)象,也是一種很好的選擇。
2.如何放置共享數(shù)據(jù)實(shí)現(xiàn)線程,有兩種方法,一種是繼承Thread類,一種是實(shí)現(xiàn)Runnable接口。
上面舉的例子,采用實(shí)現(xiàn)Runnable接口的方法。本文推薦這種方法。
首先,把需要共享的數(shù)據(jù)放在一個(gè)實(shí)現(xiàn)Runnable接口的類里面,然后,把這個(gè)類的實(shí)例傳給多個(gè)Thread的構(gòu)造方法。這樣,新創(chuàng)建的多個(gè)Thread,都共同擁有一個(gè)Runnable實(shí)例,共享同一份數(shù)據(jù)。
如果采用繼承Thread類的方法,就只好使用static靜態(tài)成員了。如果共享的數(shù)據(jù)比較多,就需要大量的static靜態(tài)成員,令程序數(shù)據(jù)結(jié)構(gòu)混亂,難以擴(kuò)展。這種情況應(yīng)該盡量避免。
編寫一段多線程代碼,處理一個(gè)稍微復(fù)雜點(diǎn)的問題。兩種方法的優(yōu)劣,一試便知。
3.同步的粒度線程同步的粒度越小越好,即,線程同步的代碼塊越小越好。盡量避免用synchronized修飾符來聲明方法。盡量使用synchronized(anObject)的方式,如果不想引入新的同步對(duì)象,使用synchronized(this)的方式。而且,synchronized代碼塊越小越好。
4.線程之間的通知這里使用“通知”這個(gè)詞,而不用“通信”這個(gè)詞,是為了避免詞義的擴(kuò)大化。
線程之間的通知,通過Object對(duì)象的wait()和notify() 或notifyAll() 方法實(shí)現(xiàn)。
下面用一個(gè)例子,來說明其工作原理:
假設(shè)有兩個(gè)線程,A和B。共同擁有一個(gè)同步對(duì)象,lock。
1.首先,線程A通過synchronized(lock) 獲得lock同步對(duì)象,然后調(diào)用lock.wait()函數(shù),放棄lock同步對(duì)象,線程A停止運(yùn)行,進(jìn)入等待隊(duì)列。
2.線程B通過synchronized(lock) 獲得線程A放棄的lock同步對(duì)象,做完一定的處理,然后調(diào)用 lock.notify() 或者lock.notifyAll() 通知等待隊(duì)列里面的線程A。
3.線程A從等待隊(duì)列里面出來,進(jìn)入ready隊(duì)列,等待調(diào)度。
4.線程B繼續(xù)處理,出了synchronized(lock)塊之后,放棄lock同步對(duì)象。
5.線程A獲得lock同步對(duì)象,繼續(xù)運(yùn)行。
例子代碼如下:
public class SharedResource implements Runnable{
Object lock = new Object();
public void run(){
// 獲取當(dāng)前線程的名稱。
String threadName = Thread.currentThread().getName();
if( “A”.equals(threadName)){
synchronized(lock){ //線程A通過synchronized(lock) 獲得lock同步對(duì)象
try{
System.out.println(“ A gives up lock.”);
lock.wait(); // 調(diào)用lock.wait()函數(shù),放棄lock同步對(duì)象,
// 線程A停止運(yùn)行,進(jìn)入等待隊(duì)列。
}catch(InterruptedException e){ } // 線程A重新獲得lock同步對(duì)象之后,繼續(xù)運(yùn)行。
System.out.println(“ A got lock again and continue to run.”);
} // end of synchronized(lock) } if( “B”.equals(threadName)){
synchronized(lock){//線程B通過synchronized(lock) 獲得線程A放棄的lock同步對(duì)象
System.out.println(“B got lock.”);
lock.notify(); //通知等待隊(duì)列里面的線程A,進(jìn)入ready隊(duì)列,等待調(diào)度。
//線程B繼續(xù)處理,出了synchronized(lock)塊之后,放棄lock同步對(duì)象。
System.out.println(“B gives up lock.”);
} // end of synchronized(lock)
boolean hasLock = Thread.holdsLock(lock); // 檢查B是否擁有l(wèi)ock同步對(duì)象。
System.out.println(“B has lock ? -- ” hasLock); // false. } } } public class TestMain{
public static void main(){
Runnable resource = new SharedResource();
Thread A = new Thread(resource,”A”);
A.start();
// 強(qiáng)迫主線程停止運(yùn)行,以便線程A開始運(yùn)行。
try {
Thread.sleep(500);
}catch(InterruptedException e){ } Thread B = new Thread(resource,”B”);
B.start(); } }
5.跨類的同步對(duì)象對(duì)于簡(jiǎn)單的問題,可以把訪問共享資源的同步代碼都放在一個(gè)類里面。
但是對(duì)于復(fù)雜的問題,我們需要把問題分為幾個(gè)部分來處理,需要幾個(gè)不同的類來處理問題。這時(shí),就需要在不同的類中,共享同步對(duì)象。比如,在生產(chǎn)者和消費(fèi)者之間共享同步對(duì)象,在讀者和寫者之間共享同步對(duì)象。
如何在不同的類中,共享同步對(duì)象。有幾種方法實(shí)現(xiàn),
(1)前面講過的方法,使用static靜態(tài)成員,(或者使用Singleton Pattern.)
(2)用參數(shù)傳遞的方法,把同步對(duì)象傳遞給不同的類。
(3)利用字符串常量的“原子性”。
對(duì)于第三種方法,這里做一下解釋。一般來說,程序代碼中的字符串常量經(jīng)過編譯之后,都具有唯一性,即,內(nèi)存中不會(huì)存在兩份相同的字符串常量。
(通常情況下,C ,C語言程序編譯之后,也具有同樣的特性。)
比如,我們有如下代碼。
String A = “atom”;
String B = “atom”;
我們有理由認(rèn)為,A和B指向同一個(gè)字符串常量。即,A==B。
注意,聲明字符串變量的代碼,不符合上面的規(guī)則。
String C= new String(“atom”);
String D = new String(“atom”);
這里的C和D的聲明是字符串變量的聲明,所以,C != D。
有了上述的認(rèn)識(shí),我們就可以使用字符串常量作為同步對(duì)象。
比如我們?cè)诓煌念愔校褂胹ynchronized(“myLock”), “myLock”.wait(),“myLock”.notify(), 這樣的代碼,就能夠?qū)崿F(xiàn)不同類之間的線程同步。
本文并不強(qiáng)烈推薦這種用法,只是說明,有這樣一種方法存在。
本文推薦第二種方法,(2)用參數(shù)傳遞的方法,把同步對(duì)象傳遞給不同的類。
--