併發編程之線程第二篇 3.12 五種狀態 3.13 六種狀態 4.1 共用帶來的問題 4.2 synchronized解決方案 4.4 變數的線程安全分析 4.6 Monitor概念 1. 輕量級鎖 2. 鎖膨脹 3.12 五種狀態 這是從操作系統層面來描述的 【初始狀態】僅是在語音層面創建了線程對 ...
併發編程之線程第二篇
3.12 五種狀態
這是從操作系統層面來描述的
- 【初始狀態】僅是在語音層面創建了線程對象,還未與操作系統線程關聯
- 【可運行狀態】(就緒狀態)指該線程已經被創建(與操作系統線程關聯),可以由CPU調度執行
- 【運行狀態】指獲取了CPU時間片運行中的狀態
(1)當CPU時間片用完,會從【運行狀態】轉換至【可運行狀態】,會導致線程的上下文切換 - 【阻塞狀態】
(1)如果調用了阻塞API,如BIO讀寫文件,這時該線程實際不會用到CPU,會導致線程上下文切換,進入【阻塞狀態】
(2)等BIO操作完畢,會由操作系統喚醒阻塞的線程,轉換至【可運行狀態】
(3)與【可運行狀態】的區別是,對【阻塞狀態】的線程來說只要它們一直不喚醒,調度器就一直不會考慮調度它們 - 【終止狀態】表示線程已經執行完畢,生命周期已經結束,不會再轉換為其它狀態
3.13 六種狀態
這是從Java API層面來描述的
根據Thread.State枚舉,分為六種狀態
- NEW 線程剛被創建,但是還沒有調用start()方法
- RUNNABLE 當調用了start()方法之後,註意,Java API層面的RUNNABLE狀態涵蓋了操作系統層面的【可運行狀態】、【運行狀態】和【阻塞狀態】(由於BIO導致的線程阻塞,在Java里無法區分,任然認為是可運行)
- BLOCKED、WAITING、TIMED_WAITING都是Java API層面對【阻塞狀態】的細分。
- TERMINATED當線程代碼運行結束
4.1 共用帶來的問題
Java的體現
兩個線程對初始值為0的靜態變數一個做自增,一個做自減,各做5000次,結果是0嗎?
問題分析
y以上的結果可能是正數、負數、零。為什麼呢?因為Java中對靜態變數的自增,自減並不是原子操作,要徹底理解,必須從位元組碼來進行分析
例如對於 i++而言(i為靜態變數),實際會產生如下的JVM位元組碼指令 :
getstatic i // 獲取靜態變數i的值
iconst_1 // 準備常量1
iadd // 自增
putstatic i // 將修改後的值存入靜態變數i
而對應 i-- 也是類似
getstatic i // 獲取靜態變數i的值
iconst_1 // 準備常量1
isub // 自減
putstatic i // 將修改後的值存入靜態變數i
而Java的記憶體模型如下,完成靜態變數的自增,自減需要在主存和工作記憶體中進行數據交換 :
如果是單線程以上8行代碼是順序執行(不會交錯)沒有問題 :
但多線程下這8行代碼可能交錯運行 :
出現負數的情況 :
出現正數的情況
臨界區Critical Section
- 一個程式運行多個線程本身是沒有問題的
- 問題出在多個線程訪問共用資源
- 多個線程讀共用資源其實也沒有問題
- 在多個線程對共用資源讀寫操作時發生指令交錯,就會出現問題
- 一段代碼塊如果存在對共用資源的多線程讀寫操作,稱這段代碼塊為臨界區
例如,下麵代碼中臨界區
競態條件 Race Condition
多個線程在臨界區內執行,由於代碼的執行序列不同而導致結果無法預測,稱之為發生了競態條件
4.2 synchronized解決方案
- 應用之互斥
為了避免臨界區的競態條件發生,有多種手段可以達到目的。 - 阻塞式的解決方案 : synchronized,Lock
- 非阻塞式的解決方案 : 原子變數
本次使用阻塞式的解決方案 : synchronized,來解決上述問題,即俗稱的【對象鎖】,它採用互斥的方式讓同一個時刻最多只有一個線程能持有【對象鎖】,其它線程再想獲取這個【對象鎖】時就會阻塞住。這樣就能保證擁有鎖的線程可以安全的執行臨界區內的代碼,不用擔心線程上下文切換
註意
雖然java中互斥和同步都可以採用synchronized關鍵字來完成,但它們還是有區別的 : - 互斥是保證臨界區的競態條件發生,同一時刻只能有一個線程執行臨界區代碼
- 同步是由於線程執行的先後、順序不同、需要一個線程等待其它線程運行到某個點
synchronized
語法
解決
所謂的線程八鎖
其實就是考察synchronized鎖住的是哪個對象
情況1 :
用圖來解釋
思考
synchronized實際是用對象鎖保證了臨界區內代碼的原子性,臨界區內的代碼對外是不可分割的,不會被線程切換所打斷。
為了加深理解,請思考下麵的問題
- 如果把synchronized(obj)放在for迴圈的外面,如何理解?-- 原子性
- 如果t1 synchronized(obj1) 而 t2 synchronized(obj2)會怎樣運作? – 鎖對象
- 如果t1 synchronized(obj) 而t2 沒有加會怎麼樣?如何理解? – 鎖對象
面向對象改進
把需要保護的共用變數放入一個類
4.4 變數的線程安全分析
成員變數和靜態變數是否線程安全?
- 如果它們沒有共用,則線程安全
- 如果它們被共用了,根據它們的狀態是否能夠改變,又分兩種情況
- 如果只有讀操作,則線程安全
- 如果有讀寫操作,則這段代碼是臨界區,需要考慮線程安全
局部變數是否線程安全?
- 局部變數是線程安全的
- 但局部變數引用的對象則未必
- 如果該對象沒有逃離方法的作用訪問,它是線程安全的
- 如果該對象逃離方法的作用範圍,需要考慮線程安全
局部變數線程安全分析
每個線程調用test1()方法時局部變數i,會在每個線程的棧幀記憶體中被創建多份,因此不存在共用
如圖
局部變數的引用稍有不同
先看一個成員變數的例子
其中一種情況是,如果線程2還未add,線程1remove就會報錯 :
分析 :
- 無論哪個線程中的method2引用的都是同一個對象中的list成員變數
- method3與method2分析相同
將list修改成局部變數
分析 : - list是局部變數,每個線程調用時會創建其不同實例,沒有共用
- 而method2的蠶食是從method1中傳遞過來的,與method1中引用同一個對象
- method3的參數分析與method2相同
方法訪問修飾符帶來的思考,如果把method2和method3的方法修改為public會不會代理線程安全問題? - 情況1 :有其它線程調用method2和method3
- 情況2 :在情況1的基礎上,為ThreadSafe類添加子類,子類覆蓋method2或method3方法。
從這裡例子中可以看出private或final提高【安全】的意義所在,請體會開閉原則中的【閉】
常見線程安全類 - String
- Integer
- StringBuffer
- Random
- Vector
- HashTable
- java.util.concurrent包下的類
這裡說它們是線程安全的是指,多個線程調用它們同一個實例的某個方法時,是線程安全的。也可以理解為 - 它們的每個方法是原子的
- 但註意它們多個方法的組合不是原子的,見後面分析
線程安全類方法的組合
分析下麵代碼是否線程安全?
不可變類線程安全性
String、Integer等都是不可變類,因為其內部的狀態不可以改變,因此它們的方法都是線程安全的。
其中foo的行為是不確定的,可能導致不安全的發生,被稱之為外星方法
請比較JDK中String類的實現,為什麼是final修飾的?因為防止子類去實現,這樣會引起線程安全問題。也是符合開閉原則
4.6 Monitor概念
Java對象頭
以32位虛擬機為例
普通對象
數組對象
其中Mark Word結構為
Monitor
Monitor被翻譯為監視器或管程
每個Java對象都可以關聯一個Monitor對象,如果使用synchronized給對象上鎖(重量級)之後,該對象頭的Mark Word中就被設置指向Monitor對象的指針
Monitor結構如下
- 剛開始Monitor中Owner為null
- 當Thread-2指向synchronized(obj)就會將Monitor的所有者Owner置為Thread-2,Monitor中只能有一個Owner
- 在Thread-2上鎖的過程中,如果Thread-3,Thread-4,Thread-5也來執行synchronized(obj),就會進入EntryList BLOCKED
- Thread-2執行完同步代碼塊的內容,然後喚醒EntryList中等待的線程來競爭鎖,競爭的時是非公平的
- 圖中WaitSet中的Thread-0,Thread-1是之前獲得過鎖,但條件不滿足進入WAITING狀態的線程,後面講wait-notify時會分析
註意 :- synchronized必須是進入同一個對象的monitor才有上述的效果
- 不加synchronized的對象不會關聯監視器,不遵從以上規則
原理之synchronized
對應的位元組碼
1. 輕量級鎖
輕量級鎖的使用場景 :如果一個對象雖然有多線程訪問,但多線程訪問的時間是錯開的(也就是沒有競爭),那麼可以使用輕量級鎖來優化
輕量級鎖來優化。
輕量級鎖對使用者是透明的,即語法任然是synchronized
假設有兩個方法同步塊,利用同一個對象加鎖
- 創建鎖記錄(Lock Record)對象,每個線程都的棧幀都會包含一個鎖記錄的結構,內部可以存儲鎖定對象的Mark Word
- 讓鎖記錄中Object reference指向鎖對象,並嘗試用cas替換Object的Mark Word,將Mark Word的值存入鎖記錄
- 如果cas替換成功,對象頭中存儲了鎖記錄地址和狀態 00,表示由該線程給對象加鎖,這時圖示如下
- 如果cas失敗,有兩種情況
- 如果是其他線程已經持有了該Object的輕量級鎖,這時錶面有競爭,進入膨脹過程
- 如果是自己執行了synchronized鎖重入,那麼再添加一條Lock Record作為重入的計數
- 當退出synchronized代碼塊(解鎖時)如果有取值為null的鎖記錄,表示有重入,這時重置鎖記錄,表示重入計數減一
- 當退出synchronized代碼塊(解鎖時)鎖記錄的值不為null,這時使用cas將Mark Word的值恢復給對象頭
- 成功,則解鎖成功
- 失敗,說明輕量級鎖進行了鎖膨脹或已經升級為重量級鎖,進入重量級鎖解鎖流程
2. 鎖膨脹
如果在嘗試加輕量級鎖的過程中,CAS操作無法成功,這時一種情況就是有其他線程為此對象加上了輕量級鎖(有競爭),這時需要進行鎖膨脹,將輕量級鎖變為重量級鎖。
- 當Thread-1進行輕量級加鎖時,Thread-0已結對該對象加了輕量級鎖
- 這時Thread-1加輕量級鎖失敗,進入鎖膨脹流程
- 即為Object對象申請Monitor鎖,讓Object指向重量級鎖地址
- 然後自己進入Monitor的EntryList BLOCKED
- 當Thread-0退出同步塊解鎖時,使用cas將Mark Word的值恢復給對象頭,失敗。這時會進入重量級解鎖流程,即 按照Monitor地址找到Monitor對象,設置Owner為null,喚醒EntryList中BLOCKED線程。