並發編程(概念簡述)

並發編程(概念簡述)

1 進程與執行緒

1.1 概念

1.1.1 執行緒

  • 程式由指令和數據組成,但這些指令要運行,數據要讀寫,就必須將指令載入至 CPU,數據載入至記憶體。在 指令運行過程中還需要用到磁碟、網路等設備。進程就是用來載入指令、管理記憶體、管理 IO 的
  • 當一個程式被運行,從磁碟載入這個程式的程式碼至記憶體,這時就開啟了一個進程。
  • 進程就可以視為程式的一個實例。大部分程式可以同時運行多個實例進程(例如記事本、畫圖、瀏覽器 等),也有的程式只能啟動一個實例進程(例如網易雲音樂、360 安全衛士等)

1.1.2 進程

  • 一個進程之內可以分為一到多個執行緒。(一個進程內包含多個執行緒)
  • 一個執行緒就是一個指令流,將指令流中的一條條指令以一定的順序交給 CPU 執行
  • Java 中,執行緒作為最小調度單位,進程作為資源分配的最小單位。 在 windows 中進程是不活動的,只是作 為執行緒的容器

1.2 二者對比

  • 進程基本上相互獨立的,而執行緒存在於進程內,是進程的一個子集
  • 進程擁有共享的資源,如記憶體空間等,供其內部的執行緒共享
  • 進程間通訊較為複雜
    • 同一台電腦的進程通訊稱為 IPC(Inter-process communication)
    • 不同電腦之間的進程通訊,需要通過網路,並遵守共同的協議,例如 HTTP
  • 執行緒通訊相對簡單,因為它們共享進程內的記憶體,一個例子是多個執行緒可以訪問同一個共享變數
  • 執行緒更輕量,執行緒上下文切換成本一般上要比進程上下文切換低

2 並行與並發

2.1 並發

單核 cpu 下,執行緒實際還是串列執行的。作業系統中有一個組件叫做任務調度器,將 cpu 的時間片(windows 下時間片最小約為 15 毫秒)分給不同的程式使用,只是由於 cpu 在執行緒間(時間片很短)的切換非常快,人類感覺是同時運行的 。總結為一句話就是: 微觀串列,宏觀並行 , 一般會將這種 執行緒輪流使用 CPU 的做法稱為並發:concurrent

每個時間段,只能處理一個執行緒

image-20220813084446988

2.2 並行

前提:是在多核CPU下才存在並行

  • 下圖是cpu在兩個核心下,同一時間處理執行緒的能力。

image-20220813084635009

2.3 兩者對比

  • 並發(concurrent)是同一時間應對(dealing with)多件事情的能力
  • 並行(parallel)是同一時間動手做(doing)多件事情的能力
  • 舉例
    • 家庭主婦做飯、打掃衛生、給孩子餵奶,她一個人輪流交替做這多件事,這時就是並發
    • 家庭主婦雇了個保姆,她們一起這些事,這時既有並發,也有並行(這時會產生競爭,例如鍋只有一口,一 個人用鍋時,另一個人就得等待)
    • 雇了3個保姆,一個專做飯、一個專打掃衛生、一個專餵奶,互不干擾,這時是並行

3 應用

3.1 同步調用

3.1.1 測試程式碼

import lombok.extern.slf4j.Slf4j;

/**
 * @author : look-word
 * 2022-08-13 08:58
 **/
@Slf4j
public class SynchronousTest {
    public static void main(String[] args) {
        readFile("xxx.text");
        log.debug("do other things ....");
    }
    // 模擬讀取文件
    private static void readFile(String s) {
        log.info("{}開始讀取", s);
        long beginTime = System.currentTimeMillis();
        try {
            Thread.sleep(1789);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("讀取時長{} s", System.currentTimeMillis() - beginTime);
    }
}

不難發現,我們的程式是同步執行的,等待讀取文件的完成,再從上往下依次執行。

  • 缺點:必須要等待讀取操作的完成,假設讀取花費5秒,這五秒cpu什麼也美干,就乾等著。(浪費時間)
  • 結論
    • 比如在項目中,影片文件需要轉換格式等操作比較費時,這時開一個新執行緒處理影片轉換,避免阻塞主執行緒
    • tomcat 的非同步 servlet 也是類似的目的,讓用戶執行緒處理耗時較長的操作,避免阻塞 tomcat 的工作執行緒
    • ui 程式中,開執行緒進行其他操作,避免阻塞 ui 執行緒

image-20220813090501414

3.2 非同步調用

3.2.1 測試程式碼

/**
 * 非同步測試
 * @author : look-word
 * 2022-08-13 08:58
 **/
@Slf4j
public class AsynchronousTest {
    public static void main(String[] args) {
        new Thread(() ->{readFile("xxx.text");}).start();
        log.debug("do other things ....");
    }
    // 模擬讀取文件
    private static void readFile(String s) {
        log.info("{}開始讀取", s);
        long beginTime = System.currentTimeMillis();
        try {
            Thread.sleep(1789);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        log.info("讀取時長{} s", System.currentTimeMillis() - beginTime);
    }
}

可以看到,我們講讀取操作放入了執行緒中執行,他們兩也沒有互相干擾,等待。分別由不同的執行緒去完成,大大加快的程式的效率。

image-20220813091136690

4 創建執行緒的三種方式

4.1 Thread

示例程式碼

/**
 * 使用 Thread實現
 *
 * @author : look-word
 * 2022-08-13 09:30
 **/
@Slf4j
public class CreateThread1 {
    public static void main(String[] args) {
        new Thread("執行緒1") { // 可以指定執行緒名稱
            public void run() {  // 需要執行的內容
                log.info("當前執行緒:{} 執行...",
                        Thread.currentThread().getName());
            }
        }.start(); // 啟動創建的執行緒

        log.info("當前執行緒:{} 執行...", Thread.currentThread().getName());
    }
}

4.2 Runnable

示例程式碼

/**
 * 使用 Ran實現
 *
 * @author : look-word
 * 2022-08-13 09:30
 **/
@Slf4j
public class CreateThread2 {
    public static void main(String[] args) {
        Runnable runnable = new Runnable() {
            @Override // 要執行的任務
            public void run() {
                log.info("當前執行緒:{} 執行...",
                        Thread.currentThread().getName());
            }
        };
        // 創建執行緒  啟動執行緒
        new Thread(runnable,"runnable").start();
        log.info("當前執行緒:{} 執行...", Thread.currentThread().getName());
    }
}

小結

Thread 是把執行緒和任務合併在了一起,Runnable 是把執行緒和任務分開了

  • 用 Runnable 更容易與執行緒池等高級 API 配合
  • 用 Runnable 讓任務類脫離了 Thread 繼承體系,更靈活

4.3 FutureTask

task.get(); 調用會阻塞主執行緒以及其他執行緒的運行。

/**
 * 使用 FutureTask實現
 *
 * @author : look-word
 * 2022-08-13 09:30
 **/
@Slf4j
public class CreateThread3 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 創建任務對象
        FutureTask<Integer> task = new FutureTask<>(new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                log.info("running...");
                Thread.sleep(1000);
                return 100;
            }
        });
        // 參數1 是任務對象; 參數2 是執行緒名字,推薦
        Thread t1 = new Thread(task, "t1");
        t1.start();
        // 主執行緒阻塞,同步等待 task 執行完畢的結果
        log.debug("阻塞任務結果,{}", task.get());
    }
}

5 原理之執行緒運行

5.1 棧與棧幀

Java Virtual Machine Stacks (Java 虛擬機棧)

我們都知道 JVM 中由堆、棧、方法區所組成,其中棧記憶體是給誰用的呢?其實就是執行緒,每個執行緒啟動後,虛擬 機就會為其分配一塊棧記憶體。

  • 每個棧由多個棧幀(Frame)組成,對應著每次方法調用時所佔用的記憶體
  • 每個執行緒只能有一個活動棧幀,對應著當前正在執行的那個方法

學習地址 從p20集開始

5.2 執行緒上下文切換 (Thread Context Switch)

因為以下一些原因導致 cpu 不再執行當前的執行緒,轉而執行另一個執行緒的程式碼

  • 執行緒的 cpu 時間片用完 垃圾回收

  • 有更高優先順序的執行緒需要運行

  • 執行緒自己調用了 sleep、yield、wait、join、park、synchronized、lock 等方法

當 Context Switch 發生時,需要由作業系統保存當前執行緒的狀態,並恢復另一個執行緒的狀態,Java 中對應的概念 就是程式計數器(Program Counter Register),它的作用是記住下一條 jvm 指令的執行地址,是執行緒私有的

  • 狀態包括程式計數器、虛擬機棧中每個棧幀的資訊,如局部變數、操作數棧、返回地址等
  • Context Switch 頻繁發生會影響性能