并發程式設計的挑戰
- 上下文切換
-
- 多線程一定快嗎
- 如何減少上下文切換
- 死鎖
- 資源限制的挑戰
(ps:本文是筆者學習方騰飛先生著作的《 Java并發程式設計的藝術》的學習筆記)
- 并發程式設計的最初目的是為了讓程式運作的更快,但是并不是啟動更多的線程就能讓程式最大限度的并發執行。在進行并發程式設計的時候會遇到很多問題。
上下文切換
- 單核處理器也支援多線執行代碼,CPU通過給每個線程配置設定CPU時間片來實作這個機制。
- 時間片是CPU配置設定給各個線程的的時間。
- 因為時間片非常短,是以CPU通過不停的切換線程執行,讓我們以為是同時運作的,時間片一般是幾十毫秒。
- CPU通過時間片配置設定算法來循環執行任務,目前一個任務在執行一個時間片後會切換到下一個任務。但是,在切換前會儲存上一個任務的狀态,以便下次切換回這個任務的時,可以再加載這個任務的狀态。是以,任務從儲存到在加載的過程,就是一次上下文切換。
多線程一定快嗎
- 串行和并發執行
package 并發程式設計的藝術.串行與并發;
/**
* @Description: //測試并發和串行
* @ClassName: ConcurrencyTest
* @Author: hdd
* @Date: 2021/4/6 10:55
* @Version: 1.0
*/
public class ConcurrencyTest {
private static final long count = 10000L;
public static void main(String[] args) throws InterruptedException {
concurrency();//并發
serial();//串行
}
//并發
private static void concurrency() throws InterruptedException {
long start = System.currentTimeMillis();//開始時間
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
}
});
thread.start();
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
thread.join();
long time = System.currentTimeMillis() - start;
System.out.println("并發:"+time+"毫秒,b="+b);
}
//串行
private static void serial() {
long start = System.currentTimeMillis();//開始時間
int a = 0;
for (long i = 0; i < count; i++) {
a += 5;
}
int b = 0;
for (long i = 0; i < count; i++) {
b--;
}
long time = System.currentTimeMillis() - start;
System.out.println("串行:"+time+"毫秒,b="+b+",a="+a);
}
}
- 循環次數為1萬時
- 循環次數為10萬時
- 循環次數為100萬時
- 循環次數為1000萬時
- 循環次數為1億時
如何減少上下文切換
- 減少上下文切換有四種方法
- 無鎖并發程式設計:多線程競争鎖時,會引起上下文切換,是以多線程處理資料時,可以用一些辦法來避免競争鎖,比如将資料的ID按照Hash算法取模分段,不同的線程處理不同段的資料。
- CAS算法:Java的Atomic包使用CAS算法來更新資料,而不需要加鎖。
- 使用最少線程:避免建立不需要的線程,比如任務很少,但是建立了很多線程來處理,這樣會造成大量線程都處于等待狀态。
- 協程:在單線程裡實作多任務的排程,并在單線程裡維持多個任務的切換。
死鎖
- 鎖可能會産生死鎖,一旦死鎖,就會造成系統功能不可用的情況。示範:
package 并發程式設計的藝術.死鎖;
/**
* @Description: // 死鎖示範
* @ClassName: DeadLockDemo
* @Author: hdd
* @Date: 2021/4/6 14:44
* @Version: 1.0
*/
public class DeadLockDemo {
private static String A = "鎖1";
private static String B = "鎖2";
public static void main(String[] args) {
new DeadLockDemo().deadLock();
}
private void deadLock() {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (A){
try {
Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B){
System.out.println("1");
}
}
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
synchronized (B){
synchronized (A){
System.out.println("2");
}
}
}
});
thread1.start();
thread2.start();
}
}
- IDEA控制台進入死鎖狀态
- 死鎖産生的4個必要條件
- 互斥: 某種資源一次隻允許一個程序通路,即該資源一旦配置設定給某個程序,其他程序就不能再通路,直到該程序通路結束。
- 占有且等待: 一個程序本身占有資源(一種或多種),同時還有資源未得到滿足,正在等待其他程序釋放該資源。
- 不可搶占: 别人已經占有了某項資源,你不能因為自己也需要該資源,就去把别人的資源搶過來。
- 循環等待: 存在一個程序鍊,使得每個程序都占有下一個程序所需的至少一種資源。
- 避免死鎖的幾個常見方法
- 避免一個線程同時擷取多個鎖。
- 避免一個線程在鎖内同時占用多個資源,盡量保證每個鎖隻占用一個資源。
- 嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用内部的鎖機制
- 對于資料庫鎖,加鎖和解鎖必須在一個資料庫連接配接裡,否則會出現解鎖失敗的情況。
資源限制的挑戰
- 什麼是資源限制
- 資源限制指的是在進行并發程式設計時,程式的執行速度受限于計算機硬體資源或者軟體資源。(例:伺服器寬帶隻有2Mb/s,某個資源的下載下傳速度是1Mb/s,系統啟動10個線程,下載下傳速度不會變成10Mb/s。)
- 硬體資源限制有寬帶的上傳和下載下傳速度、硬碟讀寫速度和CPU的處理速度。
- 軟體資源限制有資料庫的連接配接數和socket的連接配接數等
- 資源限制引發的問題
- 在并發程式設計中,将代碼執行速度加快的原則是将代碼中串行執行的部分變成并發執行。但是,如果将某段串行代碼并發執行,因為資源受限,導緻仍是在串行執行。這種情況下,代碼不僅不會變快,反而會變的更慢,因為增加了上下文切換和資源排程的時間。(例如:CPU使用率到達100%時候,串行代碼速度要比并發代碼要快)
- 如何解決資源限制的問題
- 對與硬體資源限制,可以使用叢集并行執行程式。比如自己搭建伺服器叢集,不同的機器處理不同的資料。
- 對與軟體資源限制,可以考慮使用資源池将資源複用。比如使用連接配接池将資料庫連接配接複用,或者在調用對方接口擷取資料的時候,隻建立一個連接配接。
- 在資源限制情況下進行并發程式設計
- 根據不同的資源限制調整程式的并發度。
- 比如下載下傳檔案程式依賴于兩個資源——寬帶和硬碟讀寫速度。
- 有資料庫操作時,線程數量大于資料庫連接配接數,則某些線程會被阻塞,等待資料庫連接配接。可以進行sql語句調優,加快執行速度。