Java 基礎 - 多線程基礎

來源:互聯網
上載者:User

標籤:blocks   err   重入   基本類型   park   靜態   sys   final   ons   

並發

並發在單核和多核 CPU 上都存在, 對於單核 CPU,通過輪訓時間片的方式實現並發.

線程線程對象

利用Thread對象, 有兩種方式來建立並發程式:

  1. 直接建立並管理線程. 當程式要啟動一個非同步任務的時候, 直接建立一個線程.
  2. 將線程管理抽象出來, 把並發部分的任務交給 executor.
線程的建立

有兩種方式建立線程:

  1. 提供一個實現Runnable介面的對象.
  2. 子類化Thread.

兩種方法的優缺點?

Runnable 總體來說更好一點

  1. 使用Runnable 介面的方式更加靈活, 因為可以繼續子類化某個類
  2. Runnable 介面的方式可以適配 concurrent包中的進階線程管理 API
線程的基本狀態

線程有如下狀態:

  1. NEW: 線程已經建立, 但還沒有調用 start() 開始執行.
  2. RUNNABLE: 線程已經在 JVM 中開始運行, 但有可能在等待系統資源運行.
  3. BLOCKED: 線程在等待一個 monitor lock 以進入一個 synchronized 塊. 也有可能是這個線程剛執行完 wait(), 其他線程又在擷取 wait() 對象的 monitor lock.
  4. WAITING: 線程進入等待狀態, 以下方法會使得線程進入 wait() 狀態:

    • Object.wait()
    • join()
    • LockSupport.park
  5. TIMED_WAITING: 線程進入有時間限制的等待, 一下方法會使得線程進入此狀態:

    • Thread.Sleep
    • Object.wait(long)
    • Thread.join(long)
    • LockSupport.parkNanos
    • LockSupport.parkUntil
  6. TERMINATED: 線程執行結束.

注意: 

當線程 A 調用某個對象的 Synchronized 方法的時候, 線程就獲得了這個對象的 intrinsic lock, 線程的狀態是 RUNNABLE. 其他線程假如要擷取這把鎖, 就會進入 BLOCKED 狀態.

當線程 A 調用wait方法, 那線程A 會釋放這個對象的鎖(但是扔回持有其他對象的鎖, 假如有的話), 然後線程轉入 WAITING 狀態. (若之前很多個物件 pending 在這個 lock 上, 那麼, 進入 wait 後會喚醒其他某個線程麼?)

當其他某個線程 B 在同一個對象(也就是線程 A wait 釋放的同一把鎖)調用notify 或者 notifyAll時, 線程A 狀態由 WAITING 轉化為 BLOCKED. 此時, 線程A 並不會自動擷取到鎖或者狀態變成 RUNNABLE, 實際上, 線程 A 也要和其他被阻塞的線程一樣競爭這把鎖.

WAITING 和 BLOCKED 狀態都會阻止線程運行, 但是區別卻很大.

WAITING 狀態必須被其他線程調用notify從而顯式的轉化為 BLOCKED 狀態. WAITING 狀態從來不會直接轉化為 RUNNABLE.

當一個 RUNNABLE 線程釋放了鎖(正常結束或者waiting), 某個被阻塞的線程會自動被喚醒.

notify 和 notifyAll 區別?

notify 喚醒被同一把鎖 wait的第一個線程 notifyAll 喚醒同一把鎖 wait 的所有線程, 但是優先順序最高的先執行

Thread.sleep

Thread.sleep 導致當前線程暫時掛起一段時間, 其他線程可以有機會擷取到 CPU 時間.

兩個 API:

Thread.sleep(long ms)Thread.sleep(long ms, long ns)

時間並不精確, 由於底層 OS 實現的限制.

Sleep 可以被打斷, 當線程 A 在休眠, 而另外一個線程 B 調用 A.interrupt()時, 線程 A 就會拋出 InterruptedException

中斷中斷

interrupt 會停止當前線程的進行中的任務並且取做別的事情. 至於一個 thread 應該如何響應一個中斷則是由程式員決定的. 

響應中斷

根據當前任務的長短, 做不同的處理:

  1. 當一個線程正在頻繁的調用一個會拋出InterruptedException 的方法時, 它可以通過 try...catch 捕獲, 並在catch 中做處理. 有很多方法會拋出InterruptedException, 比如 Thread.sleepsleep的中斷行為被設計成為:終止當前操作並拋出異常.

    for (int i = 0; i < ary.length; i++) {    try {        Thread.sleep(4000);    } caatch (InterruptedException e) {        return;    }    System.out.println(importantInfo[i]);}
  2. 當一個線程在執行一個長時間任務, 並且這個任務並沒有拋出 InterruptedException 的時候. 那麼就需要不停地去檢測當前線程有沒有被中斷:

    for (int i = 0; i < inputs.length; i++) {    heavyCrunch(inputs[i]);    if (Thread.interrupted()) {        // or return;        throw new InterruptedException();    }}
中斷的標誌位

中斷機制是由內部標識中斷狀態的一個標誌控制的:

  • 當調用 Thread.interrupt時, 這個標誌會被設定. 
  • 當調用 Thread.interrupted時, 標誌會被清理.
  • 當調用 Thread.isInterrupted 查詢中斷狀態時, 標誌不變.
  • 任何方法假如因為 InterruptedException 而退出, 那麼中斷標誌會被清理(但是有可能立即被設定).
Join

join 方法讓一個線程可以等待另外一個線程執行結束後再往下執行. 和 sleep一樣, join 響應中斷的方式也是退出並且拋出InterruptedException.

線程同步

線程間通訊主要靠開放欄位的訪問或者欄位引用的對象. 會帶來兩個問題:

  1. 線程幹擾(thread interference)
  2. 記憶體一致性錯誤(memory consistency)

防止這兩種錯誤的機制就是 線程同步. 然而, 線程同步會帶來線程間競爭(thread contention). 饑餓和活鎖都是 線程競爭 的表現.

線程幹擾 - Thread Interference

指的是, 一個語句可能被虛擬機器拆分成很多步執行, 然而當兩個線程交叉執行時, 線程 A 的執行導致線程 B 的運行結果是不準確的. 比如, 線程 A 執行 c++, 線程 B 執行 c--, 開始兩個線程讀到 c 的值是0, 假如線程 B 在 A 之後執行完, 那麼結果是 -1 而不是 0.

記憶體一致性錯誤 - Memory Consistency Error

Memory Consistency Error 指的是線程對同一份資料卻有不一致的值. 防止這種錯誤的關鍵是保證 happens-before 關係. 比如:

    int count = 0;

線程 A 執行:

    count++;

線程 B 列印 count的值:

    System.out.println(count);

那麼線程 B 列印出的結果可能是 0, 因為線程 A 的自增操作並沒有和 B 的列印語句建立一個 happens-before 關係_.

happens-before 關係 保證的是, 某個語句所導致的記憶體寫入動作會對另外的語句可見. 

如何建立 happens-before 關係?

  1. 同一個線程, 前面的指令總比後面的指令先執行
  2. 釋放 monitor lock(離開 synchronized 代碼塊或者方法), 總是發生在擷取同一個 monitor lock(進入 synchronized 代碼塊或者方法)之前. 由於 happens-before 關係的傳播性, 釋放鎖的方法或者代碼塊, 總是比擷取鎖或者代碼塊之前執行. 
  3. 寫入volatetile 變數總是在讀取前執行. 
  4. 調用 Thread.start 建立兩種 happens-before 關係:
    • Thread.start前面的語句會在 Thread.start 之前執行
    • Thread.start前面的語句會在新線程語句之前執行
  5. 線程 A 結束並導致線程 B Thread.join返回, 線程 A 中的所有語句會線上程 B Thread.join後面的語句之前執行
線程同步方法

Java 語言層級提供了兩種方法:

  1. synchronized method
  2. synchronzied statements

注意:

  • 建構函式不能用 synchronized 修飾, 否則會發生編譯錯誤.
  • 對象的構造一定是在同一個線程中完成的.
  • 不要提早的泄露欄位的引用. 比如在建構函式添加下列的語句:

    instances.add(this) //就會導致建構函式並未完成, 卻將它通過 instances 暴露出去了.
Intrinsic Lock / Monitor Lock

同步的機制是建立在 intrinsic lock 又稱 monitor lock 之上的. Intrinsic Lock 作用是

  • 強制獨佔對象的狀態(只要一個線程有一個 intrinsic lock, 其他線程是擷取不到這個鎖的)
  • 建立一種 happens-before 關係
  • 保證了狀態更改的可見度.
synchronized method 中的鎖

當線程調用一個 synchronized method 的時候, 線程會自動獲得對象的 intrinsic lock. 並在下列情況釋放:

  • 方法正常返回
  • 有未捕獲異常發生

當線程調用一個 static synchronized method 的時候, 線程會擷取與對象關聯的 Class 對象的鎖. 所以靜態同步方法的鎖和執行個體鎖是不同的.

synchronzied statements

寫法:

 public void addName(String name) {        synchronized(this) {                lastName = name;                nameCount++;        }        nameList.add(name); }

注意: 在 synchronized method 或者 synchronzied statements 中要避免其他對象的同步代碼(方法或代碼塊).

同步代碼重入
  • 一個線程不可以獲得其他線程擁有的鎖
  • 一個線程可以擷取自身已經擁有的鎖
原子訪問

意思是: 不可打斷的操作

Java 中的原子操作:

  • 讀取或改變參考型別的引用
  • 讀取或改變基本類型(long 和 double 除外)
  • 讀取或改變所有volatile類型的變數(包括引用, longdouble)

原子操作不會被拆分, 所以不用擔心線程幹擾(Thread Interference)的問題, 但是卻仍然要注意記憶體一致性(Memory Consistency)的問題. 

使用volatile 可以避免記憶體一致性錯誤, 因為寫入volatile 變數建立了一種 happen-before 的關係: 寫入總比後續讀先

synchronized method 和 synchronized statements 會保證原子操作. 

LivenessDeadlock

死結描述了這樣一種情況: 兩個或者多個線程進入永遠的阻塞, 互相等待.

避免方法: 上鎖的順序相同.

如何排查? 通過 JStack 可以查看:

jstack <pid>

Java stack information for the threads listed above:==================================================="Thread-1":    at basic.DeadlockBower$Friend.bowBack(DeadlockBower.java:32)    - waiting to lock <0x0000000795706590> (a basic.DeadlockBower$Friend)    at basic.DeadlockBower$Friend.bow(DeadlockBower.java:28)    - locked <0x00000007957065d8> (a basic.DeadlockBower$Friend)    at basic.DeadlockBower$2.run(DeadlockBower.java:49)    at java.lang.Thread.run(Thread.java:745)"Thread-0":    at basic.DeadlockBower$Friend.bowBack(DeadlockBower.java:32)    - waiting to lock <0x00000007957065d8> (a basic.DeadlockBower$Friend)    at basic.DeadlockBower$Friend.bow(DeadlockBower.java:28)    - locked <0x0000000795706590> (a basic.DeadlockBower$Friend)    at basic.DeadlockBower$1.run(DeadlockBower.java:43)    at java.lang.Thread.run(Thread.java:745)Found 1 deadlock. 
Starvation

饑餓描述了這樣的情況: 線程無法長時間訪問不到共用資源, 從而無法取得進展

Livelock

描述的是: 兩個線程互相根據對方的行為做出響應, 導致各自沒有實質性的進展

Guarded Blocks

線程之間經常協調他們的行為, 其中最常用的協調方法是 guarded block:

public void guardedJoy() {    // Simple loop guard. Wastes processor time. Don‘t do this!    while (!joy) { }    System.out.println("Joy has been achieved"); }

上面的代碼通過不停 檢測 joy 的狀態來決定是否往下執行. 這樣非常的耗費 CPU 時間.

更好的應該用 Object.wait() 來掛起當前線程.

public synchronized void guardedJoy() {    // This guard only loops once for each special event, which may not be     // the event we‘re waiting for.   while (!joy) {        try {            wait();        } catch (InterruptedException e) {}   }   System.out.println("Joy and efficiency have been achieved!");}

值得注意的是, 確保 wait() 在一個迴圈中, 因為你不能保證:

  1. 是 InterruptedExcpetion 還是正常喚醒導致 wait() 結束
  2. 喚醒的線程是否將 joy 的值改變(尤其是在 notifyAll中)
消費者和生產者模式

解決了, 解耦了生產者和消費者, 並且更合理的運用了 CPU 時間.

不可變對象

不可變對象是那些構造後狀態無法被改變的對象. 由於它的不可變性, 他不會有 Thread interference 和 Memory inconsistent 等問題.

不可變對象的特徵:

  1. 不要設定 setter 方法.
  2. 所有的成員都設定成 final + private
  3. 不允許子類重寫方法. 簡單的方法是將類聲明前 + final
  4. 如果執行個體變數中有參考型別, 別讓他們被更改:

    • 不要提供更改他們的方法
    • 不要共用他們的引用, 包括

      • 不要在建構函式中直接引用參數
      • 不要將執行個體引用變數返回, 如果不得不, 則, 返回拷貝!

Java 基礎 - 多線程基礎

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.