Java 多線程學習筆記

如要轉載請標明作者zjrodger和出處:http://blog.csdn.net/zjrodger/,謝謝微笑

 

                                                                                多線程筆記目錄

·多線程的概念

(·)線程和進程

1.進程的概念

2.線程的概念

3.多線程的特性

4.注意區別並行併發

 

·多線程的創建和啓動

(·)實現多線程的綜述

1.綜述

2.爲什麼要覆蓋run()方法?

3. run()方法和start()方法的區別

(·)繼承Thread

(·)實現Runnable接口實現多線程

 

·多線程的生命週期

1.線程的5種狀態

(1)新建狀態(new)

(2)就緒狀態(Runnable)

(3)運行狀態(Running)

(4)阻塞狀態(Blocked)

(5)死亡狀態(Dead)

2.線程運行和線程調度策略

 

·控制線程

(·)join線程

(·)後臺線程

       1.何爲後臺線程

       2.後臺線程的特徵

       3.與後臺線程相關的函數

(·)線程睡眠

(·)線程讓步yield

(·)改變線程優先級

 

 

·多線程的安全問題——同步

(·)發生線程安全問題的上下文情景——提出問題和解決問題

1.提出問題

2.解決問題

(·)同步機制的解釋

(·)同步機制的實現

同步能夠保證線程安全的前提重要

方法一:同步代碼塊

1.語法格式和說明

2.如何去確定哪些語句需要放在同步代碼塊中(臨界區的確定

3.同步監視器對象的指定

4.代碼示例

(1)示例代碼1:模擬多線程銀行取款機的實現。

方法二:同步方法

1.什麼是同步方法

2.構造線程安全的類

3.代碼示例

(1)代碼示例1:模擬多線程一行取款機的運行。

(·)釋放同步監視器的鎖定

1.線程釋放同步監視器鎖定的情況

2.線程不會釋放同步監視器鎖定的情況

(·)同步鎖

(·)線程同步和單例設計模式的結合(重要)

1.餓漢式

    (1)餓漢式的代碼寫法

    (2)餓漢式的特點:線程安全。

2.懶漢式

    (1)懶漢式的“線程不安全”的寫法

    (2)懶漢式的“線程不安全”的原因

    (3)懶漢式能夠保障“線程安全”的正確的代碼寫法

        3.1) 效率比較的寫法

        3.2) 效率比較的寫法

(·)死鎖

 

·線程間通信

一.引子1

引子2

二.線程的等待和喚醒過程








 

·多線程的概念

(·)線程和進程

1. 進程的概念

       幾乎所有的OS都支持進程的概念,所有運行中的任務通常對應一個進程(Process)。當一個程序進入到內存中運行時,即變成一個進程。進程是處於運行中的程序,並具有一定的獨立功能,進程是系統進行資源分配和調度的一個獨立單元。

       進程包含如下三個特性:

(1)獨立性。每個進程都有自己私有的地址空間。

(2)動態性。

(3)併發性。

 

2. 線程的概念

(1)線程是輕量級的進程

(2)線程沒有自己獨立的地址空間

(3)線程是由進程創建的(寄生在進程中)

(4)一個進程可以擁有多個線程,一個線程必須有一個父進程。

在實際應用中,多線程是非常有用的,一個瀏覽器必須能夠同時下載多個圖片;一個Web服務器必須能夠同時響應多個用戶請求;Java虛擬機本省就在後臺提供了一個線程來進行垃圾回收;圖形界面(GUI)應用也需要啓動單獨的線程從主機環境收集用戶界面事件。總之,只要程序涉及到併發,就離不開多線程編程。

       歸納起來可以這樣說:

單個OS可以同時執行多個任務,每個任務就是一個進程;

單個進程可以同時執行多個任務,每個任務就是一個線程。

 

3.多線程的特性

       多線程的特性是隨機性,多線程在運行時可以理解爲他們在互相搶奪CPU資源,誰搶到誰就被執行。至於每個搶到CPU資源的線程執行多長時間,則由CPU說了算。      

 

4.注意區別並行併發

(1)並行:parallel,指在同一時刻,有多條指令在多個處理器上同時執行。

(2)併發:concurrency,指在同一時刻,只能有一條指令執行,但多個進程指令被快速切換執行,使得在宏觀上具有有多個進程同時執行的效果。

 

 

 

 

·多線程的創建和啓動

(·)實現多線程的綜述

1.綜述

在java中,一個類要當成線程來使用有兩種方法(任何一個類都可以變成線程來使用):

(1)繼承thread類,並重寫run方法。

(2)實現Runnable接口,並重寫run方法。

假設要將類A變爲多線程類,一般情況下,建議使用“實現Runnable接口”的方法來實現多線程類A的。因爲Java是單繼承的,通過實現Runable接口做可以爲A留下一個繼承其它類的機會

調用線程類的start()方法使該線程開始執行;Java 虛擬機會去自動調用該線程的run 方法,主線程的代碼都存放在main()方法中。

 

2.爲什麼要覆蓋run()方法?

       目的:將自定義的代碼存儲在run()方法中,讓線程運行。Thread父類僅僅是提供了一個存放代碼的空間(即,run()的方法體)。

      

3.run()方法和start()方法的區別

如果僅僅調用run()方法,則結果就是程序還是一個單線程程序;只有調用start()方法,程序才能啓動多線程。

run()方法就像個容器,僅僅是封裝多線程要運行的代碼,並不能啓動多線程;Java程序不會創建線程,Java程序通過調用start()方法,然後start()方法調用OS的底層代碼,去啓動多線程。

 

             

 

 

(·)繼承Thread

package test1;
 
class ThreadTestextends Thread{ 
   public void run(){
      while(true){
         System.out.println("run()方法,返回當前正在執行的線程對象的引用名:"+Thread.currentThread().getName());
      }
   }
}
 
public class Demo1 {
   public static void main(String[] args) {
      new ThreadTest().start();
      while(true){
         System.out.println("在主類中的main():返回當前正在執行的線程對象的引用名:"+Thread.currentThread().getName());
      }
   }
}

代碼分析:

0.API解釋:public staticThread currentThread() 返回對當前正在執行的線程對象的引用。

1.線程的建立:想要將一段代碼放在一個線程中,可以繼承Thread類,或者重新Rnnable接口。

2.線程的啓動start()àrun()的本質

class A extends Thread

(1)繼承Thread類的那個子類A中並沒有start()方法,而start()方法是父類Thread類中的方法,但由於子類A繼承了Thread類,所以子類A也可以調用父類中的start()方法。start()使該線程開始執行;Java虛擬機會自動調用該線程的run 方法。

(2)Thread類中的run()方法是空的,子類A繼承Thread類時必須重寫父類Thread中的run()方法。

3.一個線程類只能啓動一次。不管這個線程是通過繼承Thread類來實現的,還是實現Runnable接口實現的

4.關於資源共享要注意的問題

使用繼承Thread類的子類來創建線程類時,多個線程類之間無法共享線程類的實例變量,即,無法共享資源,因爲在每次創建線程類對象時,都要重新創建一個對象。

       這點和實現Runnable接口來創建線程類有很大的不同。

 

 

(·)實現Runnable接口實現多線程

使用Runnable接口創建多線程:

(1)適合多個相同程序代碼的線程去處理同一資源的情況,把虛擬CPU(線程)同程序的代碼、數據有效分離,較好的體現了面向對象的設計思想。

(2)可以避免由於Java的單繼承特性帶來的侷限。

(3)當線程被構造時,需要的代碼和數據通過一個對象作爲構造函數實參傳遞進去,這個對象就是一個實現了Runnable接口類的實例。

(4)事實上,幾乎所有多線程應用都可用Runnable接口方式。

(5)Runnable對象僅僅作爲Thread對象的target,Runnable實現類中包含的run()方法僅僅作爲線程的執行體。而實際的線程對象依然是Thread實例,只是該Thread線程負責執行Runnbale實現類中的run()方法。

(6)通過實現Runnable接口和繼承Thread類來實現多線程的區別:

前者可以實現資源共享,多個線程可以同時作用於一個資源,而後者則不可以。

 

注意:實現Runnable接口的類並不是線程類,只有Thread類和繼承Thread類的子纔是線程類。 

通過實現Runnable接口的方式實現多線程的好處:避免了單繼承的侷限性。在定義線程時,建議使用實現方式。 

/**
 * 功能:模擬鐵路售票系統
 * */
package test1;
//創建一個線程對象時,只能創建一個線程,
//想要實現售票系統,則必須創建一個共有的資源對象,
//再讓多個線程共同去使用這個資源對象。
//可以通過讓一個類ThreadTest實現Runnable接口去實現這個需求
//實現方法:
//首先,創建一個ThreadTest對象(已經繼承了Runnable接口),由於ThreadTest對象中已經有了
//tickets這個屬性,所以就可以作爲即將創建的多個線程的共有的資源
//其次,通過Thread類生成多個線程,這時每一個線程都可以使用tickets這個共同的資源了。
class ThreadTestimplements Runnable{
   int tickets = 40; 
   public void run(){
      while(true){
         if(tickets>0){
            try {
                Thread.sleep(300);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }           System.out.println(Thread.currentThread().getName()+"正在賣"+tickets+"號票。");
            --tickets;
         }
      }
   }
}
 
public class Demo1{
   public static void main(String[] args){
      ThreadTest tt = new ThreadTest();
      Thread t1 = new Thread(tt);
      t1.start();
      Thread t2 = new Thread(tt);
      t2.start();
      Thread t3 = new Thread(tt);
      t3.start();
      Thread t4 = new Thread(tt);
      t4.start();
   }
}

注意,若運行的結果爲:

會出現“票數變爲負數”和“同一張票數被打印多次的問題”

這就涉及到了“多線程的同步(多線程的安全,線程死鎖問題)”

 

代碼分析:

根據上邊的代碼片段

System.out.println(Thread.currentThread().getName()+"正在賣"+tickets+"號票。");

            --tickets;

下面截取多個線程運行時的某個片段來分析。

當tickets減少到1時,CPU切換到線程1,由於tickets的值滿足if(tickets>0)的條件,線程1會先打印出"正在賣1號票。"在線程1還沒來得及對tickets進行減減運算前,CPU又切換到線程2,tickets=1的值滿足線程2if(tickets>0)的條件,線程2又會打印出"正在賣1號票。"(這就是爲什麼會出現“同一張票數會被打印多次”的問題)之後線程2再對tickets進行減減運算,此時tickets就變成0,但此時CPU又切換到線程1,由於剛纔已經判斷過了if()條件,而且線程1已經完成了“打印”的任務,所以線程1剩餘的任務就是將tickets的值進行減減運算,運算後,tickets的值就變成了-1,之後線程1再次去判斷tickets的條件看是否能滿足下次運行的條件,經判斷不滿足,此時線程1就結束了。

這就是爲什麼雖然打印條件的判斷語句爲“if(tickets> 0)才能打印”,但程序依然能夠打印,而且“票數會變成負值”的原因。如果線程的數量更多,tickets還可能爲-2,或者-3.

 

 

 ·多線程的生命週期


1.線程的5種狀態

(1)新建狀態(new)

    當程序用new關鍵字創建了一個線程之後,該線程就處於新建狀態,此時,它和其他的Java對象一樣,僅僅由Java虛擬機分配內存,並且初始化其成員變量的值。此時的線程對象沒有表現出任何線程的動態特性,程序也不會執行線程的線程主體。   

(2)就緒狀態(Runnable)

    簡而言之:此時的線程具備運行資格,但沒有CPU執行權,就處於等待狀態。

    當某線程對象調用了start()方法後,該線程對象就處於就緒狀態,該線程在等待獲得CPU資源的執行權,此時,Java虛擬機會爲其創建方法調用棧和程序計數器,處於這個狀態中的線程並沒有開始運行,只是表示該線程可以運行了。至於該線程何時開始運行,取決於JVM裏線程調度器的調度。

    注意點:

    ①啓動線程使用start()方法,而不是run()方法。

    ②只能對新建狀態的線程使用start()方法,否則,將引發IllegalThreadStateException。

(3)運行狀態(Running)

    如果處於就緒狀態的線程獲得了CPU,則開始執行run()方法的線程執行體,則該線程就處於運行狀態。

    當發生如下狀況時,線程間進入阻塞狀態:

    ①線程調用sleep()方法主動放棄所佔用的CPU資源。

    ②線程調用了一個阻塞式的IO方法(即等待用戶輸入的IO方法),在該方法返回之前,該線程被阻塞。

    ③線程試圖獲得一個“同步監視器(即同步鎖對象)”,但該同步監視器正被其他縣城所持有。

    ④線程在等待某個通知(notify)

    ⑤程序調用了線程的suspend()方法,將該線程掛起。但這個方法容易造成死鎖,所以應該儘量避免使用該方法。

(4)阻塞狀態(Blocked)

    被阻塞的線程在合適的時候就進入就緒狀態,注意是就緒狀態而不是運行狀態。即被阻塞的線程的阻塞被解除後,必須重新等待縣城調度器再次調用它。

    當發生如下特定的情況時,可以解除線程的阻塞,讓該線程重新進入就緒狀態。

    ①調用sleep()方法經過了指定的等待時間。

    ②線程調用的阻塞式IO方法已經返回。

    ③線程成功地獲得了試圖取得的同步監視器對象。

    ④線程正在等待某個通知時,其他線程發出了一個通知。

    ⑤處於掛起狀態(suspend()狀態)的線程被調用了resume()方法。

(5)死亡狀態(Dead)

    線程會以如下3種方式結束,結束後就處於死亡狀態。

    ①run()或call()方法執行完成,線程正常結束。

    ②線程拋出一個未捕獲的Exception或者Error。

    ③直接調用該線程的stop()方法來結束線程——該方法容易導致死鎖,通常不推薦使用。

    當主線程結束時,其他線程不受任何影響,並不會隨之結束。一旦子線程啓動之後,它就擁有和主線程相同的地位,他不會受主線程的影響。     

 

2.線程運行和線程調度策略

       當一個線程開始運行後,他不可能一直處於運行狀態(除非它的線程執行時間足夠短,瞬間就能夠結束了),線程在運行過程中會被中斷,目的是使其他線程獲得被執行的機會。

線程調度的細節取決於底層平臺所採用的策略。對於搶佔式調度策略的系統而言,系統會給每個可執行的線程一個小時間段來處理任務;當該時間段用完之後,系統就會剝奪該線程所佔用的資源,讓其他線程獲得執行的機會。在選擇下一個線程時,系統會考慮線程的優先級。

       所有現代的桌面和服務器操作系統都採用搶佔式調度策略,但一些小型設備如手機,則可能採用協調式策略,在這樣的系統中,只有當一個線程調用了它的sleep()或者yield()方法後纔會放棄所佔用的資源——也就是必須由該線程主動放棄所佔用的資源。

      

 

 

 

·控制線程

(·)join線程

Thread提供了讓一個線程等待領一個線程完成的方法——join()方法。當在程序的執行流程中,某個線程A調用另一個線程B的join()方法時,調用線程A將會被阻塞,直到被調用join()方法的線程B執行完畢爲止。

join()方法通常由使用線程的程序調用,用來將大問題劃分成許多小問題,每個小問題分配一個線程。但所有的問題處理完畢後,在調用主線程進一步操作。比如,pp.join()的作用是把pp線程對象所對應的線程合併到“調用pp.join();語句”的主調線程中。

示例代碼

package com.selfpractise;
 
public class ThreadTest extends Thread{
   
    //提供一個有參構造器,用於設置該線程的名字
    public ThreadTest(String name) {
       super(name);
    }
   
    public void run() {     
       for(int i=0; i<100; i++) {
           try {
              Thread.sleep(300);
           } catch (Exception e) {
              e.printStackTrace();
           }         
           System.out.println(Thread.currentThread().getName()+"  "+i);        
       }     
    }
   
    public static void main(String args[]) {
       new ThreadTest("第一個子線程").start();
      
       for(int i=0; i<100; i++) {
           try {
              Thread.sleep(200);
           } catch (Exception e) {
              e.printStackTrace();
           }
           if(i == 20) {
              ThreadTest tt1 = new ThreadTest("被join的爲了處理問題一的線程A");
              tt1.start(); 
              try {
                  //main線程調用了tt1線程的join()方法,
                  //main線程必須等tt1線程執行完畢後纔會向下執行。
                  tt1.join();  
              } catch (Exception e) {
                  e.printStackTrace();
              }            
           }     
          
           if(i == 50) {
              ThreadTest tt2 = new ThreadTest("被join的爲了處理問題二的線程B");
              tt2.start(); 
              try {
                  //main線程調用了tt2線程的join()方法,
                  //main線程必須等tt2線程執行完畢後纔會向下執行。
                  tt2.join();  
              } catch (Exception e) {
                  e.printStackTrace();
              }            
           }  
          
           if(i == 80) {
              ThreadTest tt3 = new ThreadTest("被join的爲了處理問題三的線程C");
              tt3.start(); 
              try {
                  //main線程調用了tt3線程的join()方法,
                  //main線程必須等tt3線程執行完畢後纔會向下執行。
                  tt3.join();  
              } catch (Exception e) {
                  e.printStackTrace();
              }            
           }         
          
           System.out.println(Thread.currentThread().getName()+"   "+i);
       }
    }
}

 


(·)後臺線程

1.何爲後臺線程

       有一種線程,它是在後臺運行的,它的任務是爲其他線程提供服務,這種線程被稱爲“後臺線程(DaemonThread)”,又稱爲“守護線程”或者“精靈線程”。例如,JVM的垃圾回收機制就是典型的後臺線程。

 

2.後臺線程的特徵

後臺線程的特種:如果所有的前臺線程都死亡,後臺線程就會自動死亡。

       並不是所有的線程默認都是前臺線程,有些線程默認就是後臺線程——前臺線程創建的子線程默認是前臺線程;後臺線程創建的子線程默認是後臺線程。

 

3.與後臺線程相關的函數

(1)後臺線程的設定

       ①調用Thread對象的“setDaemon(true)”方法,可以將指定的線程設置成後臺線程。

       ②要將某線程設置爲後臺線程必須在啓動該線程之前。即setDaemon(true)方法必須在start()方發之前。

(2)判斷執行線程是否爲後臺線程:isDaemon()

 

 

(·)線程睡眠

       sleep()方法讓當前線程直接進入到“阻塞狀態”,直到經過一定的阻塞時間,纔會讓當前線程進入到“就緒狀態”。

 

(·)線程讓步yield

1.線程讓步簡述

       yield()方法和sleep()方法都可以讓當前正在執行的線程暫停執行,但yield()方法不會阻塞當前線程,它只是將當前線程轉入到就緒狀態,讓OS的線程調度器從新調度一次。因此完全有可能某個線程調用yield()方法暫停之後,立即再次獲得處理其資源被執行。

 

2.sleep()方法和yield()方法的區別:

從優先級的角度

sleep()方法暫停當前線程後,會給其他線程執行機會,並不理會其他線程的優先級;但yield()方法只會給優先級相同,甚至更高的線程執行機會。

從線程生命週期的角度

sleep()方法讓當前線程直接進入到“阻塞狀態”,直到經過一定的阻塞時間,纔會讓當前線程進入到“就緒狀態”;而yield()方法不會將線程轉入阻塞狀態,它只是將當前線程進入到就緒狀態,因此完全有可能某個線程調用yield()方法暫停之後,立即再次獲得處理其資源被執行。

sleep()方法比yield()方法具有更好的移植性,通常不建議使用yield()方法來控制併發線程的執行。

 

(·)改變線程優先級

       每個線程默認的優先級都與創建它的父線程的優先級相同,在默認的情況下,main線程通常具有普通優先級(NORM_PRIORITY:5),由main線程創建的子線程也具有普通優先級。

 

 

 

 

·多線程的安全問題——同步

(·)發生線程安全問題的上下文情景——提出問題和解決問題

1.提出問題

       多線程併發,也給我們的編程帶來好處,完成更有效率的程序,但是,也會帶來線程安全問題。

       對於經典的“買票程序”來說,極可能碰到這樣一個意外,就是同一張票號可能被多個售票窗口出售,惹禍的兇手代碼就是:

   if(tickets>0){

      System.out.println(Thread.currentThread().getName()+"正在賣"+tickets+"號票。");

      --tickets;

   }

       假如,下載tickets=1,A線程剛執行完語句,還沒開始執行語句時,CPU資源就被B線程搶奪去了,B線程執行“if(tickets>0)”,因爲在此時num還是1,所以B線程將執行語句,在“--tickets”之後,ticket的數量就變成0,而此時CPU資源就被A線程搶奪去了,接着還沒有執行的語句時,由於此時“ticket=0”,在執行完語句後,ticket的數量就變成-1了。

這樣就相當於1號票被賣了兩次,而且買完票後,ticket的數量也不合理(爲負數)。這樣多併發就給我們帶來了線程安全地麻煩。

 

2.解決問題

       解決問題的關鍵就是要辦證容易出現問題的代碼的原子性,所謂原子性就是指:當A線程在執行某段代碼的時候,別的線程必須等到A線程將該段代碼執行完畢後,才能執行該代碼。

       就像人們排隊上廁所一樣,廁所只有一個,得一個一個得解決。

      

       Java處理線程同步的方法非常簡單,只需要在有待同步的代碼段用

synchronized(Object){  你要同步的代碼  }即可。

       就像某位同志上廁所前先把門關上(上鎖),完事後再出來(解鎖),那麼其他人就可以使用廁所了,如下圖所示。


 

 

(·)同步機制的解釋

       Java任意類型的對象都有一個標誌位(或者對象鎖),該標誌位具有0,1兩種狀態,其開始狀態爲1,當某個線程執行了“synchronized(Object)”語句後,Object對象(也叫做同步監視器)的標誌位(或者對象鎖)變爲0的狀態,直到執行完整個synchronized語句中的代碼塊後,該Object對象的標誌位(或者對象鎖)又回到了1狀態。

       當一個線程A執行到了“synchronized(Object)”語句的時候,先檢查Object對象(也叫做同步監視器)的標誌位(或者對象鎖)。如果爲0狀態,則表明已經有其他的線程(假設爲線程X)正在執“synchronized()”,那麼這個線程A將會暫時阻塞,讓出CPU資源,進入到線程池中去等待,直到另外的線程X執行完相關的同步代碼,線程X並將Object對象(也叫做同步監視器)的標誌位變爲1狀態,此時,線程A的阻塞就會被取消,線程A繼續運行,該線程會將Object對象(或同步監視器)的標誌位(或者對象鎖)變爲0狀態,防止其他的線程再次進入到相關的同步代碼塊中。

      

 

(·)同步機制的實現

線程同步的機制:是靠檢查“同步監視器對象”的標誌位(鎖旗標,1可以使用代碼塊,0不可以使用代碼塊)來實現的。使用同一個監視器對象的線程之間才能同步

(屬於使用同一資源的類型,符合“通過繼承Runnable接口來實現多線程”這種情形所能提供的便利。也就是說,想要實現線程的同步,最好是使用“繼承Runnable接口的多線程類”)。

 

同步能夠保證線程安全的前提重要

①必須要要2個或者2個以上的線程,才能進行同步。

②必須是多個線程使用同一個鎖

同步的好處:保證了多線程的安全。

同步的弊端:多個線程的每一個都需要判斷鎖,消耗資源。

 

編寫同步代碼的思路:

①明確哪些代碼是多線程運行代碼。

②明確共享數據。

③明確多線程運行代碼中哪些語句是操作共享數據的。

 

方法一:同步代碼塊

1.語法格式和說明:

synchronized(Object)

你要同步的代碼 

}

       上邊synchronized後括號中的“Object”就是同步監視器,上邊代碼的含義:線程開始執行同步代碼前,必須先獲得對同步監視器的鎖定

       任何一個時刻,只能有一個線程可以獲得同步監視器的鎖定,當同步代碼塊執行完畢後,該線程會釋放對該同步監視器的鎖定。

       通過同步代碼塊的方式,可以保證併發線程在任一時刻只有一個線程可以進入修改共享資源的代碼區(也被稱爲臨界區),所以同一時刻內最多隻有一個線程處於臨界區內,從而保證了線程的安全。

 

2.如何去確定哪些語句需要放在同步代碼塊中(臨界區的確定

       要達到上述目的,只要確定run()方法中的哪些代碼在操作“共享數據”,則那些代碼就要被放在同步代碼塊中。

       這些代碼所在的區域也被稱爲“臨界區”——對共享資源進行操作的代碼片段。  

 

3.同步監視器對象的指定

       雖然Java允許使用任何對象作爲同步監視器,但是,想一下同步監視器對象的作用:阻止兩個線程對同一桐鄉資源進行併發訪問,因此通常推薦使用“可能被可能被併發訪問的共享資源”對象充當同步監視器。

使用線程同步比簡簡單單地使用多線程(沒有同步)的處理速度慢得多,這就是因爲系統要不停的對同步監視器進行檢查,需要額外的開銷,這就說明同步是以犧牲程序的性能爲代價的

 

4.代碼示例

(1)示例代碼1:模擬多線程銀行取款機的實現。

package com.selfpractise;
 
/**
 * 模擬多線程銀行取款機的運行
 */
class BankAccount{
    private StringaccountNO =null;
    private double balance = 0.0f;
   
    public BankAccount(String accountNO,double balance) {
       this.accountNO = accountNO;
       this.balance = balance;
    }
 
    public String getAccountNO() {
       returnaccountNO;
    }
 
    public void setAccountNO(String accountNO) {
       this.accountNO = accountNO;
    }
 
    public double getBalance() {
       returnbalance;
    }
 
    public void setBalance(double balance) {
       this.balance = balance;
    }  
}
 
class ATMWithdrawerextends Thread{
    private BankAccountmyBankAccount =null;
    private double withdrawAmount = 0.0f;
   
    public ATMWithdrawer(String name, BankAccount myBankAccount,double withdrawAmount) {
       super(name);
       this.myBankAccount = myBankAccount;
       this.withdrawAmount = withdrawAmount;
    }
   
    public void run() {
       while(true) {
           //使用myBankAccount作爲同步監視器,任何其他線程進入下邊同步代碼塊之前必須首先獲得myBankAccount賬戶的鎖定,其他線程無法獲得鎖,也就無法修改共享資源對象的數據。
           //這種做法符合:“加鎖--->修改--->釋放鎖”的邏輯。
           synchronized(myBankAccount) {
              //如果銀行賬戶餘額大於取款數額,則可以取款。
              if(this.myBankAccount.getBalance() >=this.withdrawAmount) {
                  //取款成功後,計算賬戶餘額。
                  this.myBankAccount.setBalance(this.myBankAccount.getBalance()-this.withdrawAmount);
                  System.out.println(Thread.currentThread().getName()+",您的"+this.withdrawAmount+"元取款成功,餘額:"+this.myBankAccount.getBalance());
              }
              else {
                  System.out.println("對不起,"+Thread.currentThread().getName()+",當前賬戶餘額爲:¥"+this.myBankAccount.getBalance()+",您的餘額不足。");
                  break;
              }            
              try {
                  Thread.sleep(280);
              } catch (Exception e) {
                  e.printStackTrace();
              }
           }
       }
    }
}
 
public class MoneyWithdrawThreadTest extends Thread{
    public static void main(String[] args) {
       BankAccount myBankAccount = new BankAccount("123456789",1500);
       ATMWithdrawer atm01 = new ATMWithdrawer("張三",myBankAccount,50);
       ATMWithdrawer atm02 = new ATMWithdrawer("李四",myBankAccount,80);
      
       atm01.start();
       atm02.start();
       try {
           atm01.join();
           atm02.join();
       } catch (InterruptedException e) {
           e.printStackTrace();
       }     
      
       System.out.println("經過多人取款,銀行賬戶\""+myBankAccount.getAccountNO()+"\"上的餘額爲"+myBankAccount.getBalance());
    }
}

 

示例代碼2

package test2;
 
public class Demo1 {
   public static void main(String[] args) {
      // TODO Auto-generated method stub
      SaleTickets t = new SaleTickets();
      Thread t1 = new Thread(t);
      t1.start();
      Thread t2 = new Thread(t);
      t2.start();
      Thread t3 = new Thread(t);
      t3.start();
      Thread t4 = new Thread(t);
      t4.start();
   }
}
 
//其作用爲“看門狗”,相當於對象鎖,其中沒有任何代碼
//因爲java中任意類型的對象都可以作爲一個“對象鎖”
class GuardDog{  }
 
class SaleTicketsimplements Runnable{
   int all_tickets =20;
 
   GuardDog my_guard_dog =new GuardDog();
  
   public void run(){
      while(true){
 
         synchronized(my_guard_dog){
            if(this.all_tickets > 0){
               System.out.println(Thread.currentThread().getName()+"線程正在賣第"+this.all_tickets+"張票。");
                --this.all_tickets;
            }else{
                System.out.println("對不起,票已經賣完了。");
                break;
            }
         }
         //在整個synchronized代碼塊執行完畢後,就然線程休息1秒
         try{
            Thread.sleep(1000);
         }catch(Exception e){
            e.printStackTrace();
         }
      }
   }
}

 

示例代碼3.  synchornized同步監視器中的標誌位(0 或者 1)

package com.selfpractise;
/**
 * 功能:模擬鐵路售票系統
 * */
public class Demo1{
    public static void main(String[] args){
       ThreadTest tt = new ThreadTest();
       Thread t1 = new Thread(tt);
       t1.start();
       Thread t2 = new Thread(tt);
       t2.start();
       Thread t3 = new Thread(tt);
       t3.start();
    }
}
 
//創建一個線程對象時,只能創建一個線程,
//想要實現售票系統,則必須創建一個共有的資源對象,
//再讓多個線程共同去使用之一個資源對象、
//可以通過讓一個類ThreadTest實現Runnable接口去實現這個需求
//實現方法:
//首先,創建一個ThreadTest對象(已經繼承了Runnable接口),由於ThreadTest對象中已經有了
//tickets這個屬性,所以就可以作爲即將創建的多個線程的共有的資源
//其次,通過Thread類生成多個線程,這時每一個線程都可以使用tickets這個共同的資源了。
class ThreadTestimplements Runnable{
   
    int tickets = 40;
 
    //注意:要想線程同步,這個String是必寫的,他作爲“監視器或對象鎖的持有者”被放在synchronized後邊
    //而且,這個String對象不能被寫在run()方法裏邊。
    //爲什麼???
    //因爲如果String監視器對象要是被寫在run()方法裏邊,那麼這4個
    //線程在調用run()方法時就都會產生自己的"監視器對象"
    //而每一個監視器對象在各自的線程進入到synchronized語句時,
    //它的鎖旗標將都會是1,這樣就會到底是那個線程正在執行代碼,而其還沒有執行完
    //這樣就不能達到“線程同步”的效果。
    //切記切記切記::如果多個線程要同步,那麼他們使用的監視器對象必須是同一個對象
    String stringObject = new String("");
   
    public void run(){
       while(true){        
           //注意不能將String對象的創建寫在run()方法裏邊
           //String str = new String("");
 
           //(這裏的str是一個對象,也是一個監視器,這個監視器中有一個標誌位)
           //同步代碼塊
synchronized(stringObject){
              if(tickets> 0 ){
                  try {
                     Thread.sleep(300);
                  } catch (InterruptedException e) {
                     e.printStackTrace();
                  }                 System.out.println(Thread.currentThread().getName()+"正在賣"+tickets+"號票。");
                  --tickets;
              }
           }
       }
    }
}
代碼分析:

線程1執行到synchornized代碼塊時,就會將stringObject對象(即同步監視器)的標誌位(或者對象鎖)設置爲0,在運行過程中,如果當線程1的時間片用完後,CPU就會切換到另一個線程2線程2想進入到同一塊代碼塊時,首先會檢查stringObject對象(即同步監視器)的標誌位(或者對象鎖),在讀取到其中的值爲0後,線程2就會停止執行,將CPU資源讓給其他線程,從而處於在線程等待池中處於等待狀態,而其他線程也會遇到相似的情況,這樣最終CPU的資源就會給線程1線程1就會接着執行剛纔沒有完成的任務,執行完任務後,就會將stringObject對象的標誌位(或者對象鎖)設置爲1這樣當CPU切換到下一個線程時,下一個線程確定stringObject對象的標誌位(或者對象鎖),當該標誌位爲1時,該線程就能夠執行代碼塊,在該進程進入到代碼塊之前也會將stringObject對象的標誌位(或者對象鎖)設置爲0這樣就能保證下一個進程的執行結果不被其它線程影響,從而保證了線程的“原子性”。

 

方法二:同步方法

1.什麼是同步方法

       同步方法就是用“synchronized”關鍵字來修飾一個方法,這樣的方法被稱爲同步方法。

對於同步方法而言,無須顯示指定同步監視器,同步方法的同步監視器爲this,也就是該對象本身。

       函數需要被對象所調用,那麼函數都有一個所屬對象的引用,就是this,所以同步函數使用的鎖就是this。

 

2.構造線程安全的類

通過同步方法可以非常方便的實現線程安全的類,線程安全的類具有以下特徵:

①改類的對象可以被多個線程安全的訪問。

②每個線程調用該對象的任意方法之後,都將得到正確的結果。

③每個線程調用該對象的任意方法之後,該對象的狀態依然保持合理的狀態。

       類可以分爲“可變類”和“不可變類”,其中“不可變類”總是線程安全的,因爲它的對象狀態不可改變;但可變類需要額外的方法來保證其線程安全。

      

       可變類的線程安全是以降低程序性能爲代價的,爲了減少線程安全所帶來的負面影響,程序可採用如下策略:

(1)不要對線程安全類的所有方法都進行同步,只對那些會改變共享資源的方法進行同步。例如,銀行賬戶BankAccount類中的賬號屬性accountNO就無須同步,只對取款方法進行同步。

(2)如果可變類有兩種運行環境:單線程環境和多線程環境,則應該爲可變類提供兩種版本,即線程不安全版本和線程安全版本。在單線程環境中,可以使用線程不安全版本以保證性能,在多線程環境中使用線程安全版本。

JDK所提供的StringBuilder,StringBuffer就是爲了照顧單線程環境和多線程環境所提供的類。在單線程環境下,應該使用StringBuilder來保證性能;在多線程環境下,應該使用StringBuffer來保證線程安全。

 

3.靜態函數的鎖是Class對象。

如果同步函數被靜態static修飾符修飾後,則此方法的同步鎖就是class對象。應爲靜態方法中不可能定義this。

靜態進入內存時,內存中沒有本類的實例對象,但內存中一定有該類對應的字節碼文件對象,即“類名.class”,該對象的類型是Class。

【參考資料】

畢向東Java基礎視頻 第11天-13-多線程(多線程-靜態同步函數的鎖是Class對象)

 

4.代碼示例

(1)代碼示例1:模擬多線程一行取款機的運行。

package com.selfpractise;
 
/**
 * 模擬多線程銀行取款機的運行
 * 利用同步方法的機制,提供一個線程安全的BankAccount類,該類有自己的取款方法,以保證線程安全。
 */
class BankAccount{
    private StringaccountNO =null;
    private double balance = 0.0f;
   
    public BankAccount(String accountNO,double balance) {
       this.accountNO = accountNO;
       this.balance = balance;
    }
 
    public String getAccountNO() {
       returnaccountNO;
    }
 
    public double getBalance() {
       returnbalance;
    }
   
    //提供一個線程安全地withdrawMoney方法來完成取錢操作。
    public synchronized void withdrawMoney(double withdrawAmount) {
       if(this.balance >= withdrawAmount) {
           this.balance =this.balance - withdrawAmount;
           System.out.println(Thread.currentThread().getName()+"取款"+withdrawAmount+"元成功,您的賬餘額:¥"+this.balance);
       }else {
           System.out.println(Thread.currentThread().getName()+"您的賬戶餘額不足。");
       }
       try {
           Thread.sleep(130);
       } catch (Exception e) {
           e.printStackTrace();
       }
    }
}
 
class ATMWithdrawerextends Thread{
    private BankAccountmyBankAccount =null;
    private double withdrawAmount = 0.0f;
   
    public ATMWithdrawer(String name, BankAccount myBankAccount,double withdrawAmount) {
       super(name);
       this.myBankAccount = myBankAccount;
       this.withdrawAmount = withdrawAmount;
    }
   
    public void run() {
       //若銀行賬戶的餘額 >取款數目,則一直讓線程執行取款操作。
       while(this.myBankAccount.getBalance() >=withdrawAmount) {
           this.myBankAccount.withdrawMoney(this.withdrawAmount);
       }
    }
}
 
public class MoneyWithdrawThreadTest extends Thread{
    public static void main(String[] args) { 
       BankAccount myBankAccount = new BankAccount("123456789", 1000);
       ATMWithdrawer atm01 = new ATMWithdrawer("王五", myBankAccount, 50);
       ATMWithdrawer atm02 = new ATMWithdrawer("陳六", myBankAccount, 30);
      
       atm01.start();
       atm02.start();
       try {
           atm01.join();
           atm02.join();
       } catch (Exception e) {
           e.printStackTrace();
       }     
       System.out.println("經過多人取款,銀行賬戶"+myBankAccount.getAccountNO()+"上的餘額爲"+myBankAccount.getBalance());
    }
}

 

(2)代碼示例2

class ThreadTestimplementsRunnable{
   int tickets = 40; 
   public void run(){
      while(true){
         this.saleTickets();
      }
   }
   public synchronized void saleTickets(){
      if(this.tickets > 0){
         System.out.println(Thread.currentThread().getName()+"正在賣"+tickets+"號票。");
         --tickets;
      }
   }
}
 
public class Demo1{
   public static void main(String[] args){
      ThreadTest tt = new ThreadTest();
      Thread t1 = new Thread(tt);
      t1.start();
      Thread t2 = new Thread(tt);
      t2.start();
      Thread t3 = new Thread(tt);
      t3.start();
   }
}

代碼分析:

1.當一個線程進入到了標有synchronized關鍵字的方法時,他就得到了監視器,並且鎖定了監視器,只有這個方法執行完後,其他的線程纔可以執行這個方法。

2.代碼塊和函數之間的同步:

根據線程同步的機制 {是靠檢查“監視器對象”的標誌位(鎖旗標,1可以使用代碼塊,0不可以使用代碼塊)來實現的},只要讓代碼塊和函數使用同一個“監視器對象”,就可以實現代碼塊和函數之間的同步。

 

 

(·)釋放同步監視器的鎖定

1.線程釋放同步監視器鎖定的情況

2.線程不會釋放同步監視器鎖定的情況

       ①線程在執行同步代碼塊或同步方法時,程序調用Thread.sleep()方法,Thread.yield()方法來暫停當前線程的執行,當前線程不會釋放同步監視器的鎖定。

②線程在執行同步代碼塊,其他線程執行了該線程的suspend()方法來將該線程掛起,該線程不會釋放同步監視器的鎖定。當然,我們應當儘量避免使用suspend()和resume()方法來控制線程。

 


 

(·)線程同步和單例設計模式的結合(重要)

1.餓漢式

(1)餓漢式的代碼寫法

//單例模式————"餓漢式"的代碼規範
class SingleHungry{
 //此處有關鍵字final修飾aSingleInstanceObjRef,則表明次對象不可修改,
 //雖然不要final也可以實現效果,但這樣做更加規範。
 private static final SingleHungry aSingleInstanceObjRef = new Single();
 private SingleHungry() {}
 public SingleHungry getSingle() {
  return aSingleInstanceObjRef;
 }
}
(2)餓漢式的特點:線程安全。

 

2.懶漢式

(1)懶漢式的“線程不安全”的寫法

//單例模式————"懶漢式"的代碼
//下列代碼是“線程不安全”的寫法。
class SingleLanHan{
 //此處不能有關鍵字final,否則aSingleInstanceObjRef引用就無法被複制了,
 //因爲它被定爲以爲一個常量null
 private static SingleLanHan aSingleInstanceObjRef = null;
 private SingleLanHan() {}
 public SingleLanHan getSingleLanHan(){
  if(aSingleInstanceObjRef == null) {
   aSingleInstanceObjRef = new SingleLanHan();
   return aSingleInstanceObjRef;
  }
 }
}
上述“懶漢式”的單例模式代碼存在“線程不安全”的隱患。

    

(2)懶漢式的“線程不安全”的原因

public SingleLanHan getSingleLanHan(){
  if(aSingleInstanceObjRef == null) {
    //--->此時線程A進入該代碼處①
    //--->此時線程B進入該代碼處②
    //--->此時線程C進入該代碼處③
   aSingleInstanceObjRef = new SingleLanHan();
   return aSingleInstanceObjRef;
  }
 } 

代碼存在隱患的原因:若線程A在執行到時,線程A還沒有創建單例對象,CPU就被切換到線程B,並且當線程B執行到代碼時,CPU又被切換到線程C,如此,就會導致程序創建多個SingleLanHan對象,這個就不符合單例模式的要求。


(3)懶漢式能夠保障“線程安全”的正確的代碼寫法

3.1) 效率比較的寫法

//單例模式————"懶漢式"的代碼
//下列代碼是“線程不安全”的寫法。
class SingleLanHan{
 //此處不能有關鍵字final,否則aSingleInstanceObjRef引用就無法被複制了,
 //因爲它被定爲以爲一個常量null
 private static SingleLanHan aSingleInstanceObjRef = null;
 private SingleLanHan() {}
 public synchronized SingleLanHan getSingleLanHan(){
  if(aSingleInstanceObjRef == null) {
   aSingleInstanceObjRef = new SingleLanHan();
   return aSingleInstanceObjRef;
  }
 }
}
效率低的原因分析:

當每一個線程訪問SingleLanHan()方法時,都需要判斷是否有其他的線程在佔用着鎖,因此效率比較低。

 

3.2) 效率比較的寫法

//單例模式————"懶漢式"的代碼規範
class SingleLanHan{
 //此處不能有關鍵字final,否則aSingleInstanceObjRef引用就無法被複制了,
 //因爲它被定爲以爲一個常量null
 private static SingleLanHan aSingleInstanceObjRef = null;
 private SingleLanHan() {}
 public SingleLanHan getSingleLanHan(){
    //--->此時線程C進入該代碼處③
  if(aSingleInstanceObjRef == null) {
    //--->此時線程B進入該代碼處②
   synchronized(SingleLanHan.class) {
    //--->此時線程A進入該代碼處①
    if(aSingleInstanceObjRef == null) {
     aSingleInstanceObjRef = new SingleLanHan();
     return aSingleInstanceObjRef;
    }
   }
  }else {
   return aSingleInstanceObjRef;
  }
 }
}
效率高的原因分析:減少多個線程判斷鎖的次數。

  線程A在執行到時,先拿到了鎖,此時CPU切換給線程B,但由於此時線程A沒有釋放鎖,因此線程B不能進入同步代碼塊。

  之後CPU切換給線程A線程A成功的創建了單例對象,並且釋放鎖,之後線程B通過判斷所鎖的瑣旗標拿到了鎖,但此時“aSingleInstanceObjRef不爲 null”,因此就不能再次創建單例對象。

  若此時線程C進入到代碼處,則會對最外層的代碼“if(aSingleInstanceObjRef == null)”擋在外邊,因此,線程C就不用判斷鎖的瑣旗標了。

  同理,若有其他的線程D,線程E,線程F..........等等多個線程,這些線程都不用判斷鎖的瑣旗標,這樣就減少多個線程判斷鎖的次數,從而提高了程序的效率。

  總而言之,用雙重判斷的方式來減少多個線程判斷鎖的次數,從而提高了效率。

 

(·)死鎖

       當兩個對象相互等待對方釋放同步監視器時,就會發生死鎖,Java虛擬機沒有監測,也沒有採取措施來處理死鎖。所以多線程編程時應採取措施避免死鎖。一旦出現死鎖,整個程序既不會發生任何異常,也不會有任何提示,只是所有線程都處於阻塞狀態,無法繼續。

 

·線程間通信

一.引子1


       不同的線程之間對CPU資源是搶佔式的。

       在“賣火車票”的實驗中,同一個線程thread-2有可能打印多次,這也就說明了,線程thread-2被連續執行了多次,這是因爲線程thread-2多次成功搶佔CPU資源。

 

重要

雖然線程的同步可以保證每個線程在執行時的“原子性”,但如何保證線程A已經寫入的而且還沒來得及被輸出的數據會由於同一個線程A的再次執行而不被覆蓋呢(同一個線程A之所以會再次執行,可能是因爲此時線程A的時間片還沒有用完)??

這就涉及到了“線程間通信”

引子2

package com.selfpractise;
/**
 * 演示可能出現的錯誤:
 * 1.同一個線程有可能搶佔資源,也就是同一個線程在執行完一次代碼後,
 *   再次搶佔CPU資源,再次執行代碼。
 * */
class Buffer{
    String name;
    String gender;
}
 
class Producerimplements Runnable{
    Buffer buffer = null;   
   
    public Producer(Buffer b){
       this.buffer = b;
    }  
    public void run() {
       int i=0;
       while(true){
          
           //如果多個線程共享同一個對象資源,那麼這個對象就可以作爲“監視器對象”
           //在本例中,Producer和Customer多線程類都可以共同訪問buffer對象
           //因此buffer對象是Producer和Customer多線程類的“監視器對象”
           synchronized(buffer){
              if(i == 0){
                  buffer.name ="蘇林";
                  System.out.println("生產者已經將name="+buffer.name+"放入緩衝區中了。");
                  buffer.gender ="男";
                  System.out.println("生產者已經將gender="+buffer.gender+"放入緩衝區中了。");
              }
              else{
                  buffer.name ="廠長";
                  System.out.println("生產者已經將name="+buffer.name+"放入緩衝區中了。");
                  buffer.gender ="女";
                  System.out.println("生產者已經將gender="+buffer.gender+"放入緩衝區中了。");
              }
              i = ((++i)%2);
           }         
       }
    }
}
 
class Customerimplements Runnable{   
    Buffer buffer = null;   
    public Customer(Buffer b){
       this.buffer = b;
    }
   
    public void run(){
       while(true){
           synchronized(buffer){
              System.out.print("在消費者中取數據:"+buffer.name+"   ");
              System.out.println(buffer.gender);
           }
       }
    }
}
 
public class InternalThreadCommunication {
    public static void main(String[] args) {
       Buffer buffer = new Buffer();
       Producer p = new Producer(buffer);
       Customer c = new Customer(buffer);
      
       Thread t_p = new Thread(p);
       t_p.start();
      
       Thread t_c = new Thread(c);
       t_c.start();
    }
}

1.預期的理想狀態:

生產者線程產生一組新的數據1後就被消費者線程讀取,生產者線程產生一組新的數據2後就被消費者線程讀取…………生產者線程產生一組新的數據n後就被消費者線程讀取。

2.但上述程序的實際結果顯示:

生產者線程產生一組新的數據1後,由於他的時間片還沒有用完,CPU就無法切換到消費者線程,去讀取生產者線程剛剛已經產生的並且已經存儲到緩衝區中的數據1,這樣生產者線程就會再次生產數據2,並將原來原本應該被消費者線程讀取的數據1給覆蓋掉了。如果此時生產者線程的時間片還沒有被用完的話,那麼它就會繼續產生數據3數據4數據5……數據n

       而當生產者線程的時間片用完後,CPU切換到消費者線程去讀取已經產生的數據,就有可能會出現下述情況:消費者線程其實是想讀取數據1的,但由於生產者線程的時間片一直都沒有用完,CPU就無法切換到消費者線程,從而導致數據1被後來的數據2數據3數據4……數據n等所覆蓋,這樣消費者線程就無法得到它真正想要的數據。

 

解決方法:線程間通信。

 

重要

雖然線程的同步可以保證每個線程在執行時的“原子性”,但如何保證線程A已經寫入的而且還沒來得及被輸出的數據會由於同一個線程A的再次執行而不被覆蓋呢(同一個線程A之所以會再次執行,可能是因爲此時線程A的時間片還沒有用完)??

這就涉及到了“線程間通信”

 

 

二.線程的等待和喚醒過程

 

示例代碼:

/**
 * 功能:線程間通信機制的演示
 * 不同點:將存儲數據的成員變量和操縱數據的方法(寫入和輸出)
 *         都封裝到一個類中去,這樣是利用面向對象的思想,
 *         實現了代碼的簡介,美觀,以及安全。
 * */
package test2;
 
public class InterThreadCommunicationUpgratedVersion {
 
   public static void main(String[] args) {
     
      Buffer buffer = new Buffer();
      Producer p = new Producer(buffer);
      Customer c = new Customer(buffer);
     
      Thread t_p = new Thread(p);
      t_p.start();
      Thread t_c = new Thread(c);
      t_c.start();
   }
}
 
class Buffer{
   private Stringname;
   private Stringgender;
   private boolean isFull = false;//判斷存儲緩衝區的緩衝區是否爲滿
  
   //寫入信息的方法
   public synchronized void setInfo(String name, String gender){
      if(this.isFull ==false){
         this.name = name;
         this.gender = gender;
         System.out.println("生產者線程產生信息————姓名:"+this.name+" 性別:"+this.gender);
        
         this.isFull =true;
         this.notify();
        
      }else if(this.isFull ==true){
         try {
            this.wait();
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }
   }
  
   //讀取信息的方法
   public synchronized void getInfo(){
      if(this.isFull ==false){
         try {
            this.wait();
         } catch (InterruptedException e) {
            e.printStackTrace();
         }
      }else if(this.isFull ==true){
         System.out.println("消費者線程獲取信息————姓名:"+this.name+" 性別:"+this.gender);
        
         this.isFull =false;
         this.notify();
      }
   }
}
 
 
class Producerimplements Runnable{
  
   Buffer buffer = null;
  
   public Producer(Buffer buffer){
      this.buffer = buffer;
   }
  
   public void run(){
      int i=0;
      while(true){
         if(i == 0){
           
            buffer.setInfo("張健","男");
         }else{
            buffer.setInfo("陳陽","女");
         }
         i = ((++i)%2);
      }
   }
}
 
class Customerimplements Runnable{
  
   Buffer buffer = null;
  
   public Customer(Buffer buffer){
      this.buffer = buffer;
   }
  
   public void run(){
      while(true){
         buffer.getInfo();
      }
   }
}

 

·畢向東主講線程間通信

(·)思考

思考1:wait(),notify(),notifyAll()這些用來操作線程的方法爲什麼定義在了Object類中?

 

 

思考2:調用什麼對象的wait(),notify(),notifyAll()的方法來實現線程通訊?

       都是調用同步監視器(鎖)的wait(),notify()方法,因爲要對同步監視器(鎖)的線程進行操作。等待的線程都會被存放同步監視器的在“線程池”中。

等待喚醒機制
wait():讓線程等待。將線程存儲到一個線程池中。
notify():喚醒被等待的線程。通常都喚醒線程池中的第一個。讓被喚醒的線程處於臨時阻塞狀態。
notifyAll(): 喚醒所有的等待線程。將線程池中的所有線程都喚醒,讓它們從凍結狀體轉到臨時阻塞狀態.
這三個方法用於操作線程,可是定義在了Object類中,爲什麼呢?
因爲,這三個方法在使用時,都需要定義在同步中,要明確這些方法所操作的線程所屬於鎖。
簡單說。在A鎖被wait的線程,只能被A鎖的notify方法喚醒。
所以必須要表示waitnotify方法所屬的鎖對象,而鎖對象可以是任意的對象。
可以被任意的對象調用的方法肯定定義在Object類中。
注意:等待喚醒機制,通常都用在同步中,因爲需要鎖的支持。
而且必須要明確waitnotify 所作用的鎖對象。

 

思考3:Wait()和sleep()方法的區別

wait()釋放資源,釋放鎖。

sleep()釋放資源,不釋放鎖。

 




發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章