多執行緒程式設計基礎(一)-- 執行緒的使用

ClericYi發表於2020-02-21

多執行緒程式設計基礎(一)-- 執行緒的使用

前言

在面試過程中,多執行緒程式設計也是一個必考的知識點。就像是面試時問你,sleep()wait()兩個函式有什麼區別一樣。

思考

  1. 什麼是多執行緒程式設計?
  2. 為什麼要多執行緒程式設計?
  3. 如何多執行緒程式設計?

基礎知識

閱讀本文的讀者,應該都上過作業系統的課程。而程式和執行緒就是作業系統的知識點之一。就如同現代的pc一般,從18年開始基礎款筆記本基本上都已經使用上了四核八執行緒的晶片了。但是和這個實際意義上的晶片不同,我們講的程式設計中使用到的執行緒是需要類似對映的過程慢慢的轉換然後交給晶片處理的。

先上一張圖,讓讀者清晰的瞭解什麼叫做程式,什麼叫做執行緒。

活動監視器

這是Mac的活動監視器,Windows要訪問的話開啟工作管理員即可。從圖就很清晰的看出來什麼是程式了。而執行緒包含在程式中。

用概念型的語言描述。

  1. 程式:程式在一個資料集合上的執行過程,是執行緒的容器。
  2. 執行緒:輕量級程式。

從定義中也算很明顯的能夠感知了,為什麼我們的程式設計要叫多執行緒,而不叫做多程式。因為它輕啊,朋友們。

下面再送讀者們兩張圖片。

程式狀態轉化圖

程式轉化圖中標示了4個值用一句話來記憶比較有效。 程式因建立而產生,因排程而執行,因得不到資源而阻塞,因得不到資源而阻塞,因撤銷而消亡。 圖中代表的4個值: (1) 得到CPU的時間片 / 排程。 (2) 時間片用完,等待下一個時間片。 (3) 等待 I/O 操作 / 等待事件發生。 (4) I/O操作結束 / 事件完成。

雖然圖示和需要記憶的句子其實相同,只是省去了建立和消亡的狀態。

執行緒狀態轉化圖

這張圖其實對應的是Java執行緒執行時宣告的6種狀態,狀態轉化以及呼叫到的函式,也都清晰的呈現在圖中了。

那為什麼要多執行緒程式設計? 其實他包攬了很多事情,就拿知乎這個app舉例子,我們之前講過UI執行緒,也就是一般我們寫程式時的main()函式。他如果只請求一個列表資訊,那我們的UI執行緒可能還忙的過來,但是他如果要同時請求使用者資訊,多個列表資訊呢?他要忙到猴年馬月,這就是執行緒的作用,每個非同步處理一個任務,然後返回結果,就比較成功的完成了這個任務。

如何多程式程式設計

先寫兩種非常簡單的執行緒使用方法。

// 1
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("this is a Runnable");
    }
}
// 2
public class MyThread extends Thread {
    @Override
    public void run() {
        super.run();
        System.out.println("this is thread");
    }
}

// 具體使用
public class Main {
    public static void main(String[] args) {
        // 第一種
        Thread thread1 = new Thread(new MyRunnable());
        thread1.start();
        // 第二種
        MyThread thread2 = new MyThread();
        thread2.start();
    }
}
複製程式碼

兩種方法效果相同,不過一般來說推薦的是使用第一種,也就是重寫Runnable

多執行緒固然好,但是如果出現下面的情況,你還會願意繼續多執行緒程式設計嗎?

public class Main {
    public int i = 0;
    public void increase(){
        I++;
    }

    public static void main(String[] args) {
        final Main main = new Main();
        for(int i=0; i< 10; i++){
            new Thread(new Runnable() {
                @Override
                public void run() {
                    for(int j=0; j<1000; j++){
                        main.increase();
                    }
                }
            }).start();
        }
        while(Thread.activeCount() > 2){
            Thread.yield();
        }
        System.out.println(main.i);
    }
}
複製程式碼

多執行緒程式設計基礎(一)-- 執行緒的使用

上述結果只是通過一個自加操作得出的結果,因為兩個執行緒互不干擾,但是當他們同時對一個公共資料進行操作的時候,就會出現讀髒資料等現象,這個時候我們就需要引入同步機制。

同步

一般情況下,我們會通過兩種方式來實現。

  1. synchronized
  2. Lock

在作業系統中,有這麼一個概念,叫做臨界區。其實就是同一時間只能允許存在一個任務訪問的程式碼區間。程式碼模版如下:

Lock lock = new ReentrantLock();
public void lockModel(){
    lock.lock();
    // 用於書寫共同程式碼,比如說賣同一輛動車的車票等等。

    lock.unlock();
}

// 上述模版等價於下面的函式
public synchronized void lockModel(){}
複製程式碼

其實這就是大家常說的鎖機制,通過加鎖和解鎖的方法,來保證資料的正確性。

但是鎖的開銷還是我們需要考慮的範疇,在不太必要時,我們更長會使用是volatile關鍵詞來修飾變數,來保證資料的準確性。

Java記憶體模型

對上述的共享變數記憶體而言,如果執行緒A和B之間要通訊,則必須先更新主記憶體中的共享變數,然後由另外一個執行緒去主記憶體中去讀取。但是普通變數一般是不可見的。而volatile關鍵詞就將這件事情變成了可能。 打個比方,共享變數如果使用了volatile關鍵詞,這個時候執行緒B改變了共享變數副本,執行緒A就能夠感知到,然後經歷上述的通訊步驟。 這個時候就保障了可見性。 但是另外兩種特性,也就是有序性和原子性中,原子性是無法保障的。 拿最開始的Main的類做例子,就只改變一個變數。

public volatile int i = 0;
複製程式碼

多執行緒程式設計基礎(一)-- 執行緒的使用

和上面的程式碼一樣,每次執行的結果都不會相同。

所以一般關於volatile有以下兩種用法。

  1. 狀態標誌 因為只會使用truefalse,自然也就滿足了原子操作。
volatile boolean flag = false;
void doSomething(){
    flag = true;
}

void check(){
    if(flag){
        // ···········
    }
}
複製程式碼
  1. 雙重檢查模式 / DCL

詳見設計模式(二)

以上就是我的學習成果,如果有什麼我沒有思考到的地方或是文章記憶體在錯誤,歡迎與我分享。


相關文章推薦:

多執行緒程式設計基礎(二)-- 執行緒池的使用

JVM必備基礎知識(一) -- 類的載入機制

聊一聊設計模式(一)-- 六大原則

相關文章