1. 程式人生 > 其它 >多執行緒程式設計學習一(Java多執行緒的基礎).

多執行緒程式設計學習一(Java多執行緒的基礎).

一、程序和執行緒的概念

程序:一次程式的執行稱為一個程序,每個 程序有獨立的程式碼和資料空間,程序間切換的開銷比較大,一個程序包含1—n個執行緒。程序是資源分享的最小單位。

執行緒:同一類執行緒共享程式碼和資料空間,每個執行緒有獨立的執行棧和程式計數器(PC),執行緒切換開銷小,執行緒是CPU排程的最小單位。

多程序:指作業系統能同時執行多個任務(程式)。

多執行緒:指同一個程式中有多個順序流在執行,執行緒是程序內部單一控制序列流。

       執行緒和程序一樣包括:建立、就緒、執行、阻塞、銷燬 五個狀態:

1、新建狀態(New):新建立了一個執行緒物件。

2、就緒狀態(Runnable):執行緒物件建立後,其他執行緒呼叫了該物件的start()方法。該狀態的執行緒位於可執行執行緒池中,變得可執行,等待獲取CPU的使用權。

3、執行狀態(Running):就緒狀態的執行緒獲取了CPU,執行程式程式碼。

4、阻塞狀態(Blocked):阻塞狀態是執行緒因為某種原因放棄CPU使用權,暫時停止執行。直到執行緒進入就緒狀態,才有機會轉到執行狀態。阻塞的情況分三種:

(一)、等待阻塞:執行的執行緒執行wait()方法,JVM會把該執行緒放入等待池中。(wait會釋放持有的鎖)

(二)、同步阻塞:執行的執行緒在獲取物件的同步鎖時,若該同步鎖被別的執行緒佔用,則JVM會把該執行緒放入鎖池中。

(三)、其他阻塞:執行的執行緒執行sleep()或join()方法,或者發出了I/O請求時,JVM會把該執行緒置為阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入就緒狀態。(注意,sleep是不會釋放持有的鎖)

5、死亡狀態(Dead):執行緒執行完了或者因異常退出了run()方法,該執行緒結束生命週期。

二、多執行緒的優勢

    單執行緒的特點就是排隊執行,也就是同步。而多執行緒能最大限度的利用CPU的空閒時間來處理其他的任務,系統的執行效率大大提升,使用多執行緒也就是在執行非同步。

三、使用多執行緒

    實現多執行緒程式設計的方式主要有兩種,一種是繼承Thread類,另一種是實現Runable介面。其實,使用繼承Thread類的方式建立新執行緒時,最大的侷限就是不支援多繼承,因為Java語言的特點就是單根繼承,所以為了支援多繼承,完全可以實現Runnable介面,一邊實現一邊繼承。但是這兩種方式建立的執行緒在工作時的性質是一樣的,沒有本質的差別。

public class Thread1 extends Thread {
    private int count=5;
    @Override
    public void run()
    {
        for (int i=0;i<2;i++){
            System.out.println("現在是執行緒"+currentThread().getName()+"在執行:"+count--); }
    }
}
public class Thread2 implements Runnable {
    private int count=5;
    @Override
    public void run() {
        for(int i=0;i<2;i++){
            System.out.println("現在是執行緒"+Thread.currentThread().getName()+"在執行:"+count--); }
    }
}
public class Test{
    public static void main(String[] args){
        //整合Thread類
        Thread1 thread1=new Thread1();
        Thread t1=new Thread(thread1,"A");
        Thread t2=new Thread(thread1,"B");
        Thread t3=new Thread(thread1,"C");
        t1.start();
        t2.start();
        t3.start();
        //實現Runable介面
        Thread2 thread2=new Thread2();
        Thread t4=new Thread(thread2,"A2");
        Thread t5=new Thread(thread2,"B2");
        Thread t6=new Thread(thread2,"C2");
        t4.start();
        t5.start();
        t6.start();
    }}

演示這個結果是為了說明以下下兩點:

1、CPU對執行緒的排程具有不確定性,採用“搶佔式”排程。

2、對於網上經常說的,實現Runnable介面的執行緒可以實現共享資料,而繼承Thread的執行緒就不能。其實不然,它們兩者的區別僅是單繼承的限制以及一些用法的不同(比如 如果你想對這個Thread物件做點別的事情(比如getName),那麼你就必須通過呼叫Thread.currentThread()方法得到對此執行緒的引用),沒有實質的差別。

多執行緒執行時為什麼呼叫的是start()方法而不是run()方法?

    如果呼叫程式碼thread.run()就不是非同步執行了,而是同步,那麼此執行緒物件就不會交給“執行緒規劃器”來進行處理。而是由main主執行緒來呼叫run()方法,也就是說必須要等到run()方法中的程式碼執行完成後才可以執行後面的程式碼。start()用來啟動一個執行緒,當呼叫start()方法時,系統才會開啟一個執行緒,通過Thead類中start()方法來啟動的執行緒處於就緒狀態(可執行狀態),此時並沒有執行,一旦得到CPU時間片,就自動開始執行run()方法。 

四、synchronized 關鍵字

    多執行緒的鎖機制,通過在多執行緒要呼叫的方法前加入synchronized 關鍵字,使多個執行緒在執行方法時,要首先嚐試去拿這把鎖,如果能夠拿到這把鎖,那麼這個執行緒就可以執行synchronize裡面的程式碼。如果不能拿到這把鎖,那麼這個執行緒就會不斷地嘗試拿這把鎖,直到拿到這把鎖。synchronized 可以在任意物件及方法上加鎖,而加鎖的這段程式碼稱為“互斥區” 或 “臨界區”。

    使用synchronized關鍵字主要是為了保證當前執行緒在執行過程中,不被其他執行緒搶佔並修改了共享的資源,從而導致執行緒不安全的情況出現。

五、常用執行緒方法

1、Thread.currentThread()方法:返回程式碼段正在被哪個執行緒呼叫的資訊。最常見的就是Thread.currentThread().getName()。

2、isAlive()方法:判斷當前的執行緒是否處於活動狀態。什麼是活動狀態呢?活動狀態就是執行緒已經啟動且尚未終止。執行緒正在執行或準備開始執行的狀態,就認為執行緒是“存活”的。

3、Thread.sleep()方法:在指定的毫秒數內讓"正在執行的執行緒"休眠(暫停執行)。這個“正在執行的執行緒”是指this.currentThread()返回的執行緒。

4、getId()方法:取得該執行緒的唯一標識。

5、Thread.interrupt()方法:用於中斷執行緒,這裡需要注意Thread.interrupt 的作用其實也不是中斷執行緒,而是「通知執行緒應該中斷了」,具體到底中斷還是繼續執行,應該由被通知的執行緒自己處理。具體來說,當對一個執行緒,呼叫 interrupt() 時,

① 如果執行緒處於被阻塞狀態(例如處於sleep, wait, join 等狀態),那麼執行緒將立即退出被阻塞狀態,並丟擲一個InterruptedException異常,並且清除中斷標誌,使之變為false。 ② 如果執行緒處於正常活動狀態,那麼會將該執行緒的中斷標誌設定為 true,僅此而已。被設定中斷標誌的執行緒將繼續正常執行,不受影響。

Thread thread = new Thread(() -> {
    while (!Thread.interrupted()) {//通過這樣來檢查這個中斷標誌位是否設定為true,是否進行程式邏輯,請不要使用廢棄的Thread.stop, Thread.suspend, Thread.resume
        // do more work.
    }
});
thread.start();

// 一段時間以後
thread.interrupt();

 值得一提的是,判斷執行緒是否中斷有兩個辦法:

  • interrupted():測試當前執行緒是否已經是中斷狀態,執行後具有將狀態標誌清除的false的功能。(這裡需要特別注意的是即使是MyThread.interrupted(),測試的仍然是當前執行緒(this.currentThread())的狀態)。
  • isInterrupted():測試執行緒Thread物件是否已經是中斷狀態,但不清除狀態標誌。

 通過丟擲異常來中斷執行緒:

public class MyThread extends Thread {
    @Override
    public void run(){
        try {
            for (int i = 0; i < 500000; i++) {
                if (this.interrupted()) {
                    System.out.println("已經是停止狀態了!我要退出了");
                    throw new InterruptedException();
                }
                System.out.println("i=" + (i + 1));
            }
        } catch (InterruptedException e) {
            System.out.println("進入MyThread.java類run方法中的catch了!");
            e.printStackTrace();
        }
    }
}

 另外,還可以通過retuen的方法來中斷執行緒。不過還是建議"拋異常"的方法來實現執行緒的停止,因為在catch塊中還可以將異常向上拋,使執行緒停止的事件得以傳播。

6、Thread.yield()方法:放棄當前的CPU資源,將它讓給其他的任務去佔用CPU的執行時間。但放棄的時間不確定,有可能剛剛放棄,馬上又獲得CPU時間片。

7、setPriority()方法:為了程式的可移植性,建議植使用 MAX_PRIORITY , NORM_PRIORITY , MIN_PRIORITY 三個級別。設定優先順序並不意味著優先順序低的就得不到呼叫,只是CPU更傾向於讓高的優先順序先執行,但是CPU具體呼叫那個執行緒是無法確定的,設定優先順序只能保證說這個執行緒被呼叫的頻率比較高。

8、setDaemon(true):守護執行緒。守護執行緒是一個特殊的執行緒,它的特性有“陪伴”的含義,當程序中不存在非守護執行緒了,則守護執行緒自動銷燬。典型的守護執行緒就是垃圾回收執行緒,當程序中沒有非守護執行緒了,則垃圾回收執行緒也就沒有存在的必要了。

9、join()方法:等待該執行緒終止。join() 方法主要是讓呼叫該方法的thread完成run方法裡面的東西后, 再執行join()方法後面的程式碼,對join()方法的呼叫可以被中斷,做法是呼叫執行緒上的的interrupt()方法。

六、其他

1、stop()方法作廢:如果強制讓執行緒停止則有可能使一些清理性的工作得不到完成。另外一種情況就是對鎖定的物件進行了“解鎖”,導致資料得不到同步的處理,出現數據不一致的問題。

2、suspend()方法暫停執行緒。resume()方法恢復執行緒的執行。

3、在使用suspend()和resume()時,如果使用不當,極易造成公共的同步物件的獨佔,使得其他執行緒無法訪問公共同步物件。比如因為執行緒的暫停而導致資料不同步、suspend 和 resume會對訪問資源的鎖進行獨佔(i++沒有鎖、println()具有同步鎖)。