標籤:blocks err 重入 基本類型 park 靜態 sys final ons
並發
並發在單核和多核 CPU 上都存在, 對於單核 CPU,通過輪訓時間片的方式實現並發.
線程線程對象
利用Thread對象, 有兩種方式來建立並發程式:
- 直接建立並管理線程. 當程式要啟動一個非同步任務的時候, 直接建立一個線程.
- 將線程管理抽象出來, 把並發部分的任務交給
executor.
線程的建立
有兩種方式建立線程:
- 提供一個實現
Runnable介面的對象.
- 子類化
Thread.
兩種方法的優缺點?
Runnable 總體來說更好一點
- 使用
Runnable 介面的方式更加靈活, 因為可以繼續子類化某個類
Runnable 介面的方式可以適配 concurrent包中的進階線程管理 API
線程的基本狀態
線程有如下狀態:
NEW: 線程已經建立, 但還沒有調用 start() 開始執行.
RUNNABLE: 線程已經在 JVM 中開始運行, 但有可能在等待系統資源運行.
BLOCKED: 線程在等待一個 monitor lock 以進入一個 synchronized 塊. 也有可能是這個線程剛執行完 wait(), 其他線程又在擷取 wait() 對象的 monitor lock.
WAITING: 線程進入等待狀態, 以下方法會使得線程進入 wait() 狀態:
Object.wait()
join()
LockSupport.park
TIMED_WAITING: 線程進入有時間限制的等待, 一下方法會使得線程進入此狀態:
Thread.Sleep
Object.wait(long)
Thread.join(long)
LockSupport.parkNanos
LockSupport.parkUntil
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 應該如何響應一個中斷則是由程式員決定的.
響應中斷
根據當前任務的長短, 做不同的處理:
當一個線程正在頻繁的調用一個會拋出InterruptedException 的方法時, 它可以通過 try...catch 捕獲, 並在catch 中做處理. 有很多方法會拋出InterruptedException, 比如 Thread.sleep, sleep的中斷行為被設計成為:終止當前操作並拋出異常.
for (int i = 0; i < ary.length; i++) { try { Thread.sleep(4000); } caatch (InterruptedException e) { return; } System.out.println(importantInfo[i]);}
當一個線程在執行一個長時間任務, 並且這個任務並沒有拋出 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.
線程同步
線程間通訊主要靠開放欄位的訪問或者欄位引用的對象. 會帶來兩個問題:
- 線程幹擾(thread interference)
- 記憶體一致性錯誤(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 關係?
- 同一個線程, 前面的指令總比後面的指令先執行
- 釋放 monitor lock(離開
synchronized 代碼塊或者方法), 總是發生在擷取同一個 monitor lock(進入 synchronized 代碼塊或者方法)之前. 由於 happens-before 關係的傳播性, 釋放鎖的方法或者代碼塊, 總是比擷取鎖或者代碼塊之前執行.
- 寫入
volatetile 變數總是在讀取前執行.
- 調用
Thread.start 建立兩種 happens-before 關係:
Thread.start前面的語句會在 Thread.start 之前執行
Thread.start前面的語句會在新線程語句之前執行
- 線程 A 結束並導致線程 B
Thread.join返回, 線程 A 中的所有語句會線上程 B Thread.join後面的語句之前執行
線程同步方法
Java 語言層級提供了兩種方法:
- synchronized method
- synchronzied statements
注意:
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類型的變數(包括引用, long, double)
原子操作不會被拆分, 所以不用擔心線程幹擾(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() 在一個迴圈中, 因為你不能保證:
- 是
InterruptedExcpetion 還是正常喚醒導致 wait() 結束
- 喚醒的線程是否將
joy 的值改變(尤其是在 notifyAll中)
消費者和生產者模式
解決了, 解耦了生產者和消費者, 並且更合理的運用了 CPU 時間.
不可變對象
不可變對象是那些構造後狀態無法被改變的對象. 由於它的不可變性, 他不會有 Thread interference 和 Memory inconsistent 等問題.
不可變對象的特徵:
- 不要設定
setter 方法.
- 所有的成員都設定成
final + private
- 不允許子類重寫方法. 簡單的方法是將類聲明前 +
final
如果執行個體變數中有參考型別, 別讓他們被更改:
- 不要提供更改他們的方法
不要共用他們的引用, 包括
- 不要在建構函式中直接引用參數
- 不要將執行個體引用變數返回, 如果不得不, 則, 返回拷貝!
Java 基礎 - 多線程基礎