1. 程式人生 > >java多執行緒總結(多處精摘,面試總結)

java多執行緒總結(多處精摘,面試總結)

      用多執行緒只有一個目的,那就是更好的利用cpu的資源,因為所有的多執行緒程式碼都可以用單執行緒來實現。說這個話其實只有一半對,因為反應“多角色”的程式程式碼,最起碼每個角色要給他一個執行緒吧,否則連實際場景都無法模擬,當然也沒法說能用單執行緒來實現:比如最常見的“生產者,消費者模型”。

很多人都對其中的一些概念不夠明確,如同步、併發等等,讓我們先建立一個數據字典,以免產生誤會。

  • 多執行緒:指的是這個程式(一個程序)執行時產生了不止一個執行緒
  • 並行與併發:
    • 並行:多個cpu例項或者多臺機器同時執行一段處理邏輯,是真正的同時。
    • 併發:通過cpu排程演算法,讓使用者看上去同時執行,實際上從cpu操作層面不是真正的同時。併發往往在場景中有公用的資源,那麼針對這個公用的資源往往產生瓶頸,我們會用TPS或者QPS來反應這個系統的處理能力。

併發和並行程式設計:

      併發: 兩隊同時用一個咖啡機

      並行:兩隊同時用兩個咖啡機

併發與並行

  • 執行緒安全:經常用來描繪一段程式碼。指在併發的情況之下,該程式碼經過多執行緒使用,執行緒的排程順序不影響任何結果。這個時候使用多執行緒,我們只需要關注系統的記憶體,cpu是不是夠用即可。反過來,執行緒不安全就意味著執行緒的排程順序會影響最終結果,如不加事務的轉賬程式碼:
    void transferMoney(User from, User to, float amount){
      to.setMoney(to.getBalance() + amount);
      from.setMoney(from.getBalance() - amount);
    }
  • 同步:Java中的同步指的是通過人為的控制和排程,保證共享資源的多執行緒訪問成為執行緒安全,來保證結果的準確。如上面的程式碼簡單加入@synchronized關鍵字。在保證結果準確的同時,提高效能,才是優秀的程式。執行緒安全的優先順序高於效能。

好了,讓我們開始吧。我準備分成幾部分來總結涉及到多執行緒的內容:

  1. 紮好馬步:執行緒的狀態
  2. 內功心法:每個物件都有的方法(機制)
  3. 太祖長拳:基本執行緒類
  4. 九陰真經:高階多執行緒控制類

紮好馬步:執行緒的狀態

執行緒有的幾種可用的狀態?   這也是面試中常問的一道題。具體分下下面5種:

1. 新建( new ):新建立了一個執行緒物件。

2. 可執行( runnable ):執行緒物件建立後,其他執行緒(比如 main 執行緒)呼叫了該物件 的 start ()方法。該狀態的執行緒位於可執行執行緒池中,等待被執行緒排程選中,獲 取 cpu 的使用權 。

3. 執行( running ):可執行狀態( runnable )的執行緒獲得了 cpu 時間片( timeslice ) ,執行程式程式碼。

4. 阻塞( block ):阻塞狀態是指執行緒因為某種原因放棄了 cpu 使用權,也即讓出了 cpu timeslice,暫時停止執行。直到執行緒進入可執行( runnable )狀態,才有 機會再次獲得 cpu timeslice 轉到執行( running )狀態。阻塞的情況分三種:

    (一). 等待阻塞:執行( running )的執行緒執行 o . wait ()方法, JVM 會把該執行緒放 入等待佇列(waitting queue )中。

    (二). 同步阻塞:執行( running )的執行緒在獲取物件的同步鎖時,若該同步鎖 被別的執行緒佔用,則 JVM 會把該執行緒放入鎖池( lock pool )中。

    (三). 其他阻塞: 執行( running )的執行緒執行 Thread . sleep ( long ms )或 t . join ()方法,或者發出了 I / O 請求時, JVM 會把該執行緒置為阻塞狀態。當 sleep ()狀態超時、 join ()等待執行緒終止或者超時、或者 I / O 處理完畢時,執行緒重新轉入可執行( runnable )狀態。

5. 死亡( dead ):執行緒 run ()、 main () 方法執行結束,或者因異常退出了 run ()方法,則該執行緒結束生命週期。死亡的執行緒不可再次復生。

                          

各種狀態一目瞭然,值得一提的是"blocked"這個狀態:
執行緒在Running的過程中可能會遇到阻塞(Blocked)情況

  1. 呼叫join()和sleep()方法,sleep()時間結束或被打斷,join()中斷,IO完成都會回到Runnable狀態,等待JVM的排程。
  2. 呼叫wait(),使該執行緒處於等待池(wait blocked pool),直到notify()/notifyAll(),執行緒被喚醒放到鎖定池(lock blocked pool ),釋放同步鎖使執行緒回到可執行狀態(Runnable)
  3. 對Running狀態的執行緒加同步鎖(Synchronized)使其進入(lock blocked pool ),同步鎖被釋放進入可執行狀態(Runnable)。

此外,在runnable狀態的執行緒是處於被排程的執行緒,此時的排程順序是不一定的。Thread類中的yield方法可以讓一個running狀態的執行緒轉入runnable。

內功心法:每個物件都有的方法(機制)

synchronized, wait, notify 是任何物件都具有的同步工具。讓我們先來了解他們。

一個Java監視器的模型:

                                

monitor(監視器)
他們是應用於同步問題的人工執行緒排程工具。講其本質,首先就要明確monitor的概念,Java中的每個物件都有一個監視器,來監測併發程式碼的重入。在非多執行緒編碼時該監視器不發揮作用,反之如果在synchronized 範圍內,監視器發揮作用。

wait/notify必須存在於synchronized塊中。並且,這三個關鍵字針對的是同一個監視器(某物件的監視器)。這意味著wait之後,其他執行緒可以進入同步塊執行。

當某程式碼並不持有監視器的使用權時(如圖中5的狀態,即脫離同步塊)wait或notify會丟擲IllegalMonitorStateException              也包括在synchronized塊中去呼叫另一個物件的wait/notify,因為不同物件的監視器不同,同樣會丟擲此異常。

在監視器(Monitor)內部,是如何做執行緒同步的?程式應該做哪種級別的同步?(面試)

監視器和鎖在Java虛擬機器中是一塊使用的。監視器監視一塊同步程式碼塊,確保一次==只有一個執行緒執行同步程式碼塊==。每一個監視器都和一個物件引用相關聯。執行緒在獲取鎖之前不允許執行同步程式碼。

再講用法:

  • synchronized單獨使用:

1.程式碼塊:如下,在多執行緒環境下,synchronized塊中的方法獲取了lock例項的monitor,如果例項相同,那麼只有一個執行緒能執行該塊內容

public class Thread1 implements Runnable {
   Object lock;
   public void run() {  
       synchronized(lock){
         ..do something
       }
   }
}

2.直接用於方法: 相當於上面程式碼中用lock來鎖定的效果,實際獲取的是Thread1類的monitor。更進一步,如果修飾的是static方法,則鎖定該類所有例項。

public class Thread1 implements Runnable {
   public synchronized void run() {  
        ..do something
   }
}

同步方法和同步程式碼塊的區別是什麼?(面試)

為何使用同步?

java允許多執行緒併發控制,當多個執行緒同時操作一個可共享的資源變數時(增刪改查),將會導致資料的不準確,相互之間產生衝突,因此加入同步鎖以避免在該執行緒沒有完成操作之前,被其他執行緒的呼叫,從而保證了該變數的唯一性和準確性。

區別

同步方法預設用this或者當前類class物件作為鎖;

同步程式碼塊可以選擇以什麼來加鎖,比同步方法要更細顆粒度,我們可以選擇只同步會發生同步問題的部分程式碼而不是整個方法;

同步方法使用關鍵字 synchronized修飾方法,而同步程式碼塊主要是修飾需要進行同步的程式碼,用synchronized(object){程式碼內容}進行修飾;

  • synchronized, wait, notify結合:典型場景生產者消費者問題
    /**
       * 生產者生產出來的產品交給店員
       */
      public synchronized void produce()        //直接用於方法    
      {
          if(this.product >= MAX_PRODUCT)
          {
              try
              {
                  wait();  
                  System.out.println("產品已滿,請稍候再生產");
              }
              catch(InterruptedException e)
              {
                  e.printStackTrace();
              }
              return;
          }
    
          this.product++;
          System.out.println("生產者生產第" + this.product + "個產品.");
          notifyAll();   //通知等待區的消費者可以取出產品了(恢復鎖)
      }
    
      /**
       * 消費者從店員取產品
       */
      public synchronized void consume()
      {
          if(this.product <= MIN_PRODUCT)
          {
              try 
              {
                  wait(); 
                  System.out.println("缺貨,稍候再取");
              } 
              catch (InterruptedException e) 
              {
                  e.printStackTrace();
              }
              return;
          }
    
          System.out.println("消費者取走了第" + this.product + "個產品.");
          this.product--;
          notifyAll();   //通知等待區的生產者可以生產產品了
      }

volatile

多執行緒的記憶體模型:main memory(主存)、working memory(執行緒棧),在處理資料時,執行緒會把值從主存load到本地棧,完成操作後再save回去(volatile關鍵詞的作用:每次針對該變數的操作都激發一次load and save。

               

針對多執行緒使用的變數如果不是volatile或者final修飾的,很有可能產生不可預知的結果(另一個執行緒修改了這個值,但是之後在某執行緒看到的是修改之前的值)。其實道理上講同一例項的同一屬性本身只有一個副本。但是多執行緒是會快取值的,本質上,volatile就是不去快取,直接取值。線上程安全的情況下加volatile會犧牲效能。

太祖長拳:基本執行緒類

基本執行緒類指的是Thread類,Runnable介面,Callable介面
Thread 類實現了Runnable介面,啟動一個執行緒的方法:

 MyThread my = new MyThread();
  my.start();

Thread類相關方法:

start();//啟動執行緒

getId();//獲得執行緒ID

getName();//獲得執行緒名字

getPriority();//獲得優先權

isAlive();//判斷執行緒是否活動

isDaemon();//判斷是否守護執行緒

getState();//獲得執行緒狀態

sleep(long mill);//休眠執行緒

join();//等待執行緒結束

yield();//放棄cpu使用權利

interrupt();//中斷執行緒

currentThread();//獲得正在執行的執行緒物件

關於中斷:它並不像stop方法那樣會中斷一個正在執行的執行緒。執行緒會不時地檢測中斷標識位,以判斷執行緒是否應該被中斷(中斷標識值是否為true)。終端只會影響到wait狀態、sleep狀態和join狀態。被打斷的執行緒會丟擲InterruptedException。
Thread.interrupted()檢查當前執行緒是否發生中斷,返回boolean
synchronized在獲鎖的過程中是不能被中斷的。

中斷是一個狀態!interrupt()方法只是將這個狀態置為true而已。所以說正常執行的程式不去檢測狀態,就不會終止,而wait等阻塞方法會去檢查並丟擲異常。如果在正常執行的程式中新增while(!Thread.interrupted()) ,則同樣可以在中斷後離開程式碼體

Thread類最佳實踐
寫的時候最好要設定執行緒名稱 Thread.name,並設定執行緒組 ThreadGroup,目的是方便管理。在出現問題的時候,列印執行緒棧 (jstack -pid) 一眼就可以看出是哪個執行緒出的問題,這個執行緒是幹什麼的。

如何獲取執行緒中的異常

不能用try,catch來獲取執行緒中的異常

Runnable

與Thread類似

Callable

future模式:併發模式的一種,可以有兩種形式,即無阻塞和阻塞,分別是isDone和get。其中Future物件用來存放該執行緒的返回值以及狀態

ExecutorService e = Executors.newFixedThreadPool(3);
 //submit方法有多重引數版本,及支援callable也能夠支援runnable介面型別.
Future future = e.submit(new myCallable());
future.isDone() //return true,false 無阻塞
future.get() // return 返回值,阻塞直到該執行緒執行結束

什麼是死鎖(deadlock)?(面試)

所謂死鎖是指多個程序因==競爭資源==而造成的一種僵局(互相等待),若無外力作用,這些程序都將無法向前推進。死鎖產生的4個必要條件:

互斥條件:程序要求對所分配的資源(如印表機)進行排他性控制,即在一段時間內某 資源僅為一個程序所佔有。此時若有其他程序請求該資源,則請求程序只能等待。

不剝奪條件:程序所獲得的資源在未使用完畢之前,不能被其他程序強行奪走,即只能 由獲得該資源的程序自己來釋放(只能是主動釋放)。

請求和保持條件:程序已經保持了至少一個資源,但又提出了新的資源請求,而該資源 已被其他程序佔有,此時請求程序被阻塞,但對自己已獲得的資源保持不放。

迴圈等待條件:存在一種程序資源的==迴圈等待鏈==,鏈中每一個程序已獲得的資源同時被鏈中下一個程序所請求。

如何確保N個執行緒可以訪問N個資源同時又不導致死鎖?

使用多執行緒的時候,一種非常簡單的避免死鎖的方式就是:==指定獲取鎖的順序==,並強制執行緒按照指定的順序獲取鎖。因此,如果所有的執行緒都是以同樣的順序加鎖和釋放鎖,就不會出現死鎖了。