Java多線程詳解-入門篇

進程與線程

在講多線程以前,我以爲有必要先說一下進程與線程之間的關係與差別。java

一、進程是資源分配的最小單位,線程是程序執行的最小單位(資源調度的最小單位);chrome

二、進程有本身的獨立地址空間,每啓動一個進程,系統就會爲它分配地址空間,創建數據表來維護代碼段、堆棧段和數據段,這種操做很是昂貴;編程

而線程是共享進程中的數據的,使用相同的地址空間,所以CPU切換一個線程的花費遠比進程要小不少,同時建立一個線程的開銷也比進程要小不少;安全

三、線程之間的通訊更方便,同一進程下的線程共享全局變量、靜態變量等數據,而進程之間的通訊須要以通訊的方式(IPC)進行。不過如何處理好同步與互斥是編寫多線程程序的難點;bash

四、可是多進程程序更健壯,多線程程序只要有一個線程死掉,整個進程也死掉了,而一個進程死掉並不會對另一個進程形成影響,由於進程有本身獨立的地址空間。數據結構

通俗點來說,進程就像是任務管理器中的qq,chrome,網易雲音樂這種一個個應用,而線程就像是在這個進程中間的一次任務,好比你點擊切換音樂,聊天發送信息等。多線程

多線程的實現

在Java中多線程的實現有三種形式,這裏只說前兩種,繼承Thread類和實現Runnable接口。dom

1 繼承Thread類
//繼承Thread實現多線程
class Thread1 extends Thread{  
    private String name;  
    public Thread1(String name) {  
       this.name=name;  
    }  
    public void run() {  
        for (int i = 0; i < 5; i++) {  
            System.out.println(name + "運行 : " + i);  
            try {  
                sleep((int) Math.random() * 10);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
         
    }  
}  
public class Main {  
  
    public static void main(String[] args) {  
        Thread1 mTh1=new Thread1("A");  
        Thread1 mTh2=new Thread1("B");  
        mTh1.start();  
        mTh2.start();  
  
    }  
}  
複製代碼

上面這個兩個類,Thread1類繼承了Thread父類,並重寫了裏面的run方法。實現了多線程裏面的方法,並在main函數中進行實例化了兩個mTh1,mTh2兩個線程。異步

啓動main函數:ide

輸出:
A運行 : 0
B運行 : 0
A運行 : 1
A運行 : 2
A運行 : 3
A運行 : 4
B運行 : 1
B運行 : 2
B運行 : 3
B運行 : 4
複製代碼

再運行一下:

A運行 : 0
B運行 : 0
B運行 : 1
B運行 : 2
B運行 : 3
B運行 : 4
A運行 : 1
A運行 : 2
A運行 : 3
A運行 : 4
複製代碼

能夠看到兩次運行的結果是不太同樣的。

說明

程序在啓動main函數時,Java虛擬機就已經啓動了一個主線程來運行main函數,在調用到mTh1mTh2的start方法時,就至關於有三個線程在同時工做了,這就是多線程的模式,進入了mTh1子線程,這個線程中的操做,在這個線程中有sleep()方法,Thread.sleep()方法調用目的是不讓當前線程獨自霸佔該進程所獲取的CPU資源,以留出必定時間給其餘線程執行的機會。

實際上全部的線程執行順序都是不肯定的,CPU資源的獲取徹底是看兩個線程之間誰先搶佔上誰就先運行,當mTh1搶佔上線程後,運行run方法中的代碼,到sleep()方法進入休眠狀態,也就是阻塞狀態,而後CPU資源會被釋放,AB再次進行搶佔CPU資源操做,搶佔上的繼續運行。在運行的結果中你也能夠看到這個現象。

注意:

一個實例的start()方法不能重複調用,不然會出現java.lang.IllegalThreadStateException異常。

2 實現java.lang.Runnable接口

採用Runnable也是很是常見的一種,咱們只須要重寫run方法便可。下面也來看個實例。

class Thread2 implements Runnable{  
    private String name;  
  
    public Thread2(String name) {  
        this.name=name;  
    }  
  
    @Override  
    public void run() {  
          for (int i = 0; i < 5; i++) {  
                System.out.println(name + "運行 : " + i);  
                try {  
                    Thread.sleep((int) Math.random() * 10);  
                } catch (InterruptedException e) {  
                    e.printStackTrace();  
                }  
            }  
          
    }  
      
}  
public class Main {  
  
    public static void main(String[] args) {  
        new Thread(new Thread2("C")).start();  
        new Thread(new Thread2("D")).start();  
    }  
  
}  
複製代碼

總體和繼承Thread差異不大,由於在Thread類中也是繼承的Runnable接口。

輸出運行:

C運行 : 0
D運行 : 0
D運行 : 1
C運行 : 1
D運行 : 2
C運行 : 2
D運行 : 3
C運行 : 3
D運行 : 4
C運行 : 4
複製代碼

說明:

Thread2類經過實現Runnable接口,使得該類有了多線程類的特徵。run()方法是多線程程序的一個約定。全部的多線程代碼都在run方法裏面。Thread類實際上也是實現了Runnable接口的類。

在啓動的多線程的時候,須要先經過Thread類的構造方法Thread(Runnable target)構造出對象,而後調用Thread對象的start()方法來運行多線程代碼。

實際上全部的多線程代碼都是經過運行Threadstart()方法來運行的。所以,無論是擴展Thread類仍是實現Runnable接口來實現多線程,最終仍是經過Thread的對象的API來控制線程的,熟悉Thread類的API是進行多線程編程的基礎。

Thread類和Runnable接口的區別

若是一個類繼承Thread,則不適合資源共享。可是若是實現了Runable接口的話,則很容易的實現資源共享。

總結:

實現Runnable接口比繼承Thread類所具備的優點:

1):適合多個相同的程序代碼的線程去處理同一個資源

2):能夠避免java中的單繼承的限制

3):增長程序的健壯性,代碼能夠被多個線程共享,代碼和數據獨立

4):線程池只能放入實現Runablecallable類線程,不能直接放入繼承Thread的類

提醒一下你們:main方法其實也是一個線程。在java中因此的線程都是同時啓動的,至於何時,哪一個先執行,徹底看誰先獲得CPU的資源。

java中,每次程序運行至少啓動2個線程。一個是main線程,一個是垃圾收集線程。由於每當使用java命令執行一個類的時候,實際上都會啓動一個JVM,每個JVM實習在就是在操做系統中啓動了一個進程。

線程的狀態

下面先放一張線程的展現圖

img

1:新建狀態(New):new Thread(),新建立了一個線程;

2:就緒狀態(Runnable):新建完成後,主線程(main()方法)調用了該線程的start()方法,CPU目前在執行其餘任務或者線程,這個建立好的線程就會進入就緒狀態,等待CPU資源運行程序,在運行以前的這段時間處於就緒狀態;

3:運行狀態(Running):字面意思,線程調用了start()方法以後而且搶佔到了CPU資源,運行run方法中的程序代碼;

4:阻塞狀態(Blocked):阻塞狀態時線程在運行過程當中由於某些操做暫停運行,放棄CPU使用權,進入就緒狀態和其餘線程一同進行下次CPU資源的搶佔。

當發生以下狀況時,線程將會進入阻塞狀態

  ① 線程調用sleep()方法主動放棄所佔用的處理器資源

  ② 線程調用了一個阻塞式IO方法,在該方法返回以前,該線程被阻塞

  ③ 線程試圖得到一個同步監視器,但該同步監視器正被其餘線程所持有。關於同步監視器的知識、後面將有深刻的介紹

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

  ⑤ 程序調用了線程的suspend()方法將該線程掛起。但這個方法容易致使死鎖,因此應該儘可能避免使用該方法

  當前正在執行的線程被阻塞以後,其餘線程就能夠得到執行的機會。被阻塞的線程會在合適的時候從新進入就緒狀態,注意是就緒狀態而不是運行狀態。也就是說,被阻塞線程的阻塞解除後,必須從新等待線程調度器再次調度它。

解除阻塞

  針對上面幾種狀況,當發生以下特定的狀況時能夠解除上面的阻塞,讓該線程從新進入就緒狀態:

  ① 調用sleep()方法的線程通過了指定時間。

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

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

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

  ⑤ 處於掛起狀態的線程被調甩了resdme()恢復方法(會致使死鎖,儘可能避免使用)。

5:死亡狀態(Dead):線程程序執行完成或者由於發生異常跳出了run()方法,線程生命週期結束。

線程的調度

1:調整線程優先級:Java線程有優先級,優先級高的線程會得到較多的運行機會。

Java線程的優先級用整數表示,取值範圍是1~10,Thread類有如下三個靜態常量:

static int MAX_PRIORITY線程能夠具備的最高優先級,取值爲10。

static int MIN_PRIORITY線程能夠具備的最低優先級,取值爲1。

static int NORM_PRIORITY分配給線程的默認優先級,取值爲5。

Thread類的setPriority()getPriority()方法分別用來設置和獲取線程的優先級。

每一個線程都有默認的優先級。主線程的默認優先級爲Thread.NORM_PRIORITY

線程的優先級有繼承關係,好比A線程中建立了B線程,那麼B將和A具備相同的優先級。

JVM提供了10個線程優先級,但與常見的操做系統都不能很好的映射。若是但願程序能移植到各個操做系統中,應該僅僅使用Thread類有如下三個靜態常量做爲優先級,這樣能保證一樣的優先級採用了一樣的調度方式。

二、線程睡眠:Thread.sleep(long millis)方法,使線程轉到阻塞狀態。millis參數設定睡眠的時間,以毫秒爲單位。當睡眠結束後,就轉爲就緒(Runnable)狀態。sleep()平臺移植性好。

三、線程等待:Object類中的wait()方法,致使當前的線程等待,直到其餘線程調用此對象的 notify() 方法或notifyAll()喚醒方法。這個兩個喚醒方法也是Object類中的方法,行爲等價於調用 wait(0) 同樣。

四、線程讓步:Thread.yield()方法,暫停當前正在執行的線程對象,把執行機會讓給相同或者更高優先級的線程。

五、線程加入:join()方法,等待其餘線程終止。在當前線程中調用另外一個線程的join()方法,則當前線程轉入阻塞狀態,直到另外一個進程運行結束,當前線程再由阻塞轉爲就緒狀態。

六、**線程喚醒:**Object類中的notify()方法,喚醒在此對象監視器上等待的單個線程。若是全部線程都在此對象上等待,則會選擇喚醒其中一個線程。選擇是任意性的,並在對實現作出決定時發生。線程經過調用其中一個 wait 方法,在對象的監視器上等待。 直到當前的線程放棄此對象上的鎖定,才能繼續執行被喚醒的線程。被喚醒的線程將以常規方式與在該對象上主動同步的其餘全部線程進行競爭;例如,喚醒的線程在做爲鎖定此對象的下一個線程方面沒有可靠的特權或劣勢。相似的方法還有一個notifyAll(),喚醒在此對象監視器上等待的全部線程。

注意:Thread中suspend()和resume()兩個方法在JDK1.5中已經廢除,再也不介紹。由於有死鎖傾向。

經常使用函數說明

1:sleep(long millis): 在指定的毫秒數內讓當前正在執行的線程休眠(暫停執行);

sleep()使當前線程進入停滯狀態(阻塞當前線程),讓出CUP的使用、目的是不讓當前線程獨自霸佔該進程所獲的CPU資源,以留必定時間給其餘線程執行的機會;

   sleep()是Thread類的Static(靜態)的方法;所以他不能改變對象的機鎖,因此當在一個Synchronized塊中調用Sleep()方法是,線程雖然休眠了,可是對象的機鎖並木有被釋放,其餘線程沒法訪問這個對象(即便睡着也持有對象鎖)。

  在sleep()休眠時間期滿後,該線程不必定會當即執行,這是由於其它線程可能正在運行並且沒有被調度爲放棄執行,除非此線程具備更高的優先級。

2:join():指等待t線程終止。

Thread t = new AThread(); t.start(); t.join();  
複製代碼

爲何要用join()方法

在不少狀況下,主線程生成並起動了子線程,若是子線程裏要進行大量的耗時的運算,主線程每每將於子線程以前結束,可是若是主線程處理完其餘的事務後,須要用到子線程的處理結果,也就是主線程須要等待子線程執行完成以後再結束,這個時候就要用到join()方法了。

不加join()方法:

class Thread1 extends Thread{  
    private String name;  
    public Thread1(String name) {  
        super(name);  
       this.name=name;  
    }  
    public void run() {  
        System.out.println(Thread.currentThread().getName() + " 線程運行開始!");  
        for (int i = 0; i < 5; i++) {  
            System.out.println("子線程"+name + "運行 : " + i);  
            try {  
                sleep((int) Math.random() * 10);  
            } catch (InterruptedException e) {  
                e.printStackTrace();  
            }  
        }  
        System.out.println(Thread.currentThread().getName() + " 線程運行結束!");  
    }  
}  
  
public class Main {  
  
    public static void main(String[] args) {  
        System.out.println(Thread.currentThread().getName()+"主線程運行開始!");  
        Thread1 mTh1=new Thread1("A");  
        Thread1 mTh2=new Thread1("B");  
        mTh1.start();  
        mTh2.start();  
        System.out.println(Thread.currentThread().getName()+ "主線程運行結束!");  
  
    }  
  
}  
複製代碼

輸出結果:

main主線程運行開始!
main主線程運行結束!
B 線程運行開始!
子線程B運行 : 0
A 線程運行開始!
子線程A運行 : 0
子線程B運行 : 1
子線程A運行 : 1
子線程A運行 : 2
子線程A運行 : 3
子線程A運行 : 4
A 線程運行結束!
子線程B運行 : 2
子線程B運行 : 3
子線程B運行 : 4
B 線程運行結束!
複製代碼

發現了main函數主線程比A,B子線程都提早結束。

加入join()方法:

(線程方法一致,再也不重複)

public class Main {  
  
    public static void main(String[] args) {  
        System.out.println(Thread.currentThread().getName()+"主線程運行開始!");  
        Thread1 mTh1=new Thread1("A");  
        Thread1 mTh2=new Thread1("B");  
        mTh1.start();  
        mTh2.start();  
        try {  
            mTh1.join();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        try {  
            mTh2.join();  
        } catch (InterruptedException e) {  
            e.printStackTrace();  
        }  
        System.out.println(Thread.currentThread().getName()+ "主線程運行結束!");  
  
    }  

}  
複製代碼

運行結果:

main主線程運行開始!
A 線程運行開始!
子線程A運行 : 0
B 線程運行開始!
子線程B運行 : 0
子線程A運行 : 1
子線程B運行 : 1
子線程A運行 : 2
子線程B運行 : 2
子線程A運行 : 3
子線程B運行 : 3
子線程A運行 : 4
子線程B運行 : 4
A 線程運行結束!
main主線程運行結束!
複製代碼

主線程必定會等子線程都結束了才結束

3:yield():暫停當前正在執行的線程對象,並執行其餘線程。

Thread.yield()方法做用是:暫停當前正在執行的線程對象,並執行其餘線程。

yield()應該作的是讓當前運行線程回到可運行狀態,以容許具備相同優先級的其餘線程得到運行機會。所以,使用yield()的目的是讓相同優先級的線程之間能適當的輪轉執行。可是,實際中沒法保證yield()達到讓步目的,由於讓步的線程還有可能被線程調度程序再次選中。

結論:yield()從未致使線程轉到等待/睡眠/阻塞狀態。在大多數狀況下,yield()將致使線程從運行狀態轉到可運行狀態,但有可能沒有效果。可看上面的圖。

class ThreadYield extends Thread{  
    public ThreadYield(String name) {  
        super(name);  
    }  
   
    @Override  
    public void run() {  
        for (int i = 1; i <= 50; i++) {  
            System.out.println("" + this.getName() + "-----" + i);  
            // 當i爲30時,該線程就會把CPU時間讓掉,讓其餘或者本身的線程執行(也就是誰先搶到誰執行) 
            if (i ==30) {  
                this.yield();  
            }  
        }  
      
}  
}  
  
public class Main {  
  
    public static void main(String[] args) {  
          
        ThreadYield yt1 = new ThreadYield("張三");  
        ThreadYield yt2 = new ThreadYield("李四");  
        yt1.start();  
        yt2.start();  
    }  
  
}  
複製代碼

運行結果:

第一種狀況:李四(線程)當執行到30時會CPU時間讓掉,這時張三(線程)搶到CPU時間並執行。

第二種狀況:李四(線程)當執行到30時會CPU時間讓掉,這時李四(線程)搶到CPU時間並執行。

sleep()和yield()的區別

sleep()使當前線程進入停滯狀態,確切來講進入阻塞狀態,等sleep()規定的時間過了以後,該線程會繼續執行,而停滯時間內會執行其餘線程,yield()方法是直接中止該線程而後讓線程從運行狀態變成就緒狀態,跟其餘線程一塊去搶奪CPU資源,有可能他會當即又搶奪到CPU資源,繼續執行線程。

sleep 方法使當前運行中的線程睡眠一段時間,進入不可運行狀態,這段時間的長短是由程序設定的,yield 方法使當前線程讓出 CPU 佔有權,但讓出的時間是不可設定的。實際上,yield()方法對應了以下操做:先檢測當前是否有相同優先級的線程處於同可運行狀態,若有,則把 CPU 的佔有權交給此線程,不然,繼續運行原來的線程。因此yield()方法稱爲「退讓」,它把運行機會讓給了同等優先級的其餘線程。

另外,sleep 方法容許較低優先級的線程得到運行機會,但 yield() 方法執行時,當前線程仍處在可運行狀態,因此,不可能讓出較低優先級的線程些時得到 CPU 佔有權。在一個運行系統中,若是較高優先級的線程沒有調用 sleep 方法,又沒有受到 I\O 阻塞,那麼,較低優先級線程只能等待全部較高優先級的線程運行結束,纔有機會運行。

4:setPriority(): 更改線程的優先級。   MIN_PRIORITY = 1   NORM_PRIORITY = 5 MAX_PRIORITY = 10

5:interrupt():

interrupt()方法不是中斷某個線程,而是給線程發送一箇中斷信號,讓線程在無限等待時(如死鎖時)能拋出異常,從而結束線程,可是若是你吃掉了這個異常,那麼這個線程仍是不會中斷的!

(中斷這塊我會專門寫一篇來說interrupt,isInterrupted,interrupted。還有已經被淘汰的stop,suspend方法爲何會被淘汰)

6:其餘方法

還有wait(),notify(),notifyAll()這些方法,由於這三個方法要跟線程的鎖結合起來說解,因此咱們放在下次跟多線程的鎖一塊講解。還有就是Java線程池的概念以及鎖中的區別等等。

線程數據傳遞

在傳統的同步開發模式下,當咱們調用一個函數時,經過這個函數的參數將數據傳入,並經過這個函數的返回值來返回最終的計算結果。但在多線程的異步開發模式下,數據的傳遞和返回和同步開發模式有很大的區別。因爲線程的運行和結束是不可預料的,所以,在傳遞和返回數據時就沒法象函數同樣經過函數參數和return語句來返回數據。

1:經過構造方法傳遞數據

在建立線程時,必需要創建一個Thread類的或其子類的實例。所以,咱們不難想到在調用start方法以前經過線程類的構造方法將數據傳入線程。並將傳入的數據使用類變量保存起來,以便線程使用(其實就是在run方法中使用)。下面的代碼演示瞭如何經過構造方法來傳遞數據:

package mythread;   
public class MyThread1 extends Thread {   
private String name;   
public MyThread1(String name) {   
this.name = name;   
}   
public void run() {   
System.out.println("hello " + name);   
}   
public static void main(String[] args) {   
Thread thread = new MyThread1("world");   
thread.start();   
}   
}   
複製代碼

因爲這種方法是在建立線程對象的同時傳遞數據的,所以,在線程運行以前這些數據就就已經到位了,這樣就不會形成數據在線程運行後才傳入的現象。若是要傳遞更復雜的數據,可使用集合、類等數據結構。使用構造方法來傳遞數據雖然比較安全,但若是要傳遞的數據比較多時,就會形成不少不便。因爲Java沒有默認參數,要想實現相似默認參數的效果,就得使用重載,這樣不但使構造方法自己過於複雜,又會使構造方法在數量上大增。所以,要想避免這種狀況,就得經過類方法或類變量來傳遞數據。

2:經過變量和方法傳遞數據

向對象中傳入數據通常有兩次機會,第一次機會是在創建對象時經過構造方法將數據傳入,另一次機會就是在類中定義一系列的public的方法或變量(也可稱之爲字段)。而後在創建完對象後,經過對象實例逐個賦值。下面的代碼是對MyThread1類的改版,使用了一個setName方法來設置 name變量:

package mythread;   
public class MyThread2 implements Runnable {   
private String name;   
public void setName(String name) {   
this.name = name;   
}   
public void run() {   
System.out.println("hello " + name);   
}   
public static void main(String[] args) {   
MyThread2 myThread = new MyThread2();   
myThread.setName("world");   
Thread thread = new Thread(myThread);   
thread.start();   
}   
}   
複製代碼

3:經過回調函數傳遞數據

上面討論的兩種向線程中傳遞數據的方法是最經常使用的。但這兩種方法都是main方法中主動將數據傳入線程類的。這對於線程來講,是被動接收這些數據的。然而,在有些應用中須要在線程運行的過程當中動態地獲取數據,如在下面代碼的run方法中產生了3個隨機數,而後經過Work類的process方法求這三個隨機數的和,並經過Data類的value將結果返回。從這個例子能夠看出,在返回value以前,必需要獲得三個隨機數。也就是說,這個 value是沒法事先就傳入線程類的。

package mythread;   
class Data {   
public int value = 0;   
}   
class Work {   
public void process(Data data, Integer numbers) {   
for (int n : numbers)   
{   
data.value += n;   
}   
}   
}   
public class MyThread3 extends Thread {   
private Work work;   
public MyThread3(Work work) {   
this.work = work;   
}   
public void run() {   
java.util.Random random = new java.util.Random();   
Data data = new Data();   
int n1 = random.nextInt(1000);   
int n2 = random.nextInt(2000);   
int n3 = random.nextInt(3000);   
work.process(data, n1, n2, n3); // 使用回調函數 
System.out.println(String.valueOf(n1) + "+" + String.valueOf(n2) + "+"   
+ String.valueOf(n3) + "=" + data.value);   
}   
public static void main(String[] args) {   
Thread thread = new MyThread3(new Work());   
thread.start();   
}   
}   
複製代碼

總結

這篇基本講了Java多線程中的基礎部分,後續還會有線程同步(鎖),線程如何正確的中斷,線程池等。Java的多線程部分是比較複雜的,只有平時多看多練才能記住並應用到實際項目中去。互勉~

相關文章
相關標籤/搜索