在常見的多線程編程裏面,我們總是希望創建多個線程來讓程序的運行速度更加快速。但是,並不是啓動更加多的線程就可以讓程序運行地更加快速了。例如說當出現了上下文切換的情況和死鎖的情況。因此下邊小編會編寫一些典型的案例來說明併發和串行的效率區別。
上下文切換:
使是單核處理器也支持多線程執行代碼,CPU通過給每個線程分配CPU時間片來實現這個機制。時間片是CPU分配給各個線程的時間,因爲時間片非常短,所以CPU通過不停地切換線程執行,讓我們感覺多個線程是同時執行的,時間片一般是幾十毫秒(ms)。
多線程執行案例:
package 併發編程01.串行和併發的效率比較;
public class ConcurrencyTest {
private static final long count=100000000l;
public static void concurrency() throws InterruptedException {
long begin=System.currentTimeMillis();
Thread thread= new Thread(new Runnable(){
@Override
public void run() {
for(int i=0;i<count;i++){
}
}
});
thread.start();
long end=System.currentTimeMillis();
thread.join();
System.out.println("併發消耗時間爲:"+(end-begin));
}
public static void serial(){
long begin=System.currentTimeMillis();
for(int i=0;i<count;i++){
}
long end=System.currentTimeMillis();
System.out.println("串行消耗時間爲:"+(end-begin));
}
public static void main(String[] args) throws InterruptedException {
concurrency();
serial();
}
}
在經過多次調整的比較下,我得出了以下數據:
循環10次: 併發: 1ms 串行:0ms
循環100次: 併發: 1ms 串行:0ms
循環1000次: 併發: 1ms 串行:0ms
循環10000次: 併發: 1ms 串行:1ms
循環100000次: 併發: 1ms 串行:3ms
循環1000000次: 併發: 1ms 串行:4ms
循環10000000次: 併發: 1ms 串行:12ms
循環100000000次: 併發: 1ms 串行:63ms
通過這一組數據,我們可以發現,當數據量較小的情況下,串行的效率會比並發要高些,這是因爲併發需要進行不斷地上下文切換操作,這個操作會消耗一定的時間,但是當數據量增大的時候,併發的效率反而會更加高。
減少上下文切換操作的方法:
1.無鎖併發編程。多線程競爭鎖時,會引起上下文切換,所以多線程處理數據時,可以用一些辦法來避免使用鎖,如將數據的ID按照Hash算法取模分段,不同的線程處理不同
2.CAS算法。Java的Atomic包使用CAS算法來更新數據,而不需要加鎖。
3.使用最少線程。避免創建不需要的線程,比如任務很少,但是創建了很多線程來處理,這樣會造成大量線程都處於等待狀態。
4.協程:在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換。
死鎖
鎖這種工具是非常有效的,但是在併發編程裏面,鎖的過多使用容易造成資源的相互爭奪,當兩個或者多個線程都持有對方相應的資源而不肯釋放的情況下,會出現死鎖這種現象,導致系統卡死,或功能失效。
下邊來舉一個死鎖的案例代碼:
package 併發編程01.死鎖的案例;
import java.awt.*;
/*
*
* @author idea
* @date 2018/7/13
* @des 模擬在多線程編程裏面出現的死鎖情況
* 這一段代碼的主要功能就在於讓t1,t2兩個線程互相搶佔對方的資源,導致雙方都沒辦法獲取到相應的鎖
*/
public class DeadLock {
public String locka="a";
public String lockb="b";
public static void main(String[] args) {
new DeadLock().deadLock();
}
public void deadLock(){
Thread t1=new Thread(new Runnable() {
@Override
public void run() {
synchronized (locka){
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lockb){
System.out.println("this is a");
}
}
}
});
Thread t2=new Thread(new Runnable() {
@Override
public void run() {
synchronized (lockb){
synchronized (locka){
System.out.println("this is b");
}
}
}
});
t1.start();
t2.start();
}
}
如何預防死鎖這種情況呢?
1.避免一個線程同時獲取多個鎖。
2.避免一個線程在鎖內同時佔用多個資源,儘量保證每個鎖只佔用一個資源。
3.嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制。
4.對於數據庫鎖,加鎖和解鎖必須在一個數據庫連接裏,否則會出現解鎖失敗的情況。
資源的搶奪
但是光只是從這些方面來考慮問題還遠不遠不夠,因爲併發實際是一個非常複雜的場景問題,併發的高效性還和資源的數量有關,因此我們不得不得提及到一個概念,叫做資源限制
資源限制是指在進行併發編程時,程序的執行速度受限於計算機硬件資源或軟件資源。例如,服務器的帶寬只有2Mb/s,某個資源的下載速度是1Mb/s每秒,系統啓動10個線程下載資源,下載速度不會變成10Mb/s,所以在進行併發編程時,要考慮這些資源的限制。硬件資源限制有帶寬的上傳/下載速度、硬盤讀寫速度和CPU的處處理速度。軟件資源限制有數據庫的連接數和socket連接數等。
如何解決資源限制問題?
對於硬件資源限制,可以考慮使用集羣並行執行程序。既然單機的資源有限制,那麼就讓程序在多機上運行。比如使用ODPS、Hadoop或者自己搭建服務器集羣,不同的機器處理不同的數據。可以通過“數據ID%機器數”,計算得到一個機器編號,然後由對應編號的機器處理這筆數據。
對於軟件資源限制,可以考慮使用資源池將資源複用。比如使用連接池將數據庫和Socket連接複用,或者在調用對方webservice接口獲取數據時,只建立一個連接。