Java 併發編程基礎知識

Java 併發編程基礎知識

CPU 核心數和線程數的關係

目前的 CPU 有雙核,四核,八核,一般情況下,它和線程數是1:1的對應關係,也就是四核 CPU 一般就能並行執行 4 個線程。但 Intel 引入超線程技術後,使核心數與線程數形成1:2的關係,也就是我們常說的 4核8線程

線程調度與線程調度模型

任意時刻,只有一個線程佔用 CPU,處於運行狀態。而多線程併發執行就是輪流獲取 CPU 執行權。

  • 分時調用模型

輪流獲取 CPU 執行權,均分 CPU 執行時間。

  • 搶佔式調度模型

優先級高的線程優先獲取 CPU 執行權,這也是 JVM 採用的線程調度模型。

進程與線程

進程是程序運行資源分配的最小單位。

這些資源就包括 CPU,內存空間,磁盤 IO 等。同一進程中的多個線程共享該進程的所有資源,而不同進程是彼此獨立的。舉個栗子,在手機開啓一個 APP 實際上就是開啓了一個進程了,而每一個 APP 程序會有很多線程在跑,例如刷新 UI 的線程等,所以說進程是包含線程的。

線程是 CPU 調度的最小單位,必須依賴於進程而存在。

線程是比進程更小的,能獨立運行的基本單位,每一個線程都有一個程序計數器,虛擬機棧等,它可以與同一進程下的其它線程共享該進程的資源。

並行與併發

  • 並行

指應用能夠同時執行不同的任務。例如多輛汽車可以同時在同一條公路上的不同車道上並行通行。

  • 併發

指應用能夠交替執行不同的任務,因爲一般的計算機只有一個 CPU 也就是隻有一顆心,如果一個 CPU 要運行多個進程,那就需要使用到併發技術了,例如時間片輪轉進程調度算。比如單 CPU 核心下執行多線程任務時並非同時執行多個任務,而是以一個非常短的時間不斷地切換執行不同的任務,這個時間是我們無法察覺的出來的。

兩者的區別:並行是同時執行,併發是交替執行。

高併發編程的意義

  • 充分利用 CPU 資源

線程是 CPU 調度的最小的單位,我們的程序是跑在 CPU 的一個核中的某一個線程中的,如果在程序中只有一個線程,那麼對於雙核心4線程的CPU來說就要浪費了 3/4 的 CPU 性能了,所以在創建線程的時候需要合理的利用 CPU 資源,具體可以看看 AsyncTask 內部的線程池是如何設計的

  • 加快響應用戶的時間

如果多個任務時串行執行的話,那麼效果肯定不好,在移動端開發中,併發執行多個任務是很常見的操作,最常見的就是多線程下載了。

  • 可以使代碼模塊化,異步化,簡單化

在 Android 應用程序開發中的,一般 UI 線程負責去更新界面相關的工作,而一些 IO,網絡等操作一般會放在異步的工作線程去執行,這樣使得不同的線程各司其職,異步化。

線程之間的安全性問題

問題1:同一進程間的多個線程是可以共享該進程的資源的,當多個線程訪問共享變量時,就會線程安全問題。

問題2:爲了解決線程之間的同步問題,一般會引入鎖機制,對於線程之間搶奪鎖時也是有可能造成死鎖問題。

問題3:在 JVM 內存模型中,每一個線程都會分配一個虛擬機棧,這個虛擬機棧是需要佔用內存空間的,如果無限制的創建線程的話,會耗盡系統的內存。

線程的開啓與關閉

線程的啓動

  • 派生 Thread 類
//開啓一個線程
Thread thread = new Thread() {
    @Override
    public void run() {
        super.run();
        System.out.println("thread started");
    }
};
thread.start();
  • 實現 Runnable 接口,將其交給 Thread 類去執行
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("runnable run invoked");
    }
});
thread.start();
  • 實現 Callable 接口

因爲 Thread 構造中只接收 Runnable 類型的接口,需要實現將 Callable 的實現類包裝爲 FutureTask 之後交給 Thread 類去執行。

Callable<String> callable = new Callable<String>() {
    @Override
    public String call() throws Exception {
        Thread.sleep(1500);
        return "work done!";
    }
};

FutureTask<String> futureTask = new FutureTask(callable);
Thread thread = new Thread(futureTask);
thread.start();
try {
    //get() 是一個阻塞式的操作,一直等待 call 方法執行完畢。
    String resule = futureTask.get();
} catch (ExecutionException e) {
    e.printStackTrace();
}

再來看 FutureTask 的應用:我們觀察到 AsyncTask 內部就使用到了 FutureTask ,因爲在 doInBackground() 需要有一個返回值,而恰好 Callable 就可以實現子線程執行完有返回值。使用 FutureTask 來封裝 WorkRunnable 對象,然後再交給對應的線程池去執行。具體的代碼如下:

FutureTask

總結:對於第1,2兩種方式是在線程執行完畢後,無法得到執行的結果,而第三種方式是可以獲取執行結果的。

線程的終止

  • 線程自然終止

也就是 run 執行完畢,或者是內部出現一個異常導致線程提前結束。

  • 暴力終止

    • suspend() 使線程掛起,並且不會釋放線程佔有的資源(例如鎖),resume() 使掛起的線程恢復。

    • stop() 暴力停止,立刻釋放鎖,導致共享資源可能不同步。

以上幾個方法已經被 JDK 標記爲廢棄狀態。

  • interrupt() 安全終止

第一種情況:如果線程處於正常運行狀態,那麼線程的中斷狀態會被置爲 true ,並且線程還是會正常執行,僅此而已。

第二種情況:如果當前線程如果是處於阻塞狀態,例如調用了 wait,join,sleep 等方法,那麼則會拋出InterruptedException異常。

總結: interrupt() 並不能中斷線程,需要線程配合才能實現中斷操作。

示例01

public class EndThread extends Thread {

    @Override
    public void run() {
        super.run();

        System.out.println("isInterrupted:"+isInterrupted());
        while (true) {//儘管在其他線程中調用了 interrupt() 方法,但是線程並不會終止
            //while (!isInterrupted()){
            System.out.println("I am Thread body");
        }
//        System.out.println("isInterrupted:"+isInterrupted());
    }

    public static void main(String[] args) throws InterruptedException {

        final EndThread endThread = new EndThread();

        endThread.start();

        Thread.sleep(10);
        //在其他線程中去調用線程的interrupt方法給線程打一個終止的標記
        endThread.interrupt();
    }
}

上面的代碼時在線程體中執行一個 while(true)的死循環,然後在其他線程中調用 endThread.interrupt() 觀察當前線程是否會執行完畢。

經測試:在調用線程的 interrupt()方法之後,while(true)是不會結束循環的,也就是線程還是一直在運行着。所以說 interrupt() 並不會應用 run 方法的執行

示例02

下面再來看看另一個關於 interrupt 方法的使用

在線程體內部 sleep(2000) 並且 try catch 對應的 InterruptedException 異常,如果在其他線程調用了 endThread.interrupt() 那麼此處就會拋出 InterruptedException 異常,並且會isInterrupted() 會返回 false 。

public class EndThread2 extends Thread {
    @Override
    public void run() {
        super.run();

        System.out.println("isInterrupted:" + isInterrupted());
        try {
            //在其他線程調用 endThread.interrupt() 之後,會拋出 InterruptedException 異常並且線程的
            // isInterrupted 會被標記爲 false。因此最後輸出的結果還是 false
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
            System.out.println("catch InterruptedException");
        }
        System.out.println("isInterrupted:" + isInterrupted());
    }

    public static void main(String[] args) throws InterruptedException {

        final EndThread2 endThread = new EndThread2();

        endThread.start();

        Thread.sleep(300);
        //在其他線程中去調用線程的interrupt方法給線程打一個終止的標記
        endThread.interrupt();

    }
}

執行結果:

isInterrupted:false
java.lang.InterruptedException: sleep interrupted
	at java.lang.Thread.sleep(Native Method)
	at endthread.EndThread.run(EndThread.java:28)
catch InterruptedException
isInterrupted:false

示例03

示例01中的 while(true) 修改爲 while(!isInterrupted()){},在外界調用了 endThread.interrupt()之後,線程的 isInterrupted() 就會返回 true,標記着你可以結束線程了。

public class EndThread extends Thread {

    @Override
    public void run() {
        super.run();

        System.out.println("isInterrupted:"+isInterrupted());
       while (!isInterrupted()){
            System.out.println("I am Thread body");
        }
       System.out.println("isInterrupted:"+isInterrupted());
    }

    public static void main(String[] args) throws InterruptedException {

        final EndThread endThread = new EndThread();

        endThread.start();

        Thread.sleep(10);
        //在其他線程中去調用線程的interrupt方法給線程打一個終止的標記
        endThread.interrupt();
     )
    }
}

示例04

還有一種方式是設置一個 boolean 類型的變量 mIsExit 標記,當線程體內部判斷到 mIsExit 爲 false 那麼就跳出循環。具體示例代碼如下:

public class EndThread extends Thread {
    //這個變量需要在其他線程中判斷,因此需要設置爲線程可見的
    private volatile boolean mIsExit = true;
    @Override
    public void run() {
        super.run();
        while(mIsExit){
            System.out.println("I am Thread body");
        }
    }

    public static void main(String[] args) throws InterruptedException {

        final EndThread endThread = new EndThread();

        endThread.start();

        Thread.sleep(3);
        //設置標記爲退出狀態
        endThread.mIsExit = false;
    }
}

線程其他 API

Thread#start() 與 Thread#run()

start() 方法調用之後會讓一個線程進入就緒等待隊列,當獲取到 CPU 執行權之後會執行線程體 run()方法。

run() 方法只是 Thread 類中一個普通方法,如果手動去調用,跟調用普通方法沒有什麼區別。

Thread#run() 與 Runnable#run()

在 Java 中只有 Thread 才能表示一個線程,而 Runnable 只能表示一個任務,任務是需要交給線程去執行的,當出現如下代碼時,你看到可能會懵逼,執行結果到底是什麼?

//接受一個 runnable 接口
Thread thread = new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("runnable run invoked");
    }
}) {
    @Override
    public void run() {
        super.run();
        System.out.println("thread started");
    }
};
thread.start();

以上方式的輸出結果是:
如果線程 run 方法內部調用了 super.run() 那麼輸出結果如下:

runnable run invoked
thread started

如果線程 run 方法內部不調用 super.run() 那麼輸出結果如下:

thread started

我們可以通過源碼來解答這個問題:在創建線程時,如果往構造函數中傳入一個 Runnable 對象,那麼它會給線程 target 屬性賦值,並且在線程體執行時先判斷 target 是否爲空,不爲空,則先執行 Runnablerun 方法,再執行當前線程體的子類中的 run 方法。

//Thread.java
private Runnable target;

public Thread(ThreadGroup group, Runnable target, String name,
              long stackSize) {
    init(group, target, name, stackSize);
}

@Override
public void run() {
    if (target != null) {
        //如果傳入的 Runnable 實例,那麼會調用調用 runnable 實例的 run 方法
        target.run();
    }
}

yield()

Java線程中的 Thread.yield() 方法,譯爲線程讓步。顧名思義,就是說當一個線程使用了這個方法之後,它會放棄自己CPU執行權,讓自己或者其它的線程運行,注意是讓自己或者其他線程運行,並不是單純的讓給其他線程。yield()的作用是讓步。它能讓當前線程由“運行狀態”進入到“就緒狀態”,從而讓其它具有相同優先級的等待線程獲取執行權;但是,並不能保證在當前線程調用yield()`之後,其它具有相同優先級的線程就一定能獲得執行權;也有可能是當前線程又進入到“運行狀態”繼續運行!

public class YieldDemo {

    public static void main(String[] args) {
        Yieldtask yieldtask = new Yieldtask();

        Thread t1 = new Thread(yieldtask, "A");
        Thread t2 = new Thread(yieldtask, "B");
        Thread t3 = new Thread(yieldtask, "C");
        Thread t4 = new Thread(yieldtask, "D");

        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }

    public static class Yieldtask implements Runnable {


        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + "-" + i);

                if (i == 5) {
                    Thread.yield();
                }
            }
        }
    }
}

從運行結果可以看到,調用 yield() 方法的線程之後,CPU 執行權不一定會給其他線程搶到,有可能還是當前線程搶到 CPU 執行權。

...
A-0
D-4
D-5
D-6//在這裏,還是執行 D 這個線程
D-7
D-8
D-9
B-2
B-3
B-4
B-5
C-6
B-6
A-1
A-2
A-3
A-4
...

join()

join() 把指定的線程加入到當前線程,可以將兩個交替執行的線程合併爲順序執行的線程。比如在線程B中調用了線程A的Join()方法,直到線程A執行完畢後,纔會繼續執行線程B。

這裏舉一個栗子:午飯時間到了,老王(線程A)調用了微波爐熱飯的方法,預約了4分鐘,當微波爐跑了2分鐘,這時老王看到一個美女(線程B)過來,這時主動調用了線程B.join()方法,此時把微波爐讓給了美女,這時老王就等待美女熱飯,直到熱好美女的飯之後,才輪到老王去繼續熱飯,這就是一個 join() 方法的作用。

下面畫了一個草圖:

join

public class JoinDemo implements Runnable {

    public static void main(String[] args) throws InterruptedException, ExecutionException {
        JoinDemo joinDemo = new JoinDemo();
        Thread thread = new Thread(joinDemo);
        thread.start();
        //join() 會阻塞當前線程,等待子線程執行完畢
        //在這裏主線程會等待子線程執行完畢之後才能往下執行。
        thread.join();
        System.out.println(Thread.currentThread().getName() + "  " + " done!");
    }

    @Override
    public void run() {
        try {
            Thread.sleep(2000);
            System.out.println(Thread.currentThread().getName()+" done!");        
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

執行結果:
Thread-0 done!
main   done!

線程狀態

在 Java Thread 類中定義了一個 State 枚舉,內部定義以下6個線程狀態。

Thread 狀態

在線程的生命週期中,它要經過新建(New)、就緒(Runnable)、運行(Running)、阻塞(TIME_WAITING,WAITING)和死亡(TERMINATED)五種狀態。

我這裏繪製一個草圖,描述了各個狀態之前的切換:

線程狀態切換圖解.png

  • 新建狀態(NEW)

新建一個線程對象,此時並沒有執行 start() 方法,這時的線程狀態就是處於新建狀態。

Thread thread = new Thread(){
    public void run() {...}
};
  • 就緒狀態(RUNNABLE)

start() 方法調用之後會讓一個線程進入就緒等待隊列,JVM 會爲其創建對應的虛擬機棧,本地方法棧和程序計數器。處於這個狀態的線程還沒開始運行,需要等待 CPU 的執行權。

  • 運行狀態(RUNNING)

處於就緒狀態的線程在搶到 CPU 執行權之後,就處於運行狀態,執行該線程的 run 方法。

  • 阻塞狀態(BLOCKED)

TIME_WAIT/WAIT:

運行的線程執行 wait() /wait(long),join()/join(long)方法,那麼這些線程放棄 CPU 執行和線程持有的鎖,並且 JVM 會將這些線程放入到等待池中。

BLOCKED:

運行時的線程在獲取同步鎖時,發現鎖被其他線程持有,這時 JVM 會將當前線程放入鎖池中。

  • 結束(TERMINATED)

線程 run 方法執行完畢,或者線程被異常終止。

總結

以上是關於 Java 多線程的一些基本知識點的總結,有任何不對的地方,請多多指正。

記錄於2019年4月7日

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章