併發編程常見問題及解決方案


併發編程的目的是爲了讓程序運行得更快
但是,並不是啓動更多的線程就能讓程序最大限度地併發執行

上下文切換問題

即使是單核處理器也支持多線程執行代碼,CPU通過給每個線程分配CPU時間片來實現這個機制。
時間片是CPU分配給各個線程的時間,因爲時間片非常短,所以CPU通過不停地切換線程執行,讓我們感覺多個線程是同時執行的,時間片一般是幾十毫秒

CPU通過時間片分配算法來循環執行任務,當前任務執行一個時間片後會切換到下一個任務。但是,在切換前會保存上一個任務的狀態,以便下次切換回這個任務時,可以再加載這個任務的狀態。所以任務從保存到再加載的過程就是一次上下文切換。

這就像我們同時讀兩本書,當我們在讀一本英文的技術書時,發現某個單詞不認識,於是便打開中英文字典,但是在放下英文技術書之前,大腦必須先記住這本書讀到了多少頁的第多少行,等查完單詞之後,能夠繼續讀這本書。這樣的切換是會影響讀書效率的,同樣上下文切換也會影響多線程的執行速度。

多線程不一定快

下面的代碼演示串行和併發執行並累加操作的時間:

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--;
}
long time = System.currentTimeMillis() - start;
thread.join();
System.out.println("concurrency :" + time+"ms,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("serial:" + time+"ms,b="+b+",a="+a);
}
}

在這裏插入圖片描述
當併發執行累加操作不超過百萬次時,速度會比串行執行累加操作要慢。那麼,爲什麼併發執行的速度會比串行慢呢?這是因爲線程有創建和上下文切換的開銷

如何測試上下文切換次數和時長

使用Lmbench3可以測量上下文切換的時長
使用vmstat可以測量上下文切換的次數
下面是利用vmstar測量上下文切換次數的示例:
在這裏插入圖片描述
CS(Content Switch)表示上下文切換的次數,從上面的測試結果中我們可以看到,上下文每秒切換1000多次。

如何減少上下文切換

減少上下文切換的方法有無鎖併發編程CAS算法使用最少線程使用協程

無鎖併發編程

多線程競爭鎖時,會引起上下文切換,所以多線程處理數據時,可以用一些辦法來避免使用鎖,如將數據的ID按照Hash算法取模分段,不同的線程處理不同段的數據。

CAS算法

Java的Atomic包使用CAS算法來更新數據,而不需要加鎖。

使用最少線程

避免創建不需要的線程,比如任務很少,但是創建了很多線程來處理,這樣會造成大量線程都處於等待狀態。

協程

在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換。

減少上下文切換的例子

通過減少線上大量WAITING的線程,來減少上下文切換次數:

  1. 用jstack命令dump線程信息,看看pid爲3117的進程裏的線程都在做什麼
    在這裏插入圖片描述
  2. 統計所有線程分別處於什麼狀態,發現300多個線程處於WAITING(onobjectmonitor)狀態
    在這裏插入圖片描述
  3. 打開dump文件查看處於WAITING(onobjectmonitor)的線程在做什麼。發現這些線程基本全是JBOSS的工作線程,在await。說明JBOSS線程池裏線程接收到的任務太少,大量線程都閒着
    在這裏插入圖片描述
  4. 減少JBOSS的工作線程數,找到JBOSS的線程池配置信息,將maxThreads降到100
    在這裏插入圖片描述
  5. 重啓JBOSS,再dump線程信息,然後統計WAITING(onobjectmonitor)的線程,發現減少了175個。WAITING的線程少了,系統上下文切換的次數就會少,因爲每一次從
    WAITTING到RUNNABLE都會進行一次上下文的切換。
    在這裏插入圖片描述

死鎖問題

鎖是個非常有用的工具,運用場景非常多,因爲它使用起來非常簡單,而且易於理解。但同時它也會帶來一些困擾,那就是可能會引起死鎖,一旦產生死鎖,就會造成系統功能不可用。

public class DeadLockDemo {
privat static String A = "A";
private static String B = "B";
public static void main(String[] args) {
new DeadLockDemo().deadLock();
}
private void deadLock() {
Thread t1 = new Thread(new Runnable() {
@Override
publicvoid run() {
synchronized (A) {
try { Thread.currentThread().sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (B) {
System.out.println("1");
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
publicvoid run() {
synchronized (B) {
synchronized (A) {
System.out.println("2");
}
}
}
});
t1.start();
t2.start();
}
}

這段代碼會引起死鎖,使線程t1和線程t2互相等待對方釋放鎖。
一旦出現死鎖,業務是可感知的,因爲不能繼續提供服務了,那麼只能通過dump線程查看到底是哪個線程出現了問題,以下線程信息告訴我們是DeadLockDemo類的第42行和第31行引起的死鎖。
在這裏插入圖片描述
避免死鎖的方法:

  • 避免一個線程同時獲取多個鎖。
  • 避免一個線程在鎖內同時佔用多個資源,儘量保證每一個鎖只佔用一個資源。
  • 嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制.
  • 對於數據庫鎖,加鎖和解鎖必須在一個數據庫連接裏,否則會出現解鎖失敗的情況.

資源限制

什麼是資源限制

資源限制是指在進行併發編程時,程序的執行速度受限於計算機硬件資源或軟件資源。例如,服務器的帶寬只有2Mb/s,某個資源的下載速度是1Mb/s每秒,系統啓動10個線程下載資源,下載速度不會變成10Mb/s,所以在進行併發編程時,要考慮這些資源的限制。硬件資源限制有帶寬的上傳/下載速度、硬盤讀寫速度和CPU的處理速度。軟件資源限制有數據庫的連接數和socket連接數等。

資源限制引發的問題

在併發編程中,將代碼執行速度加快的原則是將代碼中串行執行的部分變成併發執行,但是如果將某段串行的代碼併發執行,因爲受限於資源,仍然在串行執行,這時候程序不僅不會加快執行,反而會更慢,因爲增加了上下文切換和資源調度的時間。例如,之前看到一段程序使用多線程在辦公網併發地下載和處理數據時,導致CPU利用率達到100%,幾個小時都不能運行完成任務,後來修改成單線程,一個小時就執行完成了。

解決資源限制的方案

對於硬件資源限制,可以考慮使用集羣並行執行程序。既然單機的資源有限制,那麼就讓程序在多機上運行。比如使用ODPS、Hadoop或者自己搭建服務器集羣,不同的機器處理不同的數據。可以通過“數據ID%機器數”,計算得到一個機器編號,然後由對應編號的機器處理這筆數據。

對於軟件資源限制,可以考慮使用資源池將資源複用。比如使用連接池將數據庫和Socket連接複用,或者在調用對方webservice接口獲取數據時,只建立一個連接。

在資源限制情況下進行併發編程

如何在資源限制的情況下,讓程序執行得更快呢?方法就是,根據不同的資源限制調整程序的併發度,比如下載文件程序依賴於兩個資源——帶寬和硬盤讀寫速度。有數據庫操作時,涉及數據庫連接數,如果SQL語句執行非常快,而線程的數量比數據庫連接數大很多,則某些線程會被阻塞,等待數據庫連接。

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