日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

Java并發(fā)與多線程教程(1)

 nacy2012 2015-05-20

Java并發(fā)性與多線程介紹


在過去單CPU時(shí)代,單任務(wù)在一個(gè)時(shí)間點(diǎn)只能執(zhí)行單一程序。之后發(fā)展到多任務(wù)階段,計(jì)算機(jī)能在同一時(shí)間點(diǎn)并行執(zhí)行多任務(wù)或多進(jìn)程。雖然并不是真正意義上的“同一時(shí)間點(diǎn)”,而是多個(gè)任務(wù)或進(jìn)程共享一個(gè)CPU,并交由操作系統(tǒng)來完成多任務(wù)間對(duì)CPU的運(yùn)行切換,以使得每個(gè)任務(wù)都有機(jī)會(huì)獲得一定的時(shí)間片運(yùn)行。

隨著多任務(wù)對(duì)軟件開發(fā)者帶來的新挑戰(zhàn),程序不在能假設(shè)獨(dú)占所有的CPU時(shí)間、所有的內(nèi)存和其他計(jì)算機(jī)資源。一個(gè)好的程序榜樣是在其不再使用這些資源時(shí)對(duì)其進(jìn)行釋放,以使得其他程序能有機(jī)會(huì)使用這些資源。

再后來發(fā)展到多線程技術(shù),使得在一個(gè)程序內(nèi)部能擁有多個(gè)線程并行執(zhí)行。一個(gè)線程的執(zhí)行可以被認(rèn)為是一個(gè)CPU在執(zhí)行該程序。當(dāng)一個(gè)程序運(yùn)行在多線程下,就好像有多個(gè)CPU在同時(shí)執(zhí)行該程序。

多線程比多任務(wù)更加有挑戰(zhàn)。多線程是在同一個(gè)程序內(nèi)部并行執(zhí)行,因此會(huì)對(duì)相同的內(nèi)存空間進(jìn)行并發(fā)讀寫操作。這可能是在單線程程序中從來不會(huì)遇到的問題。其中的一些錯(cuò)誤也未必會(huì)在單CPU機(jī)器上出現(xiàn),因?yàn)閮蓚€(gè)線程從來不會(huì)得到真正的并行執(zhí)行。然而,更現(xiàn)代的計(jì)算機(jī)伴隨著多核CPU的出現(xiàn),也就意味著不同的線程能被不同的CPU核得到真正意義的并行執(zhí)行。

如果一個(gè)線程在讀一個(gè)內(nèi)存時(shí),另一個(gè)線程正向該內(nèi)存進(jìn)行寫操作,那進(jìn)行讀操作的那個(gè)線程將獲得什么結(jié)果呢?是寫操作之前舊的值?還是寫操作成功之后 的新值?或是一半新一半舊的值?或者,如果是兩個(gè)線程同時(shí)寫同一個(gè)內(nèi)存,在操作完成后將會(huì)是什么結(jié)果呢?是第一個(gè)線程寫入的值?還是第二個(gè)線程寫入的值? 還是兩個(gè)線程寫入的一個(gè)混合值?因此如沒有合適的預(yù)防措施,任何結(jié)果都是可能的。而且這種行為的發(fā)生甚至不能預(yù)測(cè),所以結(jié)果也是不確定性的。

Java的多線程和并發(fā)性

Java是最先支持多線程的開發(fā)的語言之一,Java從一開始就支持了多線程能力,因此Java開發(fā)者能常遇到上面描述的問題場(chǎng)景。這也是我想為Java并發(fā)技術(shù)而寫這篇系列的原因。作為對(duì)自己的筆記,和對(duì)其他Java開發(fā)的追隨者都可獲益的。

該系列主要關(guān)注Java多線程,但有些在多線程中出現(xiàn)的問題會(huì)和多任務(wù)以及分布式系統(tǒng)中出現(xiàn)的存在類似,因此該系列會(huì)將多任務(wù)和分布式系統(tǒng)方面作為參考,所以叫法上稱為“并發(fā)性”,而不是“多線程”。


多線程的優(yōu)點(diǎn)


盡管面臨很多挑戰(zhàn),多線程有一些優(yōu)點(diǎn)使得它一直被使用。這些優(yōu)點(diǎn)是:

  • 資源利用率更好
  • 程序設(shè)計(jì)在某些情況下更簡(jiǎn)單
  • 程序響應(yīng)更快

資源利用率更好

想象一下,一個(gè)應(yīng)用程序需要從本地文件系統(tǒng)中讀取和處理文件的情景。比方說,從磁盤讀取一個(gè)文件需要5秒,處理一個(gè)文件需要2秒。處理兩個(gè)文件則需要:

  1.   5 seconds reading file A  
  2.   2 seconds processing file A  
  3.   5 seconds reading file B  
  4.   2 seconds processing file B  
  5. -----------------------  
  6.  14 seconds total  

從磁盤中讀取文件的時(shí)候,大部分的CPU時(shí)間用于等待磁盤去讀取數(shù)據(jù)。在這段時(shí)間里,CPU非常的空閑。它可以做一些別的事情。通過改變操作的順序,就能夠更好的使用CPU資源??聪旅娴捻樞颍?/p>

  1.  5 seconds reading file A  
  2.  5 seconds reading file B + 2 seconds processing file A  
  3.  2 seconds processing file B  
  4. ----------------------  
  5. 12 seconds total  

CPU等待第一個(gè)文件被讀取完。然后開始讀取第二個(gè)文件。當(dāng)?shù)诙募诒蛔x取的時(shí)候,CPU會(huì)去處理第一個(gè)文件。記住,在等待磁盤讀取文件的時(shí)候,CPU大部分時(shí)間是空閑的。

總的說來,CPU能夠在等待IO的時(shí)候做一些其他的事情。這個(gè)不一定就是磁盤IO。它也可以是網(wǎng)絡(luò)的IO,或者用戶輸入。通常情況下,網(wǎng)絡(luò)和磁盤的IO比CPU和內(nèi)存的IO慢的多。

程序設(shè)計(jì)更簡(jiǎn)單

在單線程應(yīng)用程序中,如果你想編寫程序手動(dòng)處理上面所提到的讀取和處理的順序,你必須記錄每個(gè)文件讀取和處理的狀態(tài)。相反,你可以啟動(dòng)兩個(gè)線程,每 個(gè)線程處理一個(gè)文件的讀取和操作。線程會(huì)在等待磁盤讀取文件的過程中被阻塞。在等待的時(shí)候,其他的線程能夠使用CPU去處理已經(jīng)讀取完的文件。其結(jié)果就 是,磁盤總是在繁忙地讀取不同的文件到內(nèi)存中。這會(huì)帶來磁盤和CPU利用率的提升。而且每個(gè)線程只需要記錄一個(gè)文件,因此這種方式也很容易編程實(shí)現(xiàn)。

程序響應(yīng)更快

將一個(gè)單線程應(yīng)用程序變成多線程應(yīng)用程序的另一個(gè)常見的目的是實(shí)現(xiàn)一個(gè)響應(yīng)更快的應(yīng)用程序。設(shè)想一個(gè)服務(wù)器應(yīng)用,它在某一個(gè)端口監(jiān)聽進(jìn)來的請(qǐng)求。當(dāng)一個(gè)請(qǐng)求到來時(shí),它去處理這個(gè)請(qǐng)求,然后再返回去監(jiān)聽。

服務(wù)器的流程如下所述:

  1. while(server is active){  
  2.   listen for request  
  3.   process request  
  4. }  
如果一個(gè)請(qǐng)求需要占用大量的時(shí)間來處理,在這段時(shí)間內(nèi)新的客戶端就無法發(fā)送請(qǐng)求給服務(wù)端。只有服務(wù)器在監(jiān)聽的時(shí)候,請(qǐng)求才能被接收。另一種設(shè)計(jì)是,監(jiān)聽線程把請(qǐng)求傳遞給工作者線程(worker thread),然后立刻返回去監(jiān)聽。而工作者線程則能夠處理這個(gè)請(qǐng)求并發(fā)送一個(gè)回復(fù)給客戶端。這種設(shè)計(jì)如下所述:
  1. while(server is active){  
  2.   listen for request  
  3.   hand request to worker thread  
  4. }  

這種方式,服務(wù)端線程迅速地返回去監(jiān)聽。因此,更多的客戶端能夠發(fā)送請(qǐng)求給服務(wù)端。這個(gè)服務(wù)也變得響應(yīng)更快。

桌面應(yīng)用也是同樣如此。如果你點(diǎn)擊一個(gè)按鈕開始運(yùn)行一個(gè)耗時(shí)的任務(wù),這個(gè)線程既要執(zhí)行任務(wù)又要更新窗口和按鈕,那么在任務(wù)執(zhí)行的過程中,這個(gè)應(yīng)用程 序看起來好像沒有反應(yīng)一樣。相反,任務(wù)可以傳遞給工作者線程(word thread)。當(dāng)工作者線程在繁忙地處理任務(wù)的時(shí)候,窗口線程可以自由地響應(yīng)其他用戶的請(qǐng)求。當(dāng)工作者線程完成任務(wù)的時(shí)候,它發(fā)送信號(hào)給窗口線程。窗口 線程便可以更新應(yīng)用程序窗口,并顯示任務(wù)的結(jié)果。對(duì)用戶而言,這種具有工作者線程設(shè)計(jì)的程序顯得響應(yīng)速度更快。


多線程的代價(jià)


從一個(gè)單線程的應(yīng)用到一個(gè)多線程的應(yīng)用并不僅僅帶來好處,它也會(huì)有一些代價(jià)。不要僅僅為了使用多線程而使用多線程。而應(yīng)該明確在使用多線程時(shí)能多來的好處比所付出的代價(jià)大的時(shí)候,才使用多線程。如果存在疑問,應(yīng)該嘗試測(cè)量一下應(yīng)用程序的性能和響應(yīng)能力,而不只是猜測(cè)。

設(shè)計(jì)更復(fù)雜

雖然有一些多線程應(yīng)用程序比單線程的應(yīng)用程序要簡(jiǎn)單,但其他的一般都更復(fù)雜。在多線程訪問共享數(shù)據(jù)的時(shí)候,這部分代碼需要特別的注意。線程之間的交互往往非常復(fù)雜。不正確的線程同步產(chǎn)生的錯(cuò)誤非常難以被發(fā)現(xiàn),并且重現(xiàn)以修復(fù)。

上下文切換的開銷

當(dāng)CPU從執(zhí)行一個(gè)線程切換到執(zhí)行另外一個(gè)線程的時(shí)候,它需要先存儲(chǔ)當(dāng)前線程的本地的數(shù)據(jù),程序指針等,然后載入另一個(gè)線程的本地?cái)?shù)據(jù),程序指針 等,最后才開始執(zhí)行。這種切換稱為“上下文切換”(“context switch”)。CPU會(huì)在一個(gè)上下文中執(zhí)行一個(gè)線程,然后切換到另外一個(gè)上下文中執(zhí)行另外一個(gè)線程。

上下文切換并不廉價(jià)。如果沒有必要,應(yīng)該減少上下文切換的發(fā)生。

你可以通過維基百科閱讀更多的關(guān)于上下文切換相關(guān)的內(nèi)容:

http://en./wiki/Context_switch

增加資源消耗

線程在運(yùn)行的時(shí)候需要從計(jì)算機(jī)里面得到一些資源。除了CPU,線程還需要一些內(nèi)存來維持它本地的堆棧。它也需要占用操作系統(tǒng)中一些資源來管理線程。我們可以嘗試編寫一個(gè)程序,讓它創(chuàng)建100個(gè)線程,這些線程什么事情都不做,只是在等待,然后看看這個(gè)程序在運(yùn)行的時(shí)候占用了多少內(nèi)存。


如何創(chuàng)建并運(yùn)行java線程


Java線程類也是一個(gè)object類,它的實(shí)例都繼承自java.lang.Thread或其子類。 可以用如下方式用java中創(chuàng)建一個(gè)線程:

  1. Tread thread = new Thread();  
執(zhí)行該線程可以調(diào)用該線程的start()方法:

  1. thread.start();  
在上面的例子中,我們并沒有為線程編寫運(yùn)行代碼,因此調(diào)用該方法后線程就終止了。

編寫線程運(yùn)行時(shí)執(zhí)行的代碼有兩種方式:一種是創(chuàng)建Thread子類的一個(gè)實(shí)例并重寫run方法,第二種是創(chuàng)建類的時(shí)候?qū)崿F(xiàn)Runnable接口。接下來我們會(huì)具體講解這兩種方法:

創(chuàng)建Thread的子類

創(chuàng)建Thread子類的一個(gè)實(shí)例并重寫run方法,run方法會(huì)在調(diào)用start()方法之后被執(zhí)行。例子如下:

  1. public class MyThread extends Thread {  
  2.     
  3.   public void run(){  
  4.      System.out.println("MyThread running");   
  5.   }  
可以用如下方式創(chuàng)建并運(yùn)行上述Thread子類
  1. MyThread myThread = new MyThread();  
  2. myTread.start();  

一旦線程啟動(dòng)后start方法就會(huì)立即返回,而不會(huì)等待到run方法執(zhí)行完畢才返回。就好像run方法是在另外一個(gè)cpu上執(zhí)行一樣。當(dāng)run方法執(zhí)行后,將會(huì)打印出字符串MyThread running。

你也可以如下創(chuàng)建一個(gè)Thread的匿名子類:

  1. Thread thread = new Thread(){  
  2.     public void run(){  
  3.       System.out.println("Thread Running");    
  4.     }  
  5.   }  
  6.    
  7.   thread.start();  
當(dāng)新的線程的run方法執(zhí)行以后,計(jì)算機(jī)將會(huì)打印出字符串”Thread Running”。

實(shí)現(xiàn)Runnable接口

         第二種編寫線程執(zhí)行代碼的方式是新建一個(gè)實(shí)現(xiàn)了java.lang.Runnable接口的類的實(shí)例,實(shí)例中的方法可以被線程調(diào)用。下面給出例子:

  1. public class MyRunnable implements Runnable {  
  2.   
  3.   public void run(){  
  4.      System.out.println("MyRunnable running");  
  5.   }  
  6. }  
為了使線程能夠執(zhí)行run()方法,需要在Thread類的構(gòu)造函數(shù)中傳入 MyRunnable的實(shí)例對(duì)象。示例如下:

  1. Thread thread = new Thread(new MyRunnable());  
  2. thread.start();   

         當(dāng)線程運(yùn)行時(shí),它將會(huì)調(diào)用實(shí)現(xiàn)了Runnable接口的run方法。上例中將會(huì)打印出”MyRunnable running”。

同樣,也可以創(chuàng)建一個(gè)實(shí)現(xiàn)了Runnable接口的匿名類,如下所示:

  1. Runnable myRunnable = new Runnable(){  
  2.   
  3.   public void run(){  
  4.      System.out.println("Runnable running");  
  5.   }  
  6. }  
  7.   
  8.   
  9. Thread thread = new Thread(myRunnable);  
  10. thread.start();  

創(chuàng)建子類還是實(shí)現(xiàn)Runnable接口?

對(duì)于這兩種方式哪種好并沒有一個(gè)確定的答案,它們都能滿足要求。就我個(gè)人意見,我更傾向于實(shí)現(xiàn)Runnable接口這種方法。因?yàn)榫€程池可以有效的 管理實(shí)現(xiàn)了Runnable接口的線程,如果線程池滿了,新的線程就會(huì)排隊(duì)等候執(zhí)行,直到線程池空閑出來為止。而如果線程是通過實(shí)現(xiàn)Thread子類實(shí)現(xiàn) 的,這將會(huì)復(fù)雜一些。

有時(shí)我們要同時(shí)融合實(shí)現(xiàn)Runnable接口和Thread子類兩種方式。例如,實(shí)現(xiàn)了Thread子類的實(shí)例可以執(zhí)行多個(gè)實(shí)現(xiàn)了Runnable接口的線程。一個(gè)典型的應(yīng)用就是線程池。

常見錯(cuò)誤:調(diào)用run()方法而非start()方法

創(chuàng)建并運(yùn)行一個(gè)線程所犯的常見錯(cuò)誤是調(diào)用線程的run()方法而非start()方法,如下所示:

  1. Thread newThread = new Thread(MyRunnable());  
  2. thread.run();  //should be start();  

起初你并不會(huì)感覺到有什么不妥,因?yàn)閞un()方法的確如你所愿的被調(diào)用了。但是,事實(shí)上,run()方法并非是由剛創(chuàng)建的新線程所執(zhí)行的,而是被 創(chuàng)建新線程的當(dāng)前線程所執(zhí)行了。也就是被執(zhí)行上面兩行代碼的線程所執(zhí)行的。想要讓創(chuàng)建的新線程執(zhí)行run()方法,必須調(diào)用新線程的start方法。

線程名

當(dāng)創(chuàng)建一個(gè)線程的時(shí)候,可以給線程起一個(gè)名字。它有助于我們區(qū)分不同的線程。例如:如果有多個(gè)線程寫入System.out,我們就能夠通過線程名容易的找出是哪個(gè)線程正在輸出。例子如下:

  1. MyRunnable runnable = new MyRunnable();  
  2. Thread thread = new Thread(runnable, "New Thread");    
  3.   
  4. thread.start();  
  5. System.out.println(thread.getName());  
需要注意的是,因?yàn)镸yRunnable并非Thread的子類,所以MyRunnable類并沒有g(shù)etName()方法??梢酝ㄟ^以下方式得到當(dāng)前線程的引用:
  1. Thread.currentThread();  
因此,通過如下代碼可以得到當(dāng)前線程的名字
  1. String threadName = Thread.currentThread().getName();  
線程代碼舉例:
這里是一個(gè)小小的例子。首先輸出執(zhí)行main()方法線程名字。這個(gè)線程由JVM分配的。然后開啟10個(gè)線程,命名為1~10。每個(gè)線程輸出自己的名字后就退出。
  1. public class ThreadExample {  
  2.       
  3.   public static void main(String[] args){  
  4.     System.out.println(Thread.currentThread().getName());  
  5.     for(int i=0; i<10; i++){  
  6.       new Thread("" + i){  
  7.         public void run(){  
  8.           System.out.println("Thread: " + getName() + " running");  
  9.         }  
  10.       }.start();  
  11.     }  
  12.   }  
  13. }  
需要注意的是,盡管啟動(dòng)線程的順序是有序的,但是執(zhí)行的順序并非是有序的。也就是說,1號(hào)線程并不一定是第一個(gè)將自己名字輸出到控制臺(tái)的線程。這是因?yàn)榫€程是并行執(zhí)行而非順序的。Jvm和操作系統(tǒng)一起決定了線程的執(zhí)行順序,它和線程的啟動(dòng)順序并非一定是一致的。


競(jìng)態(tài)條件與臨界區(qū)


在同一程序中運(yùn)行多個(gè)線程本身不會(huì)導(dǎo)致問題,問題在于多個(gè)線程訪問了相同的資源。如同一內(nèi)存區(qū)(變量,數(shù)組,或?qū)ο螅?、系統(tǒng)(數(shù)據(jù)庫(kù),web services等)或文件。實(shí)際上,這些問題只有在一或多個(gè)線程向這些資源做了寫操作時(shí)才有可能發(fā)生,只要資源沒有發(fā)生變化,多個(gè)線程讀取相同的資源就 是安全的。

多線程同時(shí)執(zhí)行下面的代碼可能會(huì)出錯(cuò):

  1. public class Counter {  
  2.   
  3.    protected long count = 0;  
  4.   
  5.    public void add(long value){  
  6.        this.count = this.count + value;     
  7.    }  
  8. }  
想象下線程A和B同時(shí)執(zhí)行同一個(gè)Counter對(duì)象的add()方法,我們無法知道操作系統(tǒng)何時(shí)會(huì)在兩個(gè)線程之間切換。JVM并不是將這段代碼視為單條指令來執(zhí)行的,而是按照下面的順序:
  1. 從內(nèi)存獲取 this.count 的值放到寄存器  
  2. 將寄存器中的值增加value  
  3. 將寄存器中的值寫回內(nèi)存  
觀察線程A和B交錯(cuò)執(zhí)行會(huì)發(fā)生什么:
  1. this.count = 0;  
  2.   A:    讀取 this.count 到一個(gè)寄存器 (0)  
  3.   B:    讀取 this.count 到一個(gè)寄存器 (0)  
  4.   B:    將寄存器的值加2  
  5.   B:    回寫寄存器值(2)到內(nèi)存. this.count 現(xiàn)在等于 2  
  6.   A:    將寄存器的值加3  
  7.   A:    回寫寄存器值(3)到內(nèi)存. this.count 現(xiàn)在等于 3  

兩個(gè)線程分別加了2和3到count變量上,兩個(gè)線程執(zhí)行結(jié)束后count變量的值應(yīng)該等于5。然而由于兩個(gè)線程是交叉執(zhí)行的,兩個(gè)線程從內(nèi)存中讀 出的初始值都是0。然后各自加了2和3,并分別寫回內(nèi)存。最終的值并不是期望的5,而是最后寫回內(nèi)存的那個(gè)線程的值,上面例子中最后寫回內(nèi)存的是線程A, 但實(shí)際中也可能是線程B。如果沒有采用合適的同步機(jī)制,線程間的交叉執(zhí)行情況就無法預(yù)料。

競(jìng)態(tài)條件 & 臨界區(qū)

當(dāng)兩個(gè)線程競(jìng)爭(zhēng)同一資源時(shí),如果對(duì)資源的訪問順序敏感,就稱存在競(jìng)態(tài)條件。導(dǎo)致競(jìng)態(tài)條件發(fā)生的代碼區(qū)稱作臨界區(qū)。上例中add()方法就是一個(gè)臨界區(qū),它會(huì)產(chǎn)生競(jìng)態(tài)條件。在臨界區(qū)中使用適當(dāng)?shù)耐骄涂梢员苊飧?jìng)態(tài)條件。


線程安全與共享資源


允許被多個(gè)線程同時(shí)執(zhí)行的代碼稱作線程安全的代碼。線程安全的代碼不包含競(jìng)態(tài)條件。當(dāng)多個(gè)線程同時(shí)更新共享資源時(shí)會(huì)引發(fā)競(jìng)態(tài)條件。因此,了解Java線程執(zhí)行時(shí)共享了什么資源很重要。

局部變量

局部變量存儲(chǔ)在線程自己的棧中。也就是說,局部變量永遠(yuǎn)也不會(huì)被多個(gè)線程共享。所以,基礎(chǔ)類型的局部變量是線程安全的。下面是基礎(chǔ)類型的局部變量的一個(gè)例子:

  1. public void someMethod(){    
  2.   long threadSafeInt = 0;  
  3.   threadSafeInt++;  
  4. }  

局部的對(duì)象引用

對(duì)象的局部引用和基礎(chǔ)類型的局部變量不太一樣。盡管引用本身沒有被共享,但引用所指的對(duì)象并沒有存儲(chǔ)在線程的棧內(nèi)。所有的對(duì)象都存儲(chǔ)在共享堆中。如果在某個(gè)方法中創(chuàng)建的對(duì)象不會(huì)逃逸出(譯者注:即該對(duì)象不會(huì)被其它方法獲得,也不會(huì)被非局部變量引用到)該方法,那么它就是線程安全的。實(shí)際上,哪怕將這個(gè)對(duì)象作為參數(shù)傳給其它方法,只要?jiǎng)e的線程獲取不到這個(gè)對(duì)象,那它仍是線程安全的。下面是一個(gè)線程安全的局部引用樣例:

  1. public void someMethod(){  
  2.     
  3.   LocalObject localObject = new LocalObject();  
  4.   
  5.   localObject.callMethod();  
  6.   method2(localObject);  
  7. }  
  8.   
  9. public void method2(LocalObject localObject){  
  10.   localObject.setValue("value");  
  11. }  

樣例中LocalObject對(duì)象沒有被方法返回,也沒有被傳遞給someMethod()方法外的對(duì)象。每個(gè)執(zhí)行someMethod()的線程 都會(huì)創(chuàng)建自己的LocalObject對(duì)象,并賦值給localObject引用。因此,這里的LocalObject是線程安全的。事實(shí)上,整個(gè) someMethod()都是線程安全的。即使將LocalObject作為參數(shù)傳給同一個(gè)類的其它方法或其它類的方法時(shí),它仍然是線程安全的。當(dāng)然,如 果LocalObject通過某些方法被傳給了別的線程,那它就不再是線程安全的了。

對(duì)象成員

對(duì)象成員存儲(chǔ)在堆上。如果兩個(gè)線程同時(shí)更新同一個(gè)對(duì)象的同一個(gè)成員,那這個(gè)代碼就不是線程安全的。下面是一個(gè)樣例:

  1. public class NotThreadSafe{  
  2.     StringBuilder builder = new StringBuilder();  
  3.       
  4.     public add(String text){  
  5.         this.builder.append(text);  
  6.     }     
  7. }  
如果兩個(gè)線程同時(shí)調(diào)用同一個(gè)NotThreadSafe實(shí)例上的add()方法,就會(huì)有競(jìng)態(tài)條件問題。例如:
  1. NotThreadSafe sharedInstance = new NotThreadSafe();  
  2.   
  3. new Thread(new MyRunnable(sharedInstance)).start();  
  4. new Thread(new MyRunnable(sharedInstance)).start();  
  5.   
  6. public class MyRunnable implements Runnable{  
  7.   NotThreadSafe instance = null;  
  8.     
  9.   public MyRunnable(NotThreadSafe instance){  
  10.     this.instance = instance;  
  11.   }  
  12.   
  13.   public void run(){  
  14.     this.instance.add("some text");  
  15.   }  
  16. }  

注意兩個(gè)MyRunnable共享了同一個(gè)NotThreadSafe對(duì)象。因此,當(dāng)它們調(diào)用add()方法時(shí)會(huì)造成競(jìng)態(tài)條件。

當(dāng)然,如果這兩個(gè)線程在不同的NotThreadSafe實(shí)例上調(diào)用call()方法,就不會(huì)導(dǎo)致競(jìng)態(tài)條件。下面是稍微修改后的例子:

  1. new Thread(new MyRunnable(new NotThreadSafe())).start();  
  2. new Thread(new MyRunnable(new NotThreadSafe())).start();  

現(xiàn)在兩個(gè)線程都有自己?jiǎn)为?dú)的NotThreadSafe對(duì)象,調(diào)用add()方法時(shí)就會(huì)互不干擾,再也不會(huì)有競(jìng)態(tài)條件問題了。所以非線程安全的對(duì)象仍可以通過某種方式來消除競(jìng)態(tài)條件。

線程控制逃逸規(guī)則

線程控制逃逸規(guī)則可以幫助你判斷代碼中對(duì)某些資源的訪問是否是線程安全的。

  1. <span style="color:#FF0000;"><strong>如果一個(gè)資源的創(chuàng)建,使用,銷毀都在同一個(gè)線程內(nèi)完成,  
  2. 且永遠(yuǎn)不會(huì)脫離該線程的控制,則該資源的使用就是線程安全的。</strong></span>  

資源可以是對(duì)象,數(shù)組,文件,數(shù)據(jù)庫(kù)連接,套接字等等。Java中你無需主動(dòng)銷毀對(duì)象,所以“銷毀”指不再有引用指向?qū)ο蟆?/p>

即使對(duì)象本身線程安全,但如果該對(duì)象中包含其他資源(文件,數(shù)據(jù)庫(kù)連接),整個(gè)應(yīng)用也許就不再是線程安全的了。比如2個(gè)線程都創(chuàng)建了各自的數(shù)據(jù)庫(kù)連接,每個(gè)連接自身是線程安全的,但它們所連接到的同一個(gè)數(shù)據(jù)庫(kù)也許不是線程安全的。比如,2個(gè)線程執(zhí)行如下代碼:

  1. 檢查記錄X是否存在,如果不存在,插入X  
如果兩個(gè)線程同時(shí)執(zhí)行,而且碰巧檢查的是同一個(gè)記錄,那么兩個(gè)線程最終可能都插入了記錄:
  1. 線程1檢查記錄X是否存在。檢查結(jié)果:不存在  
  2. 線程2檢查記錄X是否存在。檢查結(jié)果:不存在  
  3. 線程1插入記錄X  
  4. 線程2插入記錄X  
同樣的問題也會(huì)發(fā)生在文件或其他共享資源上。因此,區(qū)分某個(gè)線程控制的對(duì)象是資源本身,還是僅僅到某個(gè)資源的引用很重要。
總結(jié):

1. 局部變量中的基本數(shù)據(jù)類型(8種)永遠(yuǎn)是線程安全的。
2. 局部變量中的對(duì)象類型只要不會(huì)被其他線程訪問到,也是線程安全的。
3. 一個(gè)對(duì)象實(shí)例被多個(gè)線程同時(shí)訪問時(shí),他的成員變量就可能是線程不安全的。


線程安全與不可變性


當(dāng)多個(gè)線程同時(shí)訪問同一個(gè)資源,并且其中的一個(gè)或者多個(gè)線程對(duì)這個(gè)資源進(jìn)行了寫操作,才會(huì)產(chǎn)生競(jìng)態(tài)條件。多個(gè)線程同時(shí)讀同一個(gè)資源不會(huì)產(chǎn)生競(jìng)態(tài)條件。

我們可以通過創(chuàng)建不可變的共享對(duì)象來保證對(duì)象在線程間共享時(shí)不會(huì)被修改,從而實(shí)現(xiàn)線程安全。如下示例:

  1. public class ImmutableValue{  
  2.   
  3.   private int value = 0;  
  4.   
  5.   public ImmutableValue(int value){  
  6.     this.value = value;  
  7.   }  
  8.   
  9.   public int getValue(){  
  10.     return this.value;  
  11.   }  
  12. }  

請(qǐng)注意ImmutableValue類的成員變量value是通過構(gòu)造函數(shù)賦值的,并且在類中沒有set方法。這意味著一旦ImmutableValue實(shí)例被創(chuàng)建,value變量就不能再被修改,這就是不可變性。但你可以通過getValue()方法讀取這個(gè)變量的值。

譯者注:注意,“不變”(Immutable)和“只讀”(Read Only)是不同的。當(dāng)一個(gè)變量是“只讀”時(shí),變量的值不能直接改變,但是可以在其它變量發(fā)生改變的時(shí)候發(fā)生改變。比如,一個(gè)人的出生年月日是“不變”屬 性,而一個(gè)人的年齡便是“只讀”屬性,但是不是“不變”屬性。隨著時(shí)間的變化,一個(gè)人的年齡會(huì)隨之發(fā)生變化,而一個(gè)人的出生年月日則不會(huì)變化。這就是“不 變”和“只讀”的區(qū)別。(摘自《Java與模式》第34章)

如果你需要對(duì)ImmutableValue類的實(shí)例進(jìn)行操作,可以通過得到value變量后創(chuàng)建一個(gè)新的實(shí)例來實(shí)現(xiàn),下面是一個(gè)對(duì)value變量進(jìn)行加法操作的示例:

  1. public class ImmutableValue{  
  2.   
  3.   private int value = 0;  
  4.   
  5.   public ImmutableValue(int value){  
  6.     this.value = value;  
  7.   }  
  8.   
  9.   public int getValue(){  
  10.     return this.value;  
  11.   }  
  12.     
  13.   public ImmutableValue add(int valueToAdd){  
  14.     return new ImmutableValue(this.value + valueToAdd);  
  15.   }  
  16.     
  17. }  

請(qǐng)注意add()方法以加法操作的結(jié)果作為一個(gè)新的ImmutableValue類實(shí)例返回,而不是直接對(duì)它自己的value變量進(jìn)行操作。

引用不是線程安全的!

重要的是要記住,即使一個(gè)對(duì)象是線程安全的不可變對(duì)象,指向這個(gè)對(duì)象的引用也可能不是線程安全的。看這個(gè)例子:

  1. public void Calculator{  
  2.   private ImmutableValue currentValue = null;  
  3.   
  4.   public ImmutableValue getValue(){  
  5.     return currentValue;  
  6.   }  
  7.   
  8.   public void setValue(ImmutableValue newValue){  
  9.     this.currentValue = newValue;  
  10.   }  
  11.   
  12.   public void add(int newValue){  
  13.     this.currentValue = this.currentValue.add(newValue);  
  14.   }  
  15. }  

Calculator類持有一個(gè)指向ImmutableValue實(shí)例的引用。注意,通過setValue()方法和add()方法可能會(huì)改變這個(gè) 引用。因此,即使Calculator類內(nèi)部使用了一個(gè)不可變對(duì)象,但Calculator類本身還是可變的,因此Calculator類不是線程安全 的。換句話說:ImmutableValue類是線程安全的,但使用它的類不是。當(dāng)嘗試通過不可變性去獲得線程安全時(shí),這點(diǎn)是需要牢記的。

要使Calculator類實(shí)現(xiàn)線程安全,將getValue()、setValue()和add()方法都聲明為同步方法即可。


    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多