前言
併發編程的目的是爲了讓程序運行得更快,但是,並不是啓動更多的線程就能讓程序最大限度地併發執行。在進行併發編程時,如果希望通過多線程執行任務讓程序運行得更快,會面臨非常多的挑戰,比如上下文切換的問題、死鎖的問題,以及受限於硬件和軟件的資源限制 問題,本章會介紹幾種併發編程的挑戰以及解決方案。
上下文切換
CPU通過時間片分配算法來循環執行任務,當前任務執行一個時間片後會切換到下一個 任務。但是,在切換前會保存上一個任務的狀態,以便下次切換回這個任務時,可以再加載這個任務的狀態。所以任務從保存到再加載的過程就是一次上下文切換。
多線程一定快嗎
當併發執行累加操作不超過百萬次時,速度會比串行執行累加操作要慢。那麼,爲什麼併發執行的速度會比串行慢呢?這是因爲線程有創建和上下文切換的開銷。
測試上下文切換次數和時長
下面我們來看看有什麼工具可以度量上下文切換帶來的消耗。
使用Lmbench3是一個性能分析工具,可以測量上下文切換的時長。
使用vmstat可以測量上下文切換的次數。
如何減少上下文切換
減少上下文切換的方法有無鎖併發編程、CAS算法、使用最少線程和使用協程。
無鎖併發編程。多線程競爭鎖時,會引起上下文切換,所以多線程處理數據時,可以用一些辦法來避免使用鎖,如將數據的ID按照Hash算法取模分段,不同的線程處理不同段的數據。
CAS算法。Java的Atomic包使用CAS算法來更新數據,而不需要加鎖。
使用最少線程。避免創建不需要的線程,比如任務很少,但是創建了很多線程來處理,這 樣會造成大量線程都處於等待狀態。
協程:在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換。
減少上下文切換
查看線程狀態,看是否有機會來減少上下文切換次數。
第一步:用jstack命令dump線程信息,看看某一進程裏的線程都在做什麼。
jstack pid > /home/develop/dump
第二步:統計所有線程分別處於什麼狀態。
grep java.lang.Thread.State dump17 | awk ‘{print $2$3$4$5}’ | sort |uniq -c
如果處於空閒的線程過多,可以合理降低線程數量。因爲每一次從 WAITTING到RUNNABLE都會進行一次上下文的切換。讀者也可以使用vmstat命令測試一下。
死鎖
產生條件
互斥條件:進程要求對所分配的資源進行排它性控制,即在一段時間內某資源僅爲一進程所佔用。
請求和保持條件:當進程因請求資源而阻塞時,對已獲得的資源保持不放。
不剝奪條件:進程已獲得的資源在未使用完之前,不能剝奪,只能在使用完時由自己釋放。
環路等待條件:在發生死鎖時,必然存在一個進程–資源的環形鏈。
避免死鎖的幾個常見方法。
避免一個線程同時獲取多個鎖。
避免一個線程在鎖內同時佔用多個資源,儘量保證每個鎖只佔用一個資源。
嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制。
對於數據庫鎖,加鎖和解鎖必須在一個數據庫連接裏,否則會出現解鎖失敗的情況。
資源限制的挑戰
什麼是資源限制
資源限制是指在進行併發編程時,程序的執行速度受限於計算機硬件資源或軟件資源。
資源限制引發的問題
在併發編程中,將代碼執行速度加快的原則是將代碼中串行執行的部分變成併發執行,
但是如果將某段串行的代碼併發執行,因爲受限於資源,仍然在串行執行,這時候程序不僅不
會加快執行,反而會更慢,因爲增加了上下文切換和資源調度的時間。
如何解決資源限制的問題
對於硬件資源限制,可以考慮使用集羣並行執行程序。既然單機的資源有限制,那麼就讓
程序在多機上運行。比如使用ODPS、Hadoop或者自己搭建服務器集羣,不同的機器處理不同
的數據。可以通過“數據ID%機器數”,計算得到一個機器編號,然後由對應編號的機器處理這 筆數據。
對於軟件資源限制,可以考慮使用資源池將資源複用。比如使用連接池將數據庫和Socket
連接複用,或者在調用對方webservice接口獲取數據時,只建立一個連接。
在資源限制情況下進行併發編程
如何在資源限制的情況下,讓程序執行得更快呢?方法就是,根據不同的資源限制調整
程序的併發度,比如下載文件程序依賴於兩個資源——帶寬和硬盤讀寫速度。有數據庫操作
時,涉及數據庫連接數,如果SQL語句執行非常快,而線程的數量比數據庫連接數大很多,則某些線程會被阻塞,等待數據庫連接。
小結
java提供的juc工具類已經可以充分解決以上出現的幾個問題。
本章知識點如下:
什麼是下文切換
多線程一定快嗎
如何測試上下文切換次數和時長
如何減少上下文切換
死鎖如何產生
如何避免死鎖
什麼是資源限制
如何解決資源限制的問題
附源碼1單線程與多線程性能對比:
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);
}
}
附源碼2 死鎖:
public class DeadLockDemo {
private 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
public void 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
public void run() {
synchronized (B) {
synchronized (A) {
System.out.println("2");
}
}
}
});
t1.start();
t2.start();
}
}