並發編程(概念簡述)
並發編程(概念簡述)
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
每個時間段,只能處理一個執行緒
2.2 並行
前提:是在
多核CPU下
才存在並行
- 下圖是cpu在兩個核心下,同一時間處理執行緒的能力。
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 執行緒
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);
}
}
可以看到,我們講讀取操作放入了執行緒中執行,他們兩也沒有互相干擾,等待。分別由不同的執行緒去完成,大大加快的程式的效率。
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)組成,對應著每次方法調用時所佔用的記憶體
- 每個執行緒只能有一個活動棧幀,對應著當前正在執行的那個方法
5.2 執行緒上下文切換 (Thread Context Switch)
因為以下一些原因導致 cpu 不再執行當前的執行緒,轉而執行另一個執行緒的程式碼
執行緒的 cpu 時間片用完 垃圾回收
有更高優先順序的執行緒需要運行
執行緒自己調用了 sleep、yield、wait、join、park、synchronized、lock 等方法
當 Context Switch 發生時,需要由作業系統保存當前執行緒的狀態,並恢復另一個執行緒的狀態,Java 中對應的概念 就是程式計數器(Program Counter Register),它的作用是記住下一條 jvm 指令的執行地址,是執行緒私有的
- 狀態包括程式計數器、虛擬機棧中每個棧幀的資訊,如局部變數、操作數棧、返回地址等
- Context Switch 頻繁發生會影響性能