1. 程式人生 > >Java併發程式設計的藝術——讀書筆記(一) 併發程式設計的挑戰

Java併發程式設計的藝術——讀書筆記(一) 併發程式設計的挑戰

第一章 併發程式設計的挑戰

因為最近找工作,準備筆試/面試,開始嘗試閱讀這本書,我不常寫部落格,距上一次寫已經過去大概一年時間了,連CSDN密碼都忘了/衰,所以這次新開一個賬號重新開始,希望我能堅持下去。

第一章沒什麼內容,我認為其目的主要是給出足夠多的閱讀這本書的理由,瞭解一些基本概念,和讓新人快速適應環境吧,那,現在開始吧。

上下文切換

上下文:以前學習Spring的時候總是不知道上下文(context)是什麼意思,後來通過查資料得知,上下文其實就是一段程式當前所處的環境,包括當前的狀態等等,換個說法就是“語境”,譬如一篇文章,只抽其中的一個小段或者一句話來讀,你很可能不理解是什麼意思,只有放入原來的位置,根據上下段落的意思才能讀懂,同樣在程式設計過程中,一段程式碼單獨拿出來執行是沒有任何意義的,只有把它放入程式中,你才會明白這段程式碼起到什麼作用,會達到怎樣的預期效果,這個所處的環境,就是“上下文”(context)

併發肯定少不了多執行緒之間的切換工作,處理器會在多個執行緒之間切換執行,在切換到另一個執行緒的過程中,需要儲存當前執行緒的狀態,以便下次切回時能保證正常的執行,這就是“上下文切換”。書上原話是這麼說的:

CPU通過給每個執行緒分配CPU時間片來實現這個機制。時間片是CPU分配給各個執行緒的時間,因為時間片非常短,所以CPU通過不停地切換執行緒執行,讓我們感覺多個執行緒是同時執行的。CPU通過時間片分配演算法來迴圈執行任務,當前任務執行一個時間片後會切換到下一個任務。但是,在切換前會儲存上一個任務的狀態,以便下次切換回這個任務時,可以再載入這 個任務的狀態。所以任務從儲存到再載入的過程就是一次上下文切換。

那麼我們不難想到,既然多執行緒情況下CPU需要不斷的進行上下文切換,肯定是需要一定的開銷的,如果併發執行比序列的效率高,那麼這種開銷就是值得的,為此書中提到了一個序列和併發執行的對比測試,通過執行某段程式n次記錄執行時間來比較兩者的效率,結果如下:

由此可以得出結論,併發並不是永遠比序列快,在迴圈次數較少的情況下(百萬級別以下),由於建立執行緒和上下文切換造成的開銷,併發速度其實是不比序列的。

減少上下文切換的方法

書中提到了幾個方法,但都只用一句話簡單介紹,具體思路和實現會在後面的章節中詳細描述,因此我也不過多累述,只把原文pull了下來。

減少上下文切換的方法有無鎖併發程式設計、CAS演算法、使用最少執行緒和使用協程。

無鎖併發程式設計。多執行緒競爭鎖時,會引起上下文切換,所以多執行緒處理資料時,可以用一 些辦法來避免使用鎖,如將資料的ID按照Hash演算法取模分段,不同的執行緒處理不同段的資料。

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

使用最少執行緒。避免建立不需要的執行緒,比如任務很少,但是建立了很多執行緒來處理,這 樣會造成大量執行緒都處於等待狀態。

協程:在單執行緒裡實現多工的排程,並在單執行緒裡維持多個任務間的切換。

死鎖

多個執行緒同時執行時,難免會對共享資源進行競爭,為了保證執行緒之間有序執行,在一個執行緒獲得資源後必須加鎖,鎖是個非常有用的工具,使用起來非常簡單且易於理解,它能保證操作的原子性和可見性

  • 操作的原子性:一個執行緒訪問一段共享資源時獲取鎖,直到執行緒釋放鎖之前保證其他執行緒不會干擾,執行緒對於資源的這段操作就是連續的,不可被分開的,因此稱為原子性;

  • 操作的可見性:一個執行緒獲取鎖後,訪問到的資源是上一個執行緒訪問後已經更新過的資源

但同時它也可能引起死鎖,比如下面這段程式碼

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
        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已獲取鎖A等待鎖B,執行緒t2已獲取鎖B等待鎖A,雙方都不釋放以獲取的鎖,就會無限的等待下去,這種現象就是死鎖。

避免死鎖的幾個辦法

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

資源限制的挑戰(略)

第一章就到此結束,其中一些測試實戰部分沒有列出來,不過基本知識點就這麼多,好久沒寫部落格可能有些生疏,不過會繼續的