多線程一(線程創建和使用)


一. 初識線程

幾乎所有的操作系統都只支持同時運行多個任務,一個任務就是一個程序,每個運行中的程序就是一個進程。當一個程序運行時,內部可能包含了多個順序執行流,每個順序執行流就是一個線程。

   1.1 進程與線程

進程是運行過程中的程序,具有一定的獨立功能,進程是系統進行資源分配和調度的一個獨立單位。一般而言,進程包括如下特征

  • 獨立性:進程是系統中獨立存在的實體,擁有自己獨立的資源,每個進程都擁有自己私有的地址空間。
  • 動態性:進程與程序的區別在於,程序只是一個靜態的指令集合,而進程是一個正在系統中活動的指令集合。
  • 並發性:多個進程可以在多個處理器上並發執行,多個進程之間不會互相影響

並發性和並行性是兩個概念。並發指在同一時刻只能有一條指令執行,但多個進程指令被快速輪換執行,是的宏觀上具有多個進程同時執行的效果。

線程是進程的組成部分,一個進程可以擁有多個線程,一個線程必須有一個父進程。線程可以擁有自己的堆棧、自己的程序計數器和自己的局部變量。但不擁有系統資源。他和父進程的其他子線程共享進程所擁有的全部資源。線程和其他線程共同協作完成進程的任務。

  1.2 線程的創建和啟動

  1.21 繼承Thread類創建線程類。

 步驟如下:

① 定義Thread的子類,重寫該類的run()方法

② 創建Thread子類的實例,創建子線程對象

③ 調用線程對象的start()方法

package com.gdut.thread;

public class FirstThread  extends Thread{

    private int i;

    public void run(){
        for (; i < 100; i++) {
            System.out.println(getName()+" "+i);
        }

    }

    public static void main(String[] args) {
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName());

            if(i == 20){
              new FirstThread().start();
              new FirstThread().start();
            }
        }
    }

}

  1.2.2 實現Runnable接口創建線程類

① 定義Runnable接口的實現類,並重寫該類的run()方法

② 創建Runnable接口實現類的實例,並以此實例作為Thread的target來創建Thread對象

③ 調用線程對象的start()方法

1.2.3 使用Callable和Future創建線程

Java 5開始,Java提供了Callable接口,該接口提供了一個call()方法可以作為線程執行體,但call()方法更加強大

  • call()方法有返回值
  • call()方法可以聲明拋出異常。

Java 5提供了Future接口來代表Callable接口里call()方法的返回值,並未Future接口提供了FutureTask實現類,該類實現了,Future接口和Runnable接口,可以作為Thread類的target。

在Future接口定義了如下公共方法來控制關聯的Callable任務

  • boolean cancle(boolean mayInterruptRunning):試圖取消Future里關聯的Callable任務
  • V get():返回Callable任務里call()方法的返回值
  • V get(Long timeout,TimeUnit unit):返回Callable任務里call()方法的返回值。該方法讓程序最多阻塞timeout和unit指定的時間,如果經過指定時間后Callable任務依然沒有返回值,將會拋出TimeoutException。
  • boolean isCancelled():如果Callable任務正常完成前被取消,則返回true。
  • boolean isdone():如果Callable任務已完成,則返回true。

創建啟動有返回值的線程步驟如下:

① 創建Callable接口的實現類,並實現Call()方法,該Call()方法將作為線程執行體,且該Call方法有返回值,在創建Callable實現類的實例

② 使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法

③ 使用FutureTask對象作為Thread對象的Target創建並啟動線程

④ 調用FutureTask對象的get()方法來獲得子線程執行結束后的返回值

package com.gdut.thread;

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class ThirdThread {

    public static void main(String[] args) {
        ThirdThread rt = new ThirdThread();
        FutureTask task = new FutureTask((Callable<Integer>)() ->{
            int i =0;

            for (; i <100 ; i++) {
                System.out.println(Thread.currentThread().getName()+"循環變量的值"+i);
            }
            return i;
        });
        for (int i = 0; i < 100; i++) {
            System.out.println(Thread.currentThread().getName()+"循環變量的值"+i);
            if(i == 20){
                new Thread(task,"有返回值的線程").start();
            }
        }
      try{
          System.out.println("子線程的返回值:"+task.get());
      }catch(Exception ex){
            ex.printStackTrace();
      }
        
        
    }
    
    
}

  1.3 創建線程的三種方式對比

采用實現Runnable、Callable接口的方式創建多線程的優缺點

  • 線程只是實現了Runnable接口或Callable接口,還可以繼承其他類
  • 在這種方式下,多個線程可以共享同一個target對象,所以非常適合多個相同線程來處理同一份資源的情況,從而可以將CPU,代碼,數據分開,形成清晰的模型,較好體現面向對象的思想
  • 劣勢是,編程稍微復雜,如果需要訪問當前線程,則必須使用Thread.currentThread()方法。

所以一般推薦使用實現Runnable、Callable接口的方式創建多線程。

  1.4 線程的生命周期

線程的生命周期經過:新建,就緒,運行,阻塞和死亡5種狀態。使用new關鍵字創建線程,該線程就處於新建狀態。當線程對象調用了start()方法之后,該程序就處於就緒狀態,Java虛擬機回為其創建方法調用棧和程序計數器,處於這個狀態的程序還沒有開始運行,只是表示該線程可以運行了,至於何時開始運行,取決於JVM里線程調度器的調度。

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

當一個線程開始運行后,他不可能一直處於運行狀態(除非它運行執行體非常短,一下子就執行完),線程在執行過程中需要被中斷,目的使其他線程獲得執行的機會,在選擇下一個線程,系統會考慮線程的優先級。

當一個線程調用了它的sleep()和yield()方法后主動放棄所占用的資源。

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

  • 線程調用sleep()方法主動放棄所占用的處理器資源
  • 線程調用了一個阻塞式的IO方法,在該方法返回前,該線程被阻塞
  • 線程試圖獲得一個同步監視器,但該同步監視器正被其他線程所持有。
  • 線程在等待某個通知
  • 程序調用了線程的suspend()方法將該線程掛起。但這個方法容易導致死鎖,應該盡量避免使用該方法。

被阻塞線程在合適的時候重新進入就緒狀態。重新等待線程調度器調度它。

當發生特定的情況時可以解除上面的阻塞

  • 調用sleep()方法的線程經過了指定時間
  • 線程調用的阻塞式IO方法已經返回
  • 線程成功的獲得了試圖取得同步監視器
  • 線程正在等待某個通知時,其他線程發出了一個通知
  • 處於掛起狀態的線程被調用了resume()回復方法

線程會以如下三種方式結束,進入死亡狀態

  • run()或call()方法執行完成,線程正常結束
  • 線程拋出一個為捕獲的Exception
  • 直接調用該線程的stop()方法來結束該線程——這個方法容易導致死鎖,應該盡量避免使用該方法。

二. 控制線程

  2.1  join線程

Thread提供了讓一個線程等待另一個線程完成的方法——join()方法。

當程序執行流中調用了其他線程的join()方法時,調用線程被阻塞,知道被join()方法加入的join線程執行完為止

join()方法通常由使用的線程的程序調用,將大問題划分許多小問題,每個小問題分配一個線程。當所有的小問題處理后,在調用主線程進一步處理。

package com.gdut.thread;

public class JoinThread extends Thread{
    public JoinThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i <100 ; i++) {
            System.out.println(getName()+" "+i);
        }
    }

    public static void main(String[] args) throws Exception {
        new JoinThread("新線程").start();
        for (int i = 0; i <100 ; i++) {
            if(i == 20){
                JoinThread jt = new JoinThread("被join的線程");
                jt.start();
                jt.join();
            }
            System.out.println(Thread.currentThread().getName()+" "+i);
        }
    }

}

   2.2 后台線程

 有一種線程,后台執行的,它的任務是為其他線程提供服務。這種線程被稱為后台線程,又稱守護線程或精靈線程。JVM的垃圾回收線程就是典型的后台線程

后台線程有個特征:如果所有的前台線程都死亡,后台線程會自動死亡。調用Thread對象的setDaemon(true)方法可將指定線程設置成后台線程。

  2.3 線程睡眠:sleep

如果需要讓正在執行的線程暫停一段時間,並進入阻塞狀態,則可以通過Thread類的靜態方法sleep()來實現。

在調用了sleep()方法之后,線程進入阻塞狀態,在其睡眠時間內,該線程將不會獲得執行的機會,即使系統中沒有其他可執行的線程。

  2.4 線程讓步

yield()方法是一個和sleep()方法有也是點相似的方法,它Thread類的提供的一個靜態方法,它可以讓當前正在執行的線程暫停,但它不會阻塞該線程,只是將線程轉入就緒狀態。yield()方法只是讓當前的線程暫停一下,讓系統的線程調度器重新調度一次,完全可能的情況是:當線程調用了yield()方法暫停之后,線程調度器又將其調度出來重新執行

關於sleep()方法和yield()方法的區別

  • sleep()方法暫停當前線程后,會給其他線程執行機會,不會理會其他線程的優先級;但yield()方法只會給優先級相同,或優先級更高的線程執行機會
  • sleep()方法會將線程轉入阻塞狀態,知道經過阻塞時間才會轉入就緒狀態;yield()方法只是強制將線程轉入就緒狀態
  • sleep()方法聲明拋出了InterruptedException異常,所以調用sleep()方法要么捕捉異常,要么顯式聲明拋出異常;而yield()方法則沒有聲明拋出異常。
  • sleep()方法比yield()方法有更好的可移植性,通常不建議使用yield()方法來控制並發線程的執行

  2.5 改變線程優先級

每個線程執行時都具有一定的優先級,優先級高的線程得到更多執行的機會,而優先級低的線程則得到較少的執行機會。

每個線程默認的優先級都和創建它的父線程的優先級相同,默認情況下,main線程具有普通優先級,由main線程創建的子線程也具有普通優先級。

Thread類提供了setPriority(int new Priority)、getPriority()方法來設置和返回指定線程的優先級,其中setPriority(int new Priority)參數可以是一個整數,范圍1~10,也可以使用Thread類的如下三個常量:

  • MAX_PRIORITY:其值是10
  • MIN_PRIORITY:其值是1
  • NORM_PRIORITY:其值是5

三. 線程同步和線程安全問題

  3.1 線程安全

臟數據問題,例如多個線程同時操作公共數據引發的數據出錯問題,這里不再敘述

  3.2 同步代碼塊

為了解決上面的問題,Java多線程支持引入了同步監視器來解決這個問題,使用同步監視器的通用方法就是同步代碼塊。同步代碼塊的語法格式如下:

synchronized(obj){
//此處代碼就是同步代碼塊
}

上面代碼的含義是:線程開始執行同步代碼塊之前,必須先獲得同步監視器的鎖定。

  3.3 同步方法

與同步代碼塊對應,Java的多線程安全還支持同步方法,同步方法就是用synchronized關鍵字來修飾某個方法,則該方法稱為同步方法。對於synchronized修飾的方法(非static方法)而言,無須顯式指定同步監視器,同步方法的監視器是this,也就是調用該方法的對象

當執行了同步監視器的wait()方法,則當前線程暫停,並釋放同步監視器。

  3.4 同步鎖

從Java 5開始,Java提供了更強大的線程同步機制——通過顯式定義同步鎖對象實現同步,在這種機制下,同步鎖由lock充當。

某些鎖可能允許對貢獻資源的訪問。如ReadWriteLock,Lock,ReentrantRock(可重入鎖)。為ReadWriteRock提供了ReentrantReadWriteLock實現類。

通常建議在Finally塊確保在必要時釋放鎖。

  3.5 死鎖

       當兩個線程相互等待對方釋放鎖是就會發生死鎖。一旦出現死鎖,整個程序將不會發生任何異常,也不會給出任何提示。只是所有的線程都處於阻塞狀態,無法繼續。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM