天天看點

Java精選筆記_多線程(建立、生命周期及狀态轉換、排程、同步、通信)線程概述線程的建立線程的生命周期及狀态轉換線程的方法線程的排程多線程同步多線程通訊線程池

線程概述

在應用程式中,不同的程式塊是可以同時運作的,這種多個程式塊同時運作的現象被稱作并發執行。

多線程可以使程式在同一時間内完成很多操作。

多線程就是指一個應用程式中有多條并發執行的線索,每條線索都被稱作一個線程

程序Process

在一個作業系統中,每個獨立執行的程式都可稱之為一個程序,也就是“正在運作的程式”。

線程Thread

一個程式至少有一個程序,一個程序至少有一個線程。程序在執行過程中擁有獨立的記憶體單元

在java中一個正在執行的程式。程式運作中至少有兩個線程在運作,一個是主函數的主線程,另一個是垃圾回收的線程。

讓程式運作的更為順暢,達到了多任務處理的目的。

優缺點

提升性能改善使用者體驗,對系統運作的其他程序/線程不友好。

線程的建立

繼承Thread類建立多線程

繼承Thread類.并重寫Thread類中的run()方法,調用線程的start方法,啟動新線程,線程啟動後,系統會自動調用run()方法,如果子類重寫了該方法便會執行子類中的方法。

作用

1.啟動新線程 start()方法

2.運作run方法。目的是将自定義的代碼存儲在run方法中,讓線程運作

cpu每次隻執行一個程式,隻是在快速的不同線程間切換,表現了多線程的随機性

Thread類的方法

對象方法

start()-啟動線程

setPriority(int)/getPriority()-設定/擷取優先級

setDaemon(boolean) - 設定守護線程(背景線程)

interrupt() - 中斷線程

join() - 等待該線程結束

靜态方法

sleep(long) - 線程休眠

yield() - 讓出CPU

currentThread() - 獲得正在執行的線程

class demo extends Thread {

  public void run() {

  }

}

run方法用于存儲線程要運作的代碼。

demo demo=new demo();  建立對象就建立了一個線程。

run方法和 start方法

run方法  僅僅是對象調用方法,并沒有運作線程

start方法   開啟線程并且執行線程中的run方法

實作Runnable接口建立多線程

Thread類提供了另外一個構造方法Thread(Runnable target),其中Runnable是一個接口,它隻有一個run()方法。

步驟

1,定義類實作Runnable接口。

2,覆寫接口中的run方法(用于封裝線程要運作的代碼)。

3,通過Thread類建立線程對象;

4,将實作了Runnable接口的子類對象作為實際參數傳遞給Thread類中的構造函數。

    為什麼要傳遞呢?因為要讓線程對象明确要運作的run方法所屬的對象。

5,調用Thread對象的start方法。開啟線程,并運作Runnable接口子類中的run方法。

  Ticket t = new Ticket();

  直接建立Ticket對象,并不是建立線程對象。

  因為建立對象隻能通過new Thread類,或者new Thread類的子類才可以。是以最終想要建立線程。既然沒有了         Thread類的子類,就隻能用Thread類。

  Thread t1 = new Thread(t); //建立線程。

  隻要将t作為Thread類的構造函數的實際參數傳入即可完成線程對象和t之間的關聯

  為什麼要将t傳給Thread類的構造函數呢?其實就是為了明确線程要運作的代碼run方法。

實作Callable<T>接口重寫call方法

Future<T>

可以線上程執行技術之後得到一個結果

兩種實作多線程方式的對比分析

繼承Thread類:線程代碼塊存放在Thread子類的run方法中

實作Runnable,線程代碼存放在接口的子類的run方法中,可以被多實作。

繼承方式有局限性。要被實作多線程的一個類 如果繼承了父類 就不能再繼承Thread類。

實作方式就改變了單繼承的局限性。

适合多個相同程式代碼的線程去處理同一個資源的情況,把線程同程式代碼、資料有效地分離,很好的展現出面向對象的程式設計思想

大多數的應用程式都會采用實作Runnable的方式實作多線程的建立

線程運作狀态

建立:start()

運作:具備執行資格,同時具備執行權;

當機:sleep(time),wait()—notify()喚醒; 線程釋放了執行權,同時釋放執行資格;

臨時阻塞狀态:線程具備cpu的執行資格,沒有cpu的執行權;

消亡:stop() run方法結束

背景線程

隻要有一個前台線程在運作,這個程序就不會結束。如果一個程序中隻有背景線程在運作,這個程序就會結束。

新建立的線程預設都是前台線程,如果某個線程對象在啟動之前調用了setDaemon(true)語句,這個線程就變成一個背景線程。

線程的生命周期及狀态轉換

線程整個生命周期可以分為五個階段,分别是建立狀态(New)、就緒狀态(Runnable)、運作狀态(Running)、阻塞狀态(Blocked)和死亡狀态(Terminated),線程的不同狀态表明了線程目前正在進行的活動。

1、建立狀态(New)

建立一個線程對象後且在調用start方法之前,該線程對象就處于建立狀态,此時它不能運作,和其它Java對象一樣,僅僅由Java虛拟機為其配置設定了記憶體,沒有表現出任何線程的動态特征。

2、就緒狀态(Runnable)

當線程對象調用了start()方法後,該線程就進入就緒狀态(也稱可運作狀态)。處于就緒狀态的線程位于可運作池中,此時它隻是具備了運作的條件(具備了除CPU之外所有運作所需資源),能否獲得CPU的使用權開始運作,還需要等待系統的排程。

3、運作狀态(Running)

如果處于就緒狀态的線程獲得了CPU的使用權,開始執行run()方法中的線程執行體,則該線程處于運作狀态。當一個線程啟動後,它不可能一直處于運作狀态(除非它的線程執行體足夠短,瞬間就結束了),當使用完系統配置設定的時間後,系統就會剝奪該線程占用的CPU資源,讓其它線程獲得執行的機會。需要注意的是,隻有處于就緒狀态的線程才可能轉換到運作狀态。

4、阻塞/等待狀态(Blocked)

一個正在執行的線程在某些特殊情況下,如執行耗時的輸入/輸出操作時,會放棄CPU的使用權,進入阻塞狀态。線程進入阻塞狀态後,就不能進入排隊隊列。隻有當引起阻塞的原因被消除後,線程才可以轉入就緒狀态。

當線程試圖擷取某個對象的同步鎖時,如果該鎖被其它線程所持有,則目前線程會進入阻塞狀态,如果想從阻塞狀态進入就緒狀态必須得擷取到其它線程所持有的鎖。

當線程調用了一個阻塞式的IO方法時,該線程就會進入阻塞狀态,如果想進入就緒狀态就必須要等到這個阻塞的IO方法傳回。

當線程調用了某個對象的wait()方法時,也會使線程進入阻塞狀态,如果想進入就緒狀态就需要使用notify()方法喚醒該線程。

當線程調用了Thread的sleep(long millis)方法時,也會使線程進入阻塞狀态,在這種情況下,隻需等到線程睡眠的時間到了以後,線程就會自動進入就緒狀态。

當在一個線程中調用了另一個線程的join()方法時,會使目前線程進入阻塞狀态,在這種情況下,需要等到新加入的線程運作結束後才會結束阻塞狀态,進入就緒狀态。

5、死亡狀态(Terminated)

線程的run()方法正常執行完畢或者線程抛出一個未捕獲的異常(Exception)、錯誤(Error),線程就進入死亡狀态。一旦進入死亡狀态,線程将不再擁有運作的資格,也不能再轉換到其它狀态,線程的整個生命周期結束。

線程的方法

擷取線程名稱和對象

線程都有自己預設的名稱:

擷取線程名稱的方法:Thread.currentThread().getName()

currentThread() 擷取目前線程對象

getName()   擷取線程名稱 

設定線程名稱 SetName(); 構造方法傳參

public final String getName() 傳回線程的名稱

public final void setName() 設定線程名稱

public final void join()throws InterruptedException 等待該線程終止

public final native boolean isAlive() 判斷線程是否在活動,如果是傳回true,否則傳回false

public final void join(long millis)

throws InterruptedException 等待該線程終止,并指定等待的時間。其中millis的機關是毫秒

public final void setPriority(int newPriority) 修改線程的優先級

public final int getPriority() 取得線程的優先級

public static void sleep(long millis) throws InterruptedException 暫停執行目前正在執行的線程并讓該線程休眠一段時間。其中millis表示休眠的時間,其機關為毫秒

public static void yield() 暫停執行目前的線程對象,并執行其他線程

public static boolean interrupted() 中斷線程,可以通過使用類名直接調用

線程的排程

線程在整個生命周期中始終處于某種狀态,從一種狀态到另一種狀态的轉換由線程排程方法實作

線程的優先級

SetPriority(1-10)設定優先級。

Thread.MAX_PRIORITY 10

Thread.MIN_PRIORITY 1

Thread.NORM_PRIORITY 5

程式在運作期間,處于就緒狀态的每個線程都有自己的優先級,例如main線程具有普通優先級。

線程優先級不是固定不變的,可以通過Thread類的public final void setPriority(int newPriority);方法對其進行設定,該方法中的參數newPriority接收的是1~10之間的整數或者Thread類的三個靜态常量。

線程休眠/睡眠

如果希望人為地控制線程,使正在執行的線程暫停,将CPU讓給别的線程,這時可以使用靜态方法public static void sleep(long millis)throws InterruptedException;,該方法可以讓目前正在執行的線程暫停一段時間,進入休眠等待狀态。

目前線程調用sleep(long millis)方法後,在指定時間(參數millis)内該線程是不會執行的,這樣其它的線程就可以得到執行的機會了。

sleep()方法是一個靜态的方法,是以sleep()方法不是依賴于某一個對象的,位置比較随意,當線上程中執行sleep()方法,則線程就進入睡眠狀态。sleep()方法是可能發生捕獲異常的,是以在使用sleep()方法時必須進行異常處理。

線程讓步

線程讓步可以通過yield()方法來實作,該方法和sleep()方法有點相似,都可以讓目前正在運作的線程暫停,差別在于yield()方法不會阻塞該線程,它隻是将線程轉換成就緒狀态,讓系統的排程器重新排程一次。當某個線程調用yield()方法之後,隻有與目前線程優先級相同或者更高的線程才能獲得執行的機會。

public static void yield();沒有聲明抛出任何異常

線程插隊

當在某個線程中調用其它線程的join()方法時,調用的線程将被阻塞,直到被join()方法加入的線程執行完成後它才會繼續運作。

停止線程

run方法結束,就會停止線程,開啟多線程運作,運作代碼通常是循環結構。隻要控制住循環,就可以讓線程結束。

方法:改變标記。

特殊情況,改變标記也不會停止的情況。

将處于當機狀态的線程恢複到運作狀态。interrupt();  中斷線程。

守護線程

SetDaemon将線程标記為守護線程或使用者線程。在啟動線程前調用 。當線程都為守護線程後,JVM退出。

Join方法:

t.join();搶過cpu執行權。

當A線程執行到了B線程的join方法時,A就會等待,等B線程執行完,A才會執行。Join可以用來臨時加入線程執行。

yield方法:暫停目前正在執行的線程對象,并執行其他線程。

new Thread() {
      for(int x=0;x<100;x++) {
         sop(Thread.currentThread().getName())
       }
}.start();
   for(int x=0;x<100;x++) {
       sop(Thread.currentThread().getName())
}
Runnable r=new Runnable() {
   public voud run() {
     for(int x=0;x<100;x++) {
        sop(Thread.currentThread().getName())
     }
   }
};
new Thread(r).start();
           

多線程同步

線程安全

安全産生的原因:當多條語句在操作同一個共享資料時,一個線程對多條語句隻執行了一部分,還沒有執行完,

另一個線程參與進來執行。導緻共享資料的錯誤。

解決辦法:對多條操作共享資料的語句,隻能讓一個線程都執行完。在執行過程中,其他線程不可以參與執行。

什麼時候需要同步

存在臨界資源(多個線程通路一個對象),對資源的通路是互斥的(排他的)

同步代碼塊

當多個線程使用同一個共享資源時,可以将處理共享資源的代碼放置在一個代碼塊中,使用synchronized關鍵字來修飾,被稱作同步代碼塊。

    synchronized(lock對象) {  火車衛生間案例

        需要被同步的代碼。(共享資料)

}

lock是一個鎖對象,它是同步代碼塊的關鍵。當線程執行同步代碼塊時,首先會檢查鎖對象的标志位,預設情況下标志位為1,此時線程會執行同步代碼塊,同時将鎖對象的标志位置為0。當一個新的線程執行到這段同步代碼塊時,由于鎖對象的标志位為0,新線程會發生阻塞,等待目前線程執行完同步代碼塊後,鎖對象的标志位被置為1,新線程才能進入同步代碼塊執行其中的代碼。循環往複,直到共享資源被處理完為止。

對象如同鎖,持有鎖的線程可以在同步中執行,沒有持有鎖的線程,即使擷取cpu的執行權,也進不去,因為沒有擷取鎖。

同步代碼塊可以有效解決線程的安全問題,當把共享資源的操作放在synchronized定義的區域内時,便為這些操作加了同步鎖。

同步的前提

1.必須要有兩個或者兩個以上的線程

2.必須多個線程必須使用同一個鎖。

  必須保證同步中隻能有一個線程在運作。

好處:解決了線程的安全問題

弊端:消耗了資源,多個線程需要判斷鎖。

同步方法

public synchronized void show() { }

被synchronized修飾的方法在某一時刻隻允許一個線程通路,通路該方法的其它線程都會發生阻塞,直到目前線程通路完畢後,其它線程才有機會執行方法。

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

想讓線程停一下  Thread.sleep(10);

如果同步函數被靜态修飾後,使用的鎖是 classsynchronized (對象名.class)

1.明确哪些代碼是多線程運作代碼

2.明确共享資料

3.明确多線程運作代碼中哪些語句是操作共享資料的

Synchronized不會被繼承,如果一個類聲明一個方法為synchronized,子類繼承該類,這個方法被繼承後則不再保持同步,除非子類中的這個方法也使用關鍵字synchronized修飾。

死鎖問題

是由于兩個線程互相等待對方已被鎖定的資源

循環等待條件:第一個線程等待其它線程,後者又在等待第一個線程。

同步中嵌套同步,可能會發生 避免死鎖:當幾個線程都要通路共享資源A、B、C時,保證使每個線程都按照同樣的順序去通路它們,比如都先通路A,在通路B和C。要避免死鎖,應該確定在擷取多個鎖時,在所有的線程中都以相同的順序擷取鎖。

多線程通訊

多個線程在操作同一個資源,但是操作的動作不同。

1.是不是兩個或兩個以上的線程。解決辦法 兩個線程都要被同步。

2.是不是同一個鎖。解決辦法 找同一個對象作為鎖。

等待喚醒機制

wait後,線程就會存線上程池中,notify後就會将線程池中的線程喚醒。

notifyAll();喚醒線程池中所有的線程。

實作方法 :

給資源加個标記 flag   

synchronized(r) {

    while(r.flag) //多個生産者和消費者   if(r.flag) //一個生産者和消費者

    r.wait();

     代碼

    r.flag=true;

    r.notify();

    r.notifyAll();

}

上面三種方法都使用在同步中,因為要對持有螢幕(鎖)的線程操作。是以要使用在同步中,因為隻有同步才具有鎖。

為什麼這些操作線程的方法要定義在object類中呢?

因為這些方法在操作同步中線程的時候,都必須要表示它們所操作線程隻有的鎖。隻有同一個鎖上的被等待線程,可以被同一個鎖上的notify喚醒,不可以對不同鎖中的線程進行喚醒。

也就是說,等待和喚醒必須是同一個鎖,而鎖可以是特意對象,可以被任意對象調用的方法定義在Object類中。

Lock接口 

Lock 替代了synchronized 

Condition 替代了 Object螢幕方法

好處

将同步synchronized 替換成了 Lock

将object中的wait notify notifyAll 替換成了 Condition對象

該對象可以Lock鎖進行擷取。一個鎖可以對應多個Condition對象

注意:釋放鎖的工作一定要執行

private Lock lock=new ReentrantLock();
private Condition condition =lock.newCondition();
public void cet(String name ) throws {
   lock.lock();
   try {
      while(flag)
           contidition.await();
       this.name=name+"--"+count++;
       sop(Thread.currentThread().getName()+"...生産者..."+this.name)
       flag=true;
       condition.signalAll();
    }
    finally {
       lock.unlock(); 
   }
}
           

final void wait() throws InterruptedException方法

通知目前的線程進入睡眠直到其他線程進入調用notify()喚醒它。在睡眠之前,線程會釋放掉所占有的“鎖标志”,則其占用的所有synchronized代碼塊可被别的線程使用。

final void notify()方法

喚醒在該同步代碼塊中第一個調用wait()的線程(即第一個進入休眠狀态的線程),并且這時會從線程等待池轉移到鎖标志等待池中,因為該線程沒有立刻擷取鎖标志。

final void notifyAll()方法

喚醒在該同步代碼塊中所有調用wait()的線程,并且會從線程等待池中所有該對象的所有線程轉移至鎖标志等待池中,因為這些線程沒有擷取鎖标志。

基于線程排程的通信

Java 5以前

wait() - 線程暫停/等待

notify() / notifyAll() - 喚醒等待的線程

這些方法在根類Object中是用final關鍵字聲明的,是以所有的類都包含這些方法

這3個方法僅在synchronized修飾塊中才能被調用。

Java 5+

Condition

await() - 線程等待

signal() / signalAll() - 喚醒等待的線程

線程池

用空間換時間,事先建立多個線程放入池中,節省建立和銷毀線程在時間上的開銷

建立線程池

Executors工具類

newFixedThreadPool(int)推薦在伺服器程式設計時使用

newCachedThreadPool

ExecutorService

execute(Runnable) - 啟動線程配置設定任務

submit(Callable<T>) - 啟動線程配置設定任務 Future<T>在将來的某個時間獲得線程的執行結果

shutdown() - 等待線程執行完畢關閉線程池

shutdownNow() - 立即關閉線程池傳回等待執行的線程

isTerminated() - 是否執行完所有線程

繼續閱讀