【併發編程】併發編程中你須要知道的基礎概念


本博客系列是學習併發編程過程當中的記錄總結。因爲文章比較多,寫的時間也比較散,因此我整理了個目錄貼(傳送門),方便查閱。html

併發編程系列博客傳送門java


多線程是Java編程中一塊很是重要的內容,其中涉及到不少概念。這些概念咱們平時常常掛在嘴上,可是真的要讓你介紹下這些概念,你可能還真的講不清楚。這篇博客就總結下多線程編程中常常用到的概念,理解這些概念能幫助咱們更好地掌握多線程編程。linux

進程(Process)與線程(Thread)

進程和線程是最常提到的概念了。在linux中,線程與進程最大的區別就是是否共享同一塊地址空間,並且共享同一塊地址空間的那一組線程將顯現相同的PID號。下面介紹下二者的概念:程序員

  • 進程是系統資源分配的最小單元,能夠簡單地理解爲系統中運行的一個程序就是一個進程。
  • 線程是CPU調度的最小單元,是進程中的一個個執行流程。
  • 一個進程至少包含一個線程,能夠包含多個線程,這些線程共享這個進程的資源(好比堆區和方法區資源)。同時每一個線程都擁有獨立的運行棧和程序計數器,線程切換開銷小。
  • 多進程指的是操做系統同時運行多個程序,如當前操做系統中同時運行着QQ、IE、微信等程序。
  • 多線程指的是同一進程中同時運行多個線程,如迅雷運行時,能夠開啓多個線程,同時進行多個文件的下載。

談到線程和進程,又勢必會涉及到線程號和進程號的概念。下面列舉了各個ID的概念。面試

  • pid: 進程ID。
  • tgid: 線程組ID,也就是線程組leader的進程ID,等於pid。
  • lwp: 線程ID。在用戶態的命令(好比ps)中經常使用的顯示方式。
  • tid: 線程ID,等於lwp。tid在系統提供的接口函數中更經常使用,好比syscall(SYS_gettid)和syscall(__NR_gettid)。

並行(Parallel)、併發(Concurrent)

  • 併發:是指多個線程任務在同一個CPU上快速地輪換執行,因爲切換的速度很是快,給人的感受就是這些線程任務是在同時進行的,但其實併發只是一種邏輯上的同時進行;
  • 並行:是指多個線程任務在不一樣CPU上同時進行,是真正意義上的同時執行。

下面貼上一張圖來解釋下這兩個概念:算法

上圖中的咖啡就能夠當作是CPU,上面的只有一個咖啡機,至關於只有一個CPU。想喝咖啡的人只有等前面的人制做完咖啡才能製做本身的開發,也就是同一時間只能有一我的在製做咖啡,這是一種併發模式。下面的圖中有兩個咖啡機,至關於有兩個CPU,同一時刻能夠有兩我的同時製做咖啡,是一種並行模式。數據庫

咱們發現並行編程中,很重要的一個特色是系統具備多核CPU。要是系統是單核的,也就談不上什麼並行編程了。編程

線程安全

這個概念多是在多線程編程中說起最多的一個概念了。在面試過程當中,我試着問過幾個面試者,可是幾乎沒人能將這個概念解釋的很好的。安全

關於這個概念,我以爲好多人都有一個誤區,包括我本身一開始也是這樣的。我一開始認爲線程安全講的是某個共享變量線程安全,其實咱們所說的線程安全是指某段代碼或者是某個方法是線程安全的。線程安全的準肯定義應該是這樣的:微信

若是線程的隨機調度順序不影響某段代碼的最後執行結果,那麼咱們認爲這段代碼是線程安全的

爲了保證代碼的線程安全,Java中推出了不少好用的工具類或者關鍵字,好比volatile、synchronized、ThreadLocal、鎖、併發集合、線程池和CAS機制等。這些工具並非在每一個場景下都能知足咱們多線程編程的需求,並非在每一個場景下都有很高的效率,須要咱們程序員根據具體的場景來選擇最適合的技術,這也許就是咱們程序員存在的價值所在。(我一直以爲若是有一個技術能很好的解決大多數場景下的問題,那麼這個領域確定是能夠作成機器自動化的。那麼對於這個領域就不太須要有多少人蔘與了。)

死鎖

線程1佔用了鎖A,等待鎖B,線程2佔用了鎖B,等待鎖A,這種狀況下就形成了死鎖。在死鎖狀態下,相關的代碼將不能再提供服務。

private void deadLock() {
      Thread t1 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (lock1) {
              try {
                Thread.currentThread().sleep(2000);
              } catch (InterruptedException e) {
                e.printStackTrace();
              }
              synchronized (lock2) {
                System.out.println("1");
              }
            }
        }
      });
      Thread t2 = new Thread(new Runnable() {
        @Override
        public void run() {
            synchronized (lock2) {
              synchronized (lock1) {
                System.out.println("2");
              }
            }
        }
      });
      t1.start();
      t2.start();
    }

這段代碼只是演示死鎖的場景,在現實中你可能不會寫出這樣的代碼。可是,在一些更爲複雜的場景中,你可能會遇到這樣的問題,好比t1拿到鎖以後,由於一些異常狀況沒有釋放鎖(死循環)。又或者是t1拿到一個數據庫鎖,釋放鎖的時候拋出了異常,沒釋放掉。

若是你懷疑代碼中有線程出現了死鎖,你能夠dump線程,而後查看線程狀態有沒有Blocked的線程(java.lang.Thread.State: BLOCKED)

"Thread-2" prio=5 tid=7fc0458d1000 nid=0x116c1c000 waiting for monitor entry [116c1b000] 
    java.lang.Thread.State: BLOCKED (on object monitor) 
     at com.ifeve.book.forkjoin.DeadLockDemo$2.run(DeadLockDemo.java:42) 
     - waiting to lock <7fb2f3ec0> (a java.lang.String) 
     - locked <7fb2f3ef8> (a java.lang.String) 
     at java.lang.Thread.run(Thread.java:695)
     
     
"Thread-1" prio=5 tid=7fc0430f6800 nid=0x116b19000 waiting for monitor entry [116b18000] 
    java.lang.Thread.State: BLOCKED (on object monitor) 
     at com.ifeve.book.forkjoin.DeadLockDemo$1.run(DeadLockDemo.java:31) 
     - waiting to lock <7fb2f3ef8> (a java.lang.String) 
     - locked <7fb2f3ec0> (a java.lang.String) 
     at java.lang.Thread.run(Thread.java:695)

避免死鎖的幾個方式:

  • 儘可能不要一個線程同時佔用多個鎖;
  • 避免一個線程在鎖內同時佔用多個資源,儘可能保證每一個鎖只佔用一個資源。
  • 嘗試使用定時鎖,使用lock.tryLock(timeout)來替代使用內部鎖機制。
  • 對於數據庫鎖,加鎖和解鎖必須在一個數據庫鏈接裏,不然會出現解鎖失敗的狀況。

飢餓

飢餓是指某一個或者多個線程由於種種緣由沒法得到所須要的資源,致使一直沒法執行。好比它的線程優先級可能過低,而高優先級的線程不斷搶佔它須要的資源,致使低優先級線程沒法工做。

在天然界中,母鳥給雛鳥餵食時很容易出現這種狀況:因爲雛鳥不少,食物有限,雛鳥之間的食物競爭可能很是厲害,常常搶不到食物的雛鳥有可能會被餓死。線程的飢餓很是相似這種狀況。

此外,某一個線程一直佔着關鍵資源不放,致使其餘須要這個資源的線程沒法正常執行,這種狀況也是飢餓的一種。與死鎖相比,飢餓仍是有可能在將來一段時間內解決的(好比,高優先級的線程已經完成任務,再也不瘋狂執行)。

活鎖

活鎖是一種很是有趣的狀況。不知道你們是否遇到過這麼一種場景,當你要坐電梯下樓時,電梯到了,門開了,這時你正準備出去。但很不巧的是,門外一我的擋着你的去路,他想進來。因而,你很禮貌地靠左走,避讓對方。同時,對方也很是禮貌地靠右走,但願避讓你。結果,大家倆就又撞上了。因而乎,大家都意識到了問題,但願儘快避讓對方,你當即向右邊走,同時,他當即向左邊走。結果,又撞上了!不過介於人類的智能,我相信這個動做重複兩三次後,你應該能夠順利解決這個問題。由於這個時候,你們都會本能地對視,進行交流,保證這種狀況再也不發生。

但若是這種狀況發生在兩個線程之間可能就不會那麼幸運了。若是線程的智力不夠,且都秉承着「謙讓」的原則,主動將資源釋放給他人使用,那麼就會致使資源不斷地在兩個線程間跳動,而沒有一個線程能夠同時拿到全部資源正常執行。這種狀況就是活鎖。

同步(Synchronous)和異步(Asynchronous)

這邊討論的同步和異步指的是同步方法和異步方法。

同步方法是指調用這個方法後,調用方必須等到這個方法執行完成以後才能繼續往下執行。
異步方法是指調用這個方法後會立馬返回,調用方能立馬往下繼續執行。被調用的異步方法實際上是由另外的線程進行執行的,若是這個異步方法有返回值的話能夠經過某種通知的方式告知調用方。

實現異步方法的方式:

  • 回調函數模式:一個方法被調用後立馬返回,調用結果經過回調函數返回給調用方;
  • MQ(發佈/訂閱):請求方將請求發送到MQ,請求處理方監聽MQ處理這些請求,並將請求處理結果也返回給某個MQ,調用方監聽這個Queue獲取處理結果;
  • 多線程處理模式:系統建立其餘線程處理調用請求,好比Spring中的@Async註解標註的方法就是這種方法。

臨界區

涉及讀寫共享資源的代碼片斷叫「臨界區」。

好比下面代碼中,1處和2處就是一個代碼臨界區。

private static class BankAccount{
        String accountName;
        double balance;

        public BankAccount(String accountName,double balance){
            this.accountName = accountName;
            this.balance = balance;
        }

        public synchronized   double deposit(double amount){
            balance = balance + amount; //1
            return balance;
        }

        public synchronized  double  withdraw(double amount){
            balance = balance - amount; //2
            return balance;
        }

    }

上下文切換

線程在CPU上運行以前須要CPU給這個線程分配時間片,當時間片運行完以後這個線程就會讓出CPU資源給其餘的線程運行。可是線程在將CPU資源讓出以前會保存當前的任務狀態以便下次得到CPU資源以後能夠繼續往下執行。因此線程從保存當前執行狀態到再加載的過程稱爲一次上下文切換。

減小上下文切換的措施

  • 無鎖併發編程。多線程競爭鎖時,會引發上下文切換,因此多線程處理數據時,能夠用一些辦法來避免使用鎖,如將數據的ID按照Hash算法取模分段,不一樣的線程處理不一樣段的數據。
  • CAS算法。Java的Atomic包使用CAS算法來更新數據,而不須要加鎖。
  • 使用最少線程。避免建立不須要的線程,好比任務不多,可是建立了不少線程來處理,這樣會形成大量線程都處於等待狀態。
  • 協程:在單線程裏實現多任務的調度,並在單線程裏維持多個任務間的切換。

多線程編程的優點和挑戰

使用併發編程的目的是讓程序運行的更快(更大限度的使用CPU資源,讓程序運行更快),可是在進行併發編程的過程也會遇到一些挑戰。

PS:多線程併發編程可讓咱們最大限度的使用系統的CPU資源,以達到讓程序運行更快的目的(不是全部狀況下多線程都更快)。可是一個硬幣具備兩面性,引入多線程編程會給咱們帶來其餘的問題,好比說線程的上下文切換問題、共享變量的線程安全問題、線程間通訊問題、線程死鎖問題和硬件資源對多線程的影響等問題。其實研究多線程併發編程就是在研究這對矛盾體,怎麼在享受多線程併發編程給咱們帶來便利的同時又能避開多線程帶來的坑。JDK中給咱們提供不少多線程相關的類

參考

  • http://blog.chinaunix.net/uid-31404751-id-5753869.html
  • https://blog.csdn.net/hanchao5272/article/details/79513153
  • 《實戰Java高併發程序設計》
  • 《Java併發編程藝術》
相關文章
相關標籤/搜索