Java併發程式設計知識概覽(一)

pjmike_pj發表於2018-11-20

原文部落格地址:pjmike的部落格

程式與執行緒

下面比較簡單介紹下程式與執行緒的概念:

程式

關於程式的定義,其實有很多:

  • 一個正在執行的程式
  • 計算機中正在執行的程式的一個例項
  • 可以分配給處理器並由處理器執行的一個例項。

個人覺得比較好的定義是:

程式是具有一定獨立功能的程式關於某個資料集合上的一次執行過程

說白了,程式就是CPU執行的一次任務,在單個CPU中一次只能執行一次任務,即CPU總是執行一個程式,其他程式處於非執行狀態。但是現在的作業系統都是多核CPU,所以可以同時執行多個程式,執行多個任務。

執行緒

執行緒實際上是一個程式中的"子任務",在一個程式中可以建立多個執行緒,打個比方,開啟QQ就是執行了一個程式,而在QQ裡與他人聊天,同時下載檔案等操作就是執行緒。

為什麼要使用多執行緒

多執行緒是指作業系統在單個程式內支援多個併發執行路徑的能力。每個程式中只有一個執行緒在執行的傳統方法稱為單執行緒方法。但是單執行緒存在很多弊端,它並不能充分利用CPU資源,而且單執行緒在應對複雜業務時響應時間也是較差的,所以我們需要使用多執行緒來幫助我們。使用多執行緒有如下好處(原因):

  • 更多的處理器核心:多執行緒可以使用多個處理器資源,提升程式的執行效率
  • 更快的響應時間 :在複雜業務中,使用多執行緒可以縮短響應時間,提升使用者體驗
  • 更好的程式設計模型 :在Java開發中,多執行緒程式設計提供了良好,考究並且一致的程式設計模型。

那我們為什麼去使用 多執行緒而不是使用多程式去進行併發程式的設計,是因為執行緒間的切換和排程的成本遠小於 程式

執行緒的狀態

首先有程式,其次才是執行緒,其實執行緒的生命週期及各種狀態轉換和程式類似,下面看一張 程式的狀態轉換圖(圖片摘自網路):

progress

程式一般有三種基本的狀態:

  • 執行態:程式正在執行
  • 就緒態:程式做好了準備,有機會就開始執行
  • 阻塞態:程式等待某一事件而停止執行

程式被建立後,加入就緒佇列等待被排程執行,CPU排程器根據一定的排程演算法排程就緒佇列中的程式在處理機上執行。

上面簡述了程式的基本狀態及變化過程,執行緒也是類似的:

java_thread

該圖表示一個執行緒的生命週期狀態流轉圖,很清楚的描繪了一個執行緒從建立到終止的一個過程。執行緒的所有狀態在 Thread類中的State列舉定義,如下:

public enum State {

    NEW,
    
    RUNNABLE,

    BLOCKED,

    WAITING,

    TIMED_WAITING,

    TERMINATED;
}
複製程式碼
  • New: 表示剛剛建立的執行緒,這種執行緒還沒執行
  • Running: 表示執行緒已經呼叫 start()方法,執行緒只在執行
  • Blocked: 表示執行緒阻塞,等待獲取鎖,如碰到 synchronized同步塊,一旦獲取鎖就進入 Running 狀態繼續執行
  • Waiting: 表示執行緒進入一個無時間限制的等待,等待一些特殊的事件來喚醒,比如通過 wait() 方法等待的執行緒在等待notify()方法,而通過 join() 方法等待的執行緒則會等待目標執行緒的終止。一旦等到了期望了事件,執行緒會再次執行,進入Running狀態
  • Timed Waiting: 表示進行一個有時限的等待,如sleep(3000)等待(睡眠)3秒後,重新進入Running狀態繼續執行
  • Terminated: 表示執行緒執行結束,進入終止狀態

注意:從New 狀態出發後,執行緒不能再回到 New狀態,同理,處於 Terminated 的執行緒也不能回到 Running狀態

下面分別對執行緒的各種狀態進行相關說明

執行緒的相關操作

新建執行緒

新建執行緒有兩種方式:

  • 繼承Thread類
  • 實現Runnable介面

Thread類是在java.lang包中定義的,本來我們可以直接繼承Thread,過載 run() 方法來自定義執行緒,但是Java 是單繼承的,如果一個類已經繼承了其他類,就無法再繼承Thread,所以我們這時就可以實現 Runnable介面來實現

// 1. 繼承Thread類
public class Thread1 extends Thread{
    @Override
    public void run() {
        System.out.println("hello world");
    }

    public static void main(String[] args) {
        Thread1 thread1 = new Thread1();
        //呼叫start()執行執行緒,執行內部的run()方法
        thread1.start();
    }
}

//2. 實現Runnable介面
public class Thread2 implements Runnable{

    @Override
    public void run() {
        System.out.println("hello world");
    }

    public static void main(String[] args) {
        Thread thread = new Thread(new Thread2());
        thread.start();
    }
}
複製程式碼

Thread有一個重要的構造方法:

public Thread(Runnable target)
複製程式碼

它傳入一個Runnable 介面的例項,在start()方法呼叫時,新的執行緒就會執行 Runnable.run()方法,實際上,預設的Thread.run()就是這麼做的:

public void run() {
    if (target != null) {
        target.run();
    }
}
複製程式碼

預設的Thread.run()就是直接呼叫 內部的 Runnable介面

終止執行緒

一般來說,執行緒在執行完畢後就會結束,無須手動關閉,但是還是有些後臺常駐執行緒可能不會自動關閉。

Thread提供了一個 stop()方法,該方法可以立即將一個執行緒終止,但是目前stop()已經被廢棄,不推薦使用,原因是 stop()方法太過於簡單粗暴,強行把執行到一半的執行緒終止,可能會引起一些資料不一致的問題。

執行緒中斷

執行緒中斷是一種重要的執行緒協作機制,它並不會使執行緒立即退出,而是給執行緒傳送一個通知,告知目標執行緒,有人希望你退出。至於目標執行緒接到通知後如何處理,則完全由目標執行緒自行決定。

與執行緒中斷有關的,有三個方法:

public void Thread.interrupt()  //中斷執行緒
public boolean Thread.isInterrupted()  //判斷是否被中斷
public static boolean Thread.interrupted()  //判斷是否被中斷,並清除當前中斷狀態
複製程式碼
  • Thread.interrupt()方法是一個例項方法,通知目標執行緒中斷,設定中斷標誌位,該標誌位表明當前執行緒已經被中斷了
  • Thread.isInterrupted()方法也是例項方法,判斷當前執行緒是否有被中斷(通過檢查中斷標誌位)
  • 靜態方法Thread.interrupted也是用來判斷當前執行緒的中斷狀態的,但同時會清楚當前執行緒的中斷標誌位狀態

舉例說明:

public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            while (true) {
                //通過中斷標誌位判斷是否被中斷
                if (Thread.currentThread().isInterrupted()) {
                    //中斷邏輯處理
                    System.out.println("Interrupted!");
                    // break退出迴圈
                    break;
                }
            }
        });
        //開啟執行緒
        thread.start();
        Thread.sleep(2000);
        //設定中斷標誌位
        thread.interrupt();
    }
複製程式碼

Thread有個 sleep方法,它會讓當前執行緒休眠若干時間,它會丟擲一個 InterruptedException中斷異常。InterruptedException不是執行時異常,也就是說程式必須捕獲並且處理它,當執行緒在 sleep()休眠時,如果被中斷,這個異常就產生了。

public static void main(String[] args) throws InterruptedException {
    Thread thread = new Thread() {
        @Override
        public void run() {
            while (true) {
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    System.out.println("Interrupted When Sleep");
                    //中斷異常會清楚中斷標記,重新設定中斷狀態
                    Thread.currentThread().interrupt();
                }
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("Interrupted!");
                    break;
                }
            }
        }
    };
    thread.start();
    Thread.sleep(2000);
    thread.interrupt();
}

//output:

Interrupted When Sleep
Interrupted!
複製程式碼

Thread.sleep() 方法由於中斷而丟擲異常,此時,它會清楚中斷標記,如果不加處理,那麼在下一次迴圈開始時,就無法捕獲這個中斷,故在異常處理中,再次設定中斷標誌位

等待(wait) 和通知(notify)

為了支援多執行緒之間的協作,JDK 提供了兩個非常重要的介面執行緒等待 wait() 方法和通知 notify() 方法,這兩個方法定義在 Object類中。

public final void wait(long timeout) throws InterruptedException;

public final void notify();
複製程式碼

當在一個物件例項上呼叫了object.wait() 方法,那麼它就會進入object物件的等待佇列進行等待,這個等待佇列中,可能會有多個執行緒,因為系統執行多個執行緒同時等待某一個物件。當object.notify()被呼叫時,它就會從 這個等待佇列中,隨機選擇一個執行緒,並將其喚醒

除了notify()方法外,Object 物件還有一個類似的 notifyAll()方法 ,它和notify()的功能一樣,不同的是,它會喚醒這個等待佇列中所有等待的執行緒

下面看一個程式碼示例:

public class Example {
    public static void main(String[] args) {
        final Object object = new Object();
        new Thread(() -> {
            System.out.println("thread A is waiting to get lock");
            synchronized (object) {
                System.out.println("thread A already get lock");
                try {
                    TimeUnit.SECONDS.sleep(1);
                    System.out.println("thread A do wait method");
                    object.wait();
                    System.out.println("thread A wait end");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

        new Thread(() -> {
            System.out.println("thread B is waiting to get lock");
            synchronized (object) {
                System.out.println("thread B already get lock");
                try {
                    TimeUnit.SECONDS.sleep(5);
                    System.out.println("thread B do wait method");
                    object.notify();
                    System.out.println("thread B do notify method");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
    }
}

複製程式碼

執行結果:

thread A is waiting to get lock
thread A already get lock
thread B is waiting to get lock
thread A do wait method
thread B already get lock
thread B do wait method
thread B do notify method
thread A wait end
複製程式碼

上述開啟了兩個執行緒A和B,A執行 wait()方法前,A先申請 object 的物件鎖,在執行 object.wait()時,持有object的鎖,執行後,A會進入等待,並釋放 object的鎖。

B在執行notify()之前也會先獲得 object的物件鎖,執行notify()之後,釋放object的鎖,然後A重新獲得鎖後繼續執行

等待執行緒結束(join) 和 謙讓(join)

join方法使當前執行緒等待呼叫 join方法的執行緒結束後才能繼續往下執行,下面是程式碼示例:

public class ThreadExample {
    public volatile static int i = 0;
    public static class Thread_ extends Thread{
        @Override
        public void run() {
            for (i = 0; i < 1000000; i++) {

            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Thread_ A = new Thread_();
        A.start();
        //讓當前的主執行緒等待thread_執行緒執行完畢後才往下執行
        A.join();
        System.out.println(i);
    }
}

複製程式碼

join()的本質實際上是讓呼叫執行緒wait()在當前執行緒物件例項上,當執行緒執行完畢後,被等待執行緒退出前呼叫 notifyAll()通知所有的等待執行緒繼續執行

以上面的例子說明,執行緒A呼叫join()使main執行緒 wait()在A物件例項上,等執行緒A執行完畢,執行緒A呼叫notifyAll(),main執行緒等到通知繼續往下執行。

而Thread.yield()方法的定義如下:

public static native void yield();
複製程式碼

它是靜態方法,一旦執行,它會使當前呼叫該方法的執行緒讓出CPU,但是讓出CPU不代表 當前執行緒不執行,當前執行緒讓出CPU後,還會進行CPU資源的爭奪,但是是否能夠被分配到,就不一定了。

Thread.yield()的呼叫就好像在說:

我已經完成了一些最重要的工作了,我應該是可以休息一下了,可以給其他執行緒一些工作機會了

守護執行緒(Daemon)

守護執行緒是一種特殊的下執行緒,它是系統的守護者,在後臺默默地完成一些系統性的服務,比如垃圾回收執行緒,JIT執行緒就可以理解為守護執行緒,與之對應的是使用者執行緒,使用者執行緒可以理解為系統的工作執行緒,它會完成這個程式應該要完成的業務操作。當一個Java應用內,只有守護執行緒時,Java虛擬機器就會自然退出。

程式碼示例如下:

public class DaemonDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread() {
            @Override
            public void run() {
                while (true) {
                    System.out.println("I am alive");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        //將執行緒t設定為守護執行緒
        t.setDaemon(true);
        t.start();
        //主執行緒休眠10s
        Thread.sleep(10000);
    }
}
複製程式碼

輸出結果:

I am alive
I am alive
I am alive
I am alive
I am alive
I am alive
I am alive
I am alive
I am alive
I am alive
複製程式碼

上面的例子,將t設定為守護執行緒,系統中只有主執行緒 main 為使用者執行緒,在 main休眠10s後,守護執行緒退出,整個執行緒也退出。

執行緒優先順序

Java 中的執行緒可以有自己的優先順序,優先順序高的執行緒在競爭資源時會更有優勢,更可能搶佔資源。執行緒的優先順序排程和作業系統密切相關,當然高優先順序可能也會有搶佔失敗的時候,但是大部分情況下,高優先順序比低優先順序對於搶佔資源來說更有優勢。

程式碼示例如下:

public class PriorityDemo {
    private static int count = 0;
    public static void main(String[] args) {
        Thread A = new Thread() {
            @Override
            public void run() {
                while (true) {
                    synchronized (PriorityDemo.class) {
                        count++;
                        if (count > 100000) {
                            System.out.println("HighPriority is complete");
                            break;
                        }
                    }
                }
            }
        };
        Thread B = new Thread() {
            @Override
            public void run() {
                while (true) {
                    synchronized (PriorityDemo.class) {
                        count++;
                        if (count > 100000) {
                            System.out.println("LowPriority is complete");
                            break;
                        }
                    }
                }
            }
        };
        //設定執行緒A為高優先順序
        A.setPriority(Thread.MAX_PRIORITY);
        //設定執行緒B為低優先順序
        B.setPriority(Thread.MIN_PRIORITY);
        //啟動執行緒B
        B.start();
        //啟動執行緒A
        A.start();
    }
}
複製程式碼

執行結果:

HighPriority is complete
LowPriority is complete
複製程式碼

由上可以初步得出,高優先順序的執行緒在大部分情況下會首先完成任務。

總結

上面簡單總結了一部分Java併發程式設計所涉及到知識點,算是入門Java併發的一個開端。

參考資料 & 鳴謝

相關文章