java併發編程的藝術(1)併發編程的挑戰

在常見的多線程編程裏面,我們總是希望創建多個線程來讓程序的運行速度更加快速。但是,並不是啓動更加多的線程就可以讓程序運行地更加快速了。例如說當出現了上下文切換的情況和死鎖的情況。因此下邊小編會編寫一些典型的案例來說明併發和串行的效率區別。

上下文切換
使是單核處理器也支持多線程執行代碼,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接口獲取數據時,只建立一個連接。

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