乡下人产国偷v产偷v自拍,国产午夜片在线观看,婷婷成人亚洲综合国产麻豆,久久综合给合久久狠狠狠9

  • <output id="e9wm2"></output>
    <s id="e9wm2"><nobr id="e9wm2"><ins id="e9wm2"></ins></nobr></s>

    • 分享

      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ù)間對CPU的運(yùn)行切換,以使得每個(gè)任務(wù)都有機(jī)會獲得一定的時(shí)間片運(yùn)行。

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

      再后來發(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í)行,因此會對相同的內(nèi)存空間進(jìn)行并發(fā)讀寫操作。這可能是在單線程程序中從來不會遇到的問題。其中的一些錯(cuò)誤也未必會在單CPU機(jī)器上出現(xiàn),因?yàn)閮蓚€(gè)線程從來不會得到真正的并行執(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)存,在操作完成后將會是什么結(jié)果呢?是第一個(gè)線程寫入的值?還是第二個(gè)線程寫入的值? 還是兩個(gè)線程寫入的一個(gè)混合值?因此如沒有合適的預(yù)防措施,任何結(jié)果都是可能的。而且這種行為的發(fā)生甚至不能預(yù)測,所以結(jié)果也是不確定性的。

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

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

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


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


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

      • 資源利用率更好
      • 程序設(shè)計(jì)在某些情況下更簡單
      • 程序響應(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會去處理第一個(gè)文件。記住,在等待磁盤讀取文件的時(shí)候,CPU大部分時(shí)間是空閑的。

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

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

      在單線程應(yīng)用程序中,如果你想編寫程序手動處理上面所提到的讀取和處理的順序,你必須記錄每個(gè)文件讀取和處理的狀態(tài)。相反,你可以啟動兩個(gè)線程,每 個(gè)線程處理一個(gè)文件的讀取和操作。線程會在等待磁盤讀取文件的過程中被阻塞。在等待的時(shí)候,其他的線程能夠使用CPU去處理已經(jīng)讀取完的文件。其結(jié)果就 是,磁盤總是在繁忙地讀取不同的文件到內(nèi)存中。這會帶來磁盤和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)來的請求。當(dāng)一個(gè)請求到來時(shí),它去處理這個(gè)請求,然后再返回去監(jiān)聽。

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

      1. while(server is active){  
      2.   listen for request  
      3.   process request  
      4. }  
      如果一個(gè)請求需要占用大量的時(shí)間來處理,在這段時(shí)間內(nèi)新的客戶端就無法發(fā)送請求給服務(wù)端。只有服務(wù)器在監(jiān)聽的時(shí)候,請求才能被接收。另一種設(shè)計(jì)是,監(jiān)聽線程把請求傳遞給工作者線程(worker thread),然后立刻返回去監(jiān)聽。而工作者線程則能夠處理這個(gè)請求并發(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ā)送請求給服務(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)其他用戶的請求。當(dāng)工作者線程完成任務(wù)的時(shí)候,它發(fā)送信號給窗口線程。窗口 線程便可以更新應(yīng)用程序窗口,并顯示任務(wù)的結(jié)果。對用戶而言,這種具有工作者線程設(shè)計(jì)的程序顯得響應(yīng)速度更快。


      多線程的代價(jià)


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

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

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

      上下文切換的開銷

      當(dāng)CPU從執(zhí)行一個(gè)線程切換到執(zhí)行另外一個(gè)線程的時(shí)候,它需要先存儲當(dāng)前線程的本地的數(shù)據(jù),程序指針等,然后載入另一個(gè)線程的本地?cái)?shù)據(jù),程序指針 等,最后才開始執(zhí)行。這種切換稱為“上下文切換”(“context switch”)。CPU會在一個(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接口。接下來我們會具體講解這兩種方法:

      創(chuàng)建Thread的子類

      創(chuàng)建Thread子類的一個(gè)實(shí)例并重寫run方法,run方法會在調(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();  

      一旦線程啟動后start方法就會立即返回,而不會等待到run方法執(zhí)行完畢才返回。就好像run方法是在另外一個(gè)cpu上執(zhí)行一樣。當(dāng)run方法執(zhí)行后,將會打印出字符串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ī)將會打印出字符串”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í)例對象。示例如下:

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

               當(dāng)線程運(yùn)行時(shí),它將會調(diào)用實(shí)現(xiàn)了Runnable接口的run方法。上例中將會打印出”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接口?

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

      起初你并不會感覺到有什么不妥,因?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. }  
      需要注意的是,盡管啟動線程的順序是有序的,但是執(zhí)行的順序并非是有序的。也就是說,1號線程并不一定是第一個(gè)將自己名字輸出到控制臺的線程。這是因?yàn)榫€程是并行執(zhí)行而非順序的。Jvm和操作系統(tǒng)一起決定了線程的執(zhí)行順序,它和線程的啟動順序并非一定是一致的。


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


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

      多線程同時(shí)執(zhí)行下面的代碼可能會出錯(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對象的add()方法,我們無法知道操作系統(tǒng)何時(shí)會在兩個(gè)線程之間切換。JVM并不是將這段代碼視為單條指令來執(zhí)行的,而是按照下面的順序:
      1. 從內(nèi)存獲取 this.count 的值放到寄存器  
      2. 將寄存器中的值增加value  
      3. 將寄存器中的值寫回內(nèi)存  
      觀察線程A和B交錯(cuò)執(zhí)行會發(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ù)料。

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

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


      線程安全與共享資源


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

      局部變量

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

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

      局部的對象引用

      對象的局部引用和基礎(chǔ)類型的局部變量不太一樣。盡管引用本身沒有被共享,但引用所指的對象并沒有存儲在線程的棧內(nèi)。所有的對象都存儲在共享堆中。如果在某個(gè)方法中創(chuàng)建的對象不會逃逸出(譯者注:即該對象不會被其它方法獲得,也不會被非局部變量引用到)該方法,那么它就是線程安全的。實(shí)際上,哪怕將這個(gè)對象作為參數(shù)傳給其它方法,只要?jiǎng)e的線程獲取不到這個(gè)對象,那它仍是線程安全的。下面是一個(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對象沒有被方法返回,也沒有被傳遞給someMethod()方法外的對象。每個(gè)執(zhí)行someMethod()的線程 都會創(chuàng)建自己的LocalObject對象,并賦值給localObject引用。因此,這里的LocalObject是線程安全的。事實(shí)上,整個(gè) someMethod()都是線程安全的。即使將LocalObject作為參數(shù)傳給同一個(gè)類的其它方法或其它類的方法時(shí),它仍然是線程安全的。當(dāng)然,如 果LocalObject通過某些方法被傳給了別的線程,那它就不再是線程安全的了。

      對象成員

      對象成員存儲在堆上。如果兩個(gè)線程同時(shí)更新同一個(gè)對象的同一個(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()方法,就會有競態(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對象。因此,當(dāng)它們調(diào)用add()方法時(shí)會造成競態(tài)條件。

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

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

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

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

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

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

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

      即使對象本身線程安全,但如果該對象中包含其他資源(文件,數(shù)據(jù)庫連接),整個(gè)應(yīng)用也許就不再是線程安全的了。比如2個(gè)線程都創(chuàng)建了各自的數(shù)據(jù)庫連接,每個(gè)連接自身是線程安全的,但它們所連接到的同一個(gè)數(shù)據(jù)庫也許不是線程安全的。比如,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  
      同樣的問題也會發(fā)生在文件或其他共享資源上。因此,區(qū)分某個(gè)線程控制的對象是資源本身,還是僅僅到某個(gè)資源的引用很重要。
      總結(jié):

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


      線程安全與不可變性


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

      我們可以通過創(chuàng)建不可變的共享對象來保證對象在線程間共享時(shí)不會被修改,從而實(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. }  

      請注意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è)人的年齡會隨之發(fā)生變化,而一個(gè)人的出生年月日則不會變化。這就是“不 變”和“只讀”的區(qū)別。(摘自《Java與模式》第34章)

      如果你需要對ImmutableValue類的實(shí)例進(jìn)行操作,可以通過得到value變量后創(chuàng)建一個(gè)新的實(shí)例來實(shí)現(xiàn),下面是一個(gè)對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. }  

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

      引用不是線程安全的!

      重要的是要記住,即使一個(gè)對象是線程安全的不可變對象,指向這個(gè)對象的引用也可能不是線程安全的??催@個(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()方法可能會改變這個(gè) 引用。因此,即使Calculator類內(nèi)部使用了一個(gè)不可變對象,但Calculator類本身還是可變的,因此Calculator類不是線程安全 的。換句話說:ImmutableValue類是線程安全的,但使用它的類不是。當(dāng)嘗試通過不可變性去獲得線程安全時(shí),這點(diǎn)是需要牢記的。

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


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

        0條評論

        發(fā)表

        請遵守用戶 評論公約

        類似文章 更多