多執行緒(一)、基礎概念及notify()和wait()的使用

EvanZch發表於2019-12-11

一、基礎概念1.1、CPU核心數和執行緒數的關係1.2、時間片輪轉機制 (RR 排程)1.3、程式和執行緒1.3.1、什麼是程式?1.3.2、什麼是執行緒?1.4、 併發與並行1.4.1、什麼是併發1.4.2、什麼是並行1.5、同步與非同步1.5.1、什麼是同步1.5.2、什麼是非同步二、多執行緒使用2.1、建立多執行緒2.1.1、實現Runnable介面2.1.2、繼承Thread類2.1.3、實現Callable介面2.2、終止執行緒2.2.1、 自然終止2.2.2、手動終止三、執行緒之間共享和協作3.1、 執行緒之間共享3.2、執行緒之間的協作3.2.1、 nitify()、notifyAll()、wait() 等待/通知機制

一、基礎概念

1.1、CPU核心數和執行緒數的關係

多核心 :單晶片多處理器( Chip Multiprocessors,簡稱CMP),其思想是將大規模並行處理器中的SMP(對稱多處理器)整合到同一晶片內,各個處理器並行執行不同的程式。這種依靠多個CPU同時並行地執行程式是實現超高速計算的一個重要方向,稱為並行處理,

多執行緒 :讓同一個處理器上的多個執行緒同步執行共享處理器的執行資源,可最大限度地實現寬發射、亂序的超標量處理,提高處理器運算部件的利用率,緩和由於資料相關或 Cache未命中帶來的訪問記憶體延時。

二者關係 : 目前CPU基本都是多核,很少看到單核CPU。增加核心數目就是為了增加執行緒數,因為作業系統是通過執行緒來執行任務的,一般情況下它們是1:1對應關係,也就是說四核CPU一般擁有四個執行緒。但Intel引入超執行緒技術後,使核心數與執行緒數形成1:2的關係.

1.2、時間片輪轉機制 (RR 排程)

定義:系統把所有就緒程式先入先出的原則排成一個佇列。新來的程式加到就緒佇列末尾。每當執行程式排程時,程式排程程式總是選出就緒佇列的隊首程式,讓它在CPU上執行一個時間片的時間。時間片是一個小的時間單位,通常為10~100ms數量級。當程式用完分給它的時間片後,系統的計時器發出時鐘中斷,排程程式便停止該程式的執行,把它放入就緒佇列的末尾;然後,把CPU分給就緒佇列的隊首程式,同樣也讓它執行一個時間片,如此往復。

根據上面CPU核心數和執行緒數的關係 1:1的關係,如果我們手機是雙核手機,那麼我們按道理只能起兩個執行緒,但是在實際的開發過程中並不是這樣,我們可能開了十幾個執行緒 "同時" 在執行,這是因為作業系統提供了CPU時間片輪轉這個機制,它為每個程式分配一個時間段(即時間片),讓他們在一段時間內交替執行。

上下文切換時間:由於時間片輪轉進位制,會使得程式之間不停的進行切換,程式之間切換涉及到儲存和裝入到暫存器值及記憶體映像,更新表格及佇列,這個過程是需要消耗時間的。

時間片時間設定: 時間片如果設定太短,會導致過多程式不斷切換,由於切換過程會產生上小文切換時間,所以降低CPU效率,設定太長,又會導致相對較短的互動請求響應變差,通常時間片設定在100ms左右比較合理。

1.3、程式和執行緒

1.3.1、什麼是程式?

程式是程式執行資源分配的最小單元

程式是作業系統進行資源分配和排程的獨立單元,資源包括CPU,記憶體空間,磁碟IO等等,同一個程式的所有執行緒共享該程式的全部資源程式與程式之間相互獨立

1.3.2、什麼是執行緒?

執行緒是CPU排程的最小單位,必須依賴程式而存在。

執行緒是程式的實體,是CPU排程和分派的基本單位,執行緒基本不擁有系統資源,但是擁有程式計數器、一組暫存器、棧等執行中不可少的資源,同一個程式中的執行緒共享程式所擁有的全部資源

1.4、 併發與並行

1.4.1、什麼是併發

併發是指一個時間段內,有幾個程式都在同一個CPU上執行,但任意一個時刻點上只有一個程式在處理機上執行。

多個執行緒 一個CPU

跟時間掛鉤,單位時間內。

1.4.2、什麼是並行

並行是指一個時間段內,有幾個程式都在幾個CPU上執行,任意一個時刻點上,有多個程式在同時執行,並且多道程式之間互不干擾。

多個執行緒 多個CPU

1.5、同步與非同步

1.5.1、什麼是同步

同步:在發出一個同步呼叫時,在沒有得到結果之前,該呼叫就不返回,直到結果的返回。

好比我給朋友打電話,你要不接電話,我就一直打,這個過程啥也不幹,就給你打電話,打到你接電話為止。

1.5.2、什麼是非同步

非同步:在發出一個非同步呼叫後,呼叫者不會立刻得到結果,該呼叫就返回了。

同樣打電話,我先給你發個訊息,告訴我有事找你,然後我就去幹我自己的事情去了,等你看到訊息給我回電話,當然,你也可以不回我電話。

二、多執行緒使用

2.1、建立多執行緒

2.1.1、實現Runnable介面
    public static class newRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("Runnable");
        }
    }
複製程式碼

呼叫:

new Thread(new newRunnable()).start();
複製程式碼
2.1.2、繼承Thread類
    public static class newThread extends Thread {
        @Override
        public void run() {
            super.run();
            System.out.println("newThread");
        }
    }
複製程式碼

呼叫:

new newThread().start();
複製程式碼

1、Thread 是java裡面對執行緒的抽象概念,我們通過new thread的時候,其實只是建立了一個thread例項,作業系統並沒有和該執行緒掛鉤,只有執行了start方法後,才是真正意義上啟動了執行緒。

2、start() 會讓一個執行緒進入就緒佇列等待分配CPU,分到CPU後才呼叫 run() 方法,

3、start() 方法不能重複呼叫,否則會丟擲 IllegalThreadStateException 異常。

2.1.3、實現Callable介面

Callable介面 是在Java1.5開始提供,可以在任務執行結束後提供返回值。

public interface Callable<V{
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */

    call() throws Exception;
}
複製程式碼

從原始碼可以看到,CallableRunnable 對比來看,不同點就在其call方法提供了返回值和進行異常丟擲

使用:

public static class newCallable implements Callable<String{
        @Override
        public String call() throws Exception {
            System.out.println("newCallable");
            Thread.sleep(3000);
            return "java1.5後提供,可在任務執行結束返回相應結果";
        }
    }
複製程式碼

對Callable的呼叫需要 FutureTask 這個類,這個類也是 Java1.5 以後提供

        FutureTask<String> futureTask = new FutureTask<>(new newCallable());
        futureTask.run();
        String result = null;
        try {
            result = futureTask.get();
        } catch (InterruptedException | ExecutionException e) {
            e.printStackTrace();
        }
複製程式碼

執行結果:

可以看到,我們通過 FutureTaskget 方法 ,get 方法會進行阻塞,直到任務結束後,才將返回值進行返回。

我們可以簡單看看 FutureTask ,它最終繼承至 Future 介面,FutureTask 除了上面說到的get方法,它還提供了以下這些方法,可以判斷任務是否完成,以及對任務進行取消操作。

  • 1、boolean cancel(boolean mayInterruptIfRunning)

嘗試取消當前任務的執行。如果任務已經取消、已經完成或者其他原因不能取消,嘗試將失敗。返回false.

如果任務還沒有啟動就呼叫了cancel(true),任務將永遠不會被執行。

如果任務已經啟動,會根據引數值決定任務是否應該中斷執行該任務的執行緒.

true: 嘗試中斷該任務,返回 True。
false: 不會中斷任務,繼續執行,返回 True。

  • 2、boolean isCancelled()
    如果任務在正常結束之前被被取消返回true

  • 3、boolean isDone()
    正常結束、異常或者被取消導致任務完成,將返回true

  • 4、V get()
    等待任務結束,然後獲取結果,如果任務在等待過程中被終端將丟擲InterruptedException,如果任務被取消將丟擲CancellationException,如果任務中執行過程中發生異常將丟擲ExecutionException。

  • 5、V get(long timeout, TimeUnit unit)
    任務最多在給定時間內完成並返回結果,如果沒有在給定時間內完成任務將丟擲TimeoutException。

2.2、終止執行緒

2.2.1、 自然終止

執行緒任務執行完成,則這個執行緒進行終止。

2.2.2、手動終止

暫停、恢復和停止操作對應線上程Thread的API就是suspend()、resume()和stop()。但是這些API是過期的,也就是不建議使用的,主要原因是方法的呼叫不能保證執行緒資源的正常釋放,容易引起其他副作用的產生。

suspend() :在呼叫後,執行緒不會釋放已經佔有的資源(比如鎖),而是佔有著資源進入睡眠狀態,這樣容易引發死鎖問題

stop() : 終結一個執行緒時不會保證執行緒的資源正常釋放,通常是沒有給予執行緒完成資源釋放工作的機會,因此會導致程式可能工作在不確定狀態下

真正安全的終止執行緒使用 interrupt() 方法

由於執行緒之間是協作式工作,所以在其他執行緒使用 interrupt() 終止某個執行緒時候,這個執行緒並不會立即終止,只是收到了終止通知,通過檢查自身的中斷標誌位是否為被置為 True 再進行相應的操作,當然,這個執行緒完全可以不用理會。

中斷標誌位的判斷:

1、isInterrupted()

判斷執行緒是否中斷,如果該執行緒已被中斷則返回 true ,否則返回 false ,該方法不更改中斷標誌位。

    /**
     * Tests whether this thread has been interrupted.  The <i>interrupted
     * status</i> of the thread is unaffected by this method.
     *
     * <p>A thread interruption ignored because a thread was not alive
     * at the time of the interrupt will be reflected by this method
     * returning false.
     *
     * @return  <code>true</code> if this thread has been interrupted;
     *          <code>false</code> otherwise.
     * @see     #interrupted()
     * @revised 6.0
     */

    public boolean isInterrupted() {
        return isInterrupted(false);
    }
複製程式碼

isInterrupted 示例:

private static class UseThread extends Thread{
        public UseThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName+" interrupt start flag  ="+isInterrupted());
            while(!isInterrupted()){
                System.out.println(threadName+" is running");
                System.out.println(threadName+" inner interrupt flag ="+isInterrupted());
            }
            System.out.println(threadName+" interrupt end flag ="+isInterrupted());
        }
    }
複製程式碼

執行上面的程式:

開啟執行緒,休眠一微秒後呼叫 interrupt() 進行中斷

    public static void main(String[] args) throws InterruptedException {
        Thread endThread = new UseThread("test isInterrupted");
        endThread.start();
        Thread.sleep(1);
        endThread.interrupt();
    }
複製程式碼

結果:

可以看到 UseThreadisInterrupted() 一直為 false ,當主執行緒執行 endThread.interrupt() 中斷方法後,其中斷標誌被置為 true ,跳出迴圈,結束 run 方法。我們後續再呼叫 isInterrupted() 方法列印中斷標誌的值一直為 true,並沒有更改。

2、interrupted()

判斷執行緒是否中斷,如果該執行緒已被中斷返回 true,狀態返回後該方法會清除中斷標誌位,重新置為 false,所以當第二次再次呼叫的時候獲取到的結果又會是 false ,(除非重新呼叫 interrupt()進行中斷 )

    /**
     * Tests whether the current thread has been interrupted.  The
     * <i>interrupted status</i> of the thread is cleared by this method.  In
     * other words, if this method were to be called twice in succession, the
     * second call would return false (unless the current thread were
     * interrupted again, after the first call had cleared its interrupted
     * status and before the second call had examined it).
     *
     * <p>A thread interruption ignored because a thread was not alive
     * at the time of the interrupt will be reflected by this method
     * returning false.
     *
     * @return  <code>true</code> if the current thread has been interrupted;
     *          <code>false</code> otherwise.
     * @see #isInterrupted()
     * @revised 6.0
     */

    public static boolean interrupted() {
        return currentThread().isInterrupted(true);
    }
複製程式碼

interrupted() 示例:

我們簡單改了一下程式碼:

 private static class UseThread extends Thread{
        public UseThread(String name) {
            super(name);
        }
        @Override
        public void run() {
            String threadName = Thread.currentThread().getName();

            while(!Thread.interrupted()){
                System.out.println(threadName+" is running");
            }
            System.out.println(threadName+" interrupted end flag ="+Thread.interrupted());
        }
    }
複製程式碼

可以看到,run 方法裡面一直迴圈執行,直到執行緒被中斷,結束後我們再次呼叫了列印了 Thread.interrupted()

呼叫:

    public static void main(String[] args) throws InterruptedException {
        Thread endThread = new UseThread("test interrupted");
        endThread.start();
        Thread.sleep(1);
        endThread.interrupt();
    }
複製程式碼

同樣休眠一微秒後進行中斷操作。

結果:

我們再分析一下,前面執行緒結束迴圈的條件是 Thread.interrupted()true , 但是當執行緒結束迴圈後,我們再次呼叫 Thread.interrupted() 方法,發現其值為又被置為 false ,說明 Thread.interrupted() 執行後,會清除中斷標誌位,並將其重新置為 false

注意:處於死鎖狀態的執行緒無法被中斷

阻塞狀態下的執行緒中斷

如果一個執行緒處於了阻塞狀態(如執行緒呼叫了thread.sleep、thread.join、thread.wait),則線上程在檢查中斷標示時如果發現中斷標示為true,則會在這些阻塞方法呼叫處丟擲InterruptedException異常,並且在丟擲異常後會立即將執行緒的中斷標示位清除,即重新設定為false。

我們在前面 isInterrupted 演示的示例中進行修改

 private static class UseThread extends Thread {
        public UseThread(String name{
            super(name);
        }

        @Override
        public void run(
{
            String threadName = Thread.currentThread().getName();
            System.out.println(threadName + " interrupt start flag  =" + isInterrupted());
            while (!isInterrupted()) {
                try {
                    // 執行緒進行休眠3秒
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    System.out.println("sleep  error=" + e.getLocalizedMessage());
                }
                System.out.println(threadName + " is running");
                System.out.println(threadName + " inner interrupt flag =" + isInterrupted());
            }
            System.out.println(threadName + " interrupt end flag =" + isInterrupted());
        }
    }
複製程式碼

我們再執行前面的方法的時候,結果:

我們可以看到,即使拋了異常,但是執行緒依舊在執行,所以我們要注意丟擲 InterruptedException 異常的時候會 清除中斷標誌位 的操作,所以我們改一下程式碼,在catch 中再次執行 interrupt()來中斷任務

 try {
       // 執行緒進行休眠3秒
       Thread.sleep(3000);
    } catch (InterruptedException e) {
      e.printStackTrace();
      // 在catch方法中,執行interrupt() 方法中斷任務。
      interrupt();
      System.out.println("sleep  error=" + e.getLocalizedMessage());
   }
複製程式碼

結果:

執行緒在異常丟擲之後,在catch中再次中斷,執行緒停止,不再執行。

三、執行緒之間共享和協作

3.1、 執行緒之間共享

前面說過,同一個程式的所有執行緒共享該程式的全部資源,共享資源就會導致一個問題,當多個執行緒同時訪問一個物件或者一個物件的成員變數,可能會導致資料不同步問題,即執行緒不安全操作,比如 執行緒A 對 資料a 進行操作,需要從記憶體中進行讀取然後進行相應的操作,操作完成後再寫入記憶體中,但是如果資料還沒有寫入記憶體中的時候,執行緒B 也來對這個資料進行操作,取到的就是還未寫入記憶體的資料,導致前後資料不同步。

為了處理這個問題,Java 中引入了關鍵字 synchronized ( 下一篇文章單獨講)。

3.2、執行緒之間的協作

執行緒之間可以相互配合,共同完成一項工作,比如執行緒A修改了某個值,這個時候需要通知另一個執行緒再執行後續操作,整個過程開始與一個執行緒,最終又再另一個執行緒執行,前者是生產者,後者就是消費者。

在 Android 中我們在進行網路請求的時候,往往會新開一個子執行緒來網路請求獲取資料,獲取到資料後,又會通過Handle來通知主執行緒進行UI更新,這個操作就體現了執行緒之間的協作關係。

3.2.1、 nitify()、notifyAll()、wait() 等待/通知機制

是指一個執行緒A呼叫了物件Owait() 方法進入等待狀態,而另一個執行緒B呼叫了物件Onotify()或者notifyAll()方法,執行緒A收到通知後從物件O的wait()方法返回,進而執行後續操作。上述兩個執行緒通過物件O來完成互動,而物件上的wait()notify、notifyAll()的關係就如同開關訊號一樣,用來完成等待方和通知方之間的互動工作。

我們知道 Object 類是所有類的父類,而 Object 類中就存在相關方法

notify():

通知一個在物件上等待的執行緒,使其從wait方法返回,而返回的前提是該執行緒獲取到了物件的鎖,沒有獲得鎖的執行緒重新進入WAITING狀態。

notifyAll():

通知所有等待在該物件上的執行緒

wait()

呼叫該方法的執行緒進入 WAITING狀態,只有等待另外執行緒的通知或被中斷才會返回.需要注意,呼叫wait()方法後,會釋放物件的鎖

wait(long)

超時等待一段時間,這裡的引數時間是毫秒,也就是等待長達n毫秒,如果沒有通知就超時返回

wait (long,int)

對於超時時間更細粒度的控制,可以達到納秒

下面通過案例說明,雙十一的時候,你購買了三件商品,你在家焦急的等待,沒事就重新整理一下手機看商品快遞資訊,我們就來模擬一個快遞資訊的更新,這裡以地點變化進行資料更新:

public class NwTest {
    // 發貨地點
    public String location = "重慶";
    // 所有貨物在不同一趟車上,貨物到了下一站,分別更新對應的快遞資訊
    public synchronized void changeLocationNotify(String location) {
        this.location = location;
        this.notify();
    }
    // 所有貨物在同一趟快遞車上,貨物到了下一站,全部資訊更新。
    public synchronized void changeLocationNotifyAll(String location) {
        this.location = location;
        System.out.println("changeLocationNotifyAll");
        this.notifyAll();
    }

    public static class LocationThread extends Thread {
        public final NwTest mNwTest;
        public LocationThread(NwTest nwTest) {
            this.mNwTest = nwTest;
        }

        @Override
        public void run() {
            super.run();
            try {
                synchronized (mNwTest) {
                    System.out.println("LocationThread  current location : " + mNwTest.location);    
                    // 等待位置更新
                    mNwTest.wait();
                    String name = Thread.currentThread().getName();
                    // 獲取當前商品的商家資訊
                    System.out.println("LocationThread——>current thread name : " + name);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 獲取更新後的位置
            System.out.println("LocationThread  update location : " + mNwTest.location);
        }
    }
複製程式碼

注意:

只能在同步方法或者同步塊中使用 wait()notifyAll()notify() 方法

否則會拋 IllegalMonitorStateException 異常

呼叫:

    public static void main(String[] args) throws InterruptedException {
        NwTest nwTest = new NwTest();
        for (int x = 0; x < 3; x++) {
            new LocationThread(nwTest).start();
        }
        // 模擬三天後
        Thread.sleep(3000);
        // 通知單個商品資訊進行更新
        nwTest.changeLocationNotify("合肥");
    }
複製程式碼

我們啟動了三個執行緒,模擬了你購買的三件貨物,如果使用 notify() ,只是使單個商品進行資訊更新(隨機喚醒)

結果:

我們看到三個貨物同時發貨,其中 Thread_0 最先到達合肥,並進行了資料更新。

如果使用 notifyAll() ,所有商品快遞資訊都會重新整理。

    public static void main(String[] args) throws InterruptedException {
        NwTest nwTest = new NwTest();
        for (int x = 0; x < 3; x++) {
            new LocationThread(nwTest).start();
        }
        Thread.sleep(3000);
        // 通知三件商品進行資訊更新
        nwTest.changeLocationNotifyAll("合肥");
    }
複製程式碼

結果:

這就是 notifyAll()notify()wait() 基本使用,其中 wait(long) 表示執行緒會等待 n 毫秒,如果這個時間段內沒有收到 notifyAll() 或者 notify() 就自動執行後續方法。

根據上面的Demo,我們可以整理一下 等待和通知的標準正規化

wait():

1)獲取物件的鎖。

2)根據判斷條件呼叫 wait() 方法。

3)條件滿足則執行對應的邏輯。

notify() 或者 notifyAll()

1)獲得物件的鎖。

2)改變條件,傳送通知。

3)通知所有等待在物件上的執行緒。

以上主要是整理的多執行緒的一些基本概念,還有 notify()和wait() 的基本使用,關鍵字 synchronized 準備下一篇單獨整理,後續計劃整理執行緒池相關知識以及Android 中 AsyncTask 的原始碼分析,喜歡的話點個讚唄!

相關文章