并發(fā)編程之線程第二篇
- 3.12 五種狀態(tài)
- 3.13 六種狀態(tài)
- 4.1 共享帶來的問題
- 4.2 synchronized解決方案
- 4.4 變量的線程安全分析
- 4.6 Monitor概念
- 1. 輕量級鎖
- 2. 鎖膨脹
3.12 五種狀態(tài)
這是從操作系統(tǒng)層面來描述的
- 【初始狀態(tài)】僅是在語音層面創(chuàng)建了線程對象介袜,還未與操作系統(tǒng)線程關(guān)聯(lián)
- 【可運行狀態(tài)】(就緒狀態(tài))指該線程已經(jīng)被創(chuàng)建(與操作系統(tǒng)線程關(guān)聯(lián))浇辜,可以由CPU調(diào)度執(zhí)行
- 【運行狀態(tài)】指獲取了CPU時間片運行中的狀態(tài)
(1)當(dāng)CPU時間片用完诞外,會從【運行狀態(tài)】轉(zhuǎn)換至【可運行狀態(tài)】慢洋,會導(dǎo)致線程的上下文切換 - 【阻塞狀態(tài)】
(1)如果調(diào)用了阻塞API,如BIO讀寫文件赖临,這時該線程實際不會用到CPU,會導(dǎo)致線程上下文切換,進入【阻塞狀態(tài)】
(2)等BIO操作完畢卒稳,會由操作系統(tǒng)喚醒阻塞的線程,轉(zhuǎn)換至【可運行狀態(tài)】
(3)與【可運行狀態(tài)】的區(qū)別是他巨,對【阻塞狀態(tài)】的線程來說只要它們一直不喚醒充坑,調(diào)度器就一直不會考慮調(diào)度它們 - 【終止?fàn)顟B(tài)】表示線程已經(jīng)執(zhí)行完畢,生命周期已經(jīng)結(jié)束染突,不會再轉(zhuǎn)換為其它狀態(tài)
3.13 六種狀態(tài)
這是從Java API層面來描述的
根據(jù)Thread.State枚舉捻爷,分為六種狀態(tài)
- NEW 線程剛被創(chuàng)建,但是還沒有調(diào)用start()方法
- RUNNABLE 當(dāng)調(diào)用了start()方法之后份企,注意也榄,Java API層面的RUNNABLE狀態(tài)涵蓋了操作系統(tǒng)層面的【可運行狀態(tài)】、【運行狀態(tài)】和【阻塞狀態(tài)】(由于BIO導(dǎo)致的線程阻塞司志,在Java里無法區(qū)分甜紫,任然認為是可運行)
- BLOCKED、WAITING骂远、TIMED_WAITING都是Java API層面對【阻塞狀態(tài)】的細分囚霸。
- TERMINATED當(dāng)線程代碼運行結(jié)束
4.1 共享帶來的問題
Java的體現(xiàn)
兩個線程對初始值為0的靜態(tài)變量一個做自增,一個做自減激才,各做5000次拓型,結(jié)果是0嗎?
問題分析
y以上的結(jié)果可能是正數(shù)瘸恼、負數(shù)劣挫、零。為什么呢钞脂?因為Java中對靜態(tài)變量的自增揣云,自減并不是原子操作,要徹底理解冰啃,必須從字節(jié)碼來進行分析
例如對于 i++而言(i為靜態(tài)變量)邓夕,實際會產(chǎn)生如下的JVM字節(jié)碼指令 :
getstatic i // 獲取靜態(tài)變量i的值
iconst_1 // 準(zhǔn)備常量1
iadd // 自增
putstatic i // 將修改后的值存入靜態(tài)變量i
而對應(yīng) i-- 也是類似
getstatic i // 獲取靜態(tài)變量i的值
iconst_1 // 準(zhǔn)備常量1
isub // 自減
putstatic i // 將修改后的值存入靜態(tài)變量i
而Java的內(nèi)存模型如下,完成靜態(tài)變量的自增阎毅,自減需要在主存和工作內(nèi)存中進行數(shù)據(jù)交換 :
如果是單線程以上8行代碼是順序執(zhí)行(不會交錯)沒有問題 :
但多線程下這8行代碼可能交錯運行 :
出現(xiàn)負數(shù)的情況 :
出現(xiàn)正數(shù)的情況
臨界區(qū)Critical Section
一個程序運行多個線程本身是沒有問題的
-
問題出在多個線程訪問共享資源
- 多個線程讀共享資源其實也沒有問題
- 在多個線程對共享資源讀寫操作時發(fā)生指令交錯焚刚,就會出現(xiàn)問題
-
一段代碼塊如果存在對共享資源的多線程讀寫操作,稱這段代碼塊為臨界區(qū)
例如扇调,下面代碼中臨界區(qū)
在這里插入圖片描述競態(tài)條件 Race Condition
多個線程在臨界區(qū)內(nèi)執(zhí)行矿咕,由于代碼的執(zhí)行序列不同而導(dǎo)致結(jié)果無法預(yù)測,稱之為發(fā)生了競態(tài)條件
4.2 synchronized解決方案
應(yīng)用之互斥
為了避免臨界區(qū)的競態(tài)條件發(fā)生,有多種手段可以達到目的碳柱。阻塞式的解決方案 : synchronized,Lock
非阻塞式的解決方案 : 原子變量
本次使用阻塞式的解決方案 : synchronized捡絮,來解決上述問題尸疆,即俗稱的【對象鎖】衰猛,它采用互斥的方式讓同一個時刻最多只有一個線程能持有【對象鎖】,其它線程再想獲取這個【對象鎖】時就會阻塞住途茫。這樣就能保證擁有鎖的線程可以安全的執(zhí)行臨界區(qū)內(nèi)的代碼瑞侮,不用擔(dān)心線程上下文切換
注意
雖然java中互斥和同步都可以采用synchronized關(guān)鍵字來完成的圆,但它們還是有區(qū)別的 :互斥是保證臨界區(qū)的競態(tài)條件發(fā)生,同一時刻只能有一個線程執(zhí)行臨界區(qū)代碼
-
同步是由于線程執(zhí)行的先后半火、順序不同越妈、需要一個線程等待其它線程運行到某個點
synchronized
語法
在這里插入圖片描述解決
在這里插入圖片描述在這里插入圖片描述所謂的線程八鎖
其實就是考察synchronized鎖住的是哪個對象
情況1 :
用圖來解釋
思考
synchronized實際是用對象鎖保證了臨界區(qū)內(nèi)代碼的原子性,臨界區(qū)內(nèi)的代碼對外是不可分割的钮糖,不會被線程切換所打斷梅掠。
為了加深理解,請思考下面的問題
- 如果把synchronized(obj)放在for循環(huán)的外面藐鹤,如何理解瓤檐?-- 原子性
- 如果t1 synchronized(obj1) 而 t2 synchronized(obj2)會怎樣運作? – 鎖對象
- 如果t1 synchronized(obj) 而t2 沒有加會怎么樣娱节?如何理解挠蛉? – 鎖對象
面向?qū)ο蟾倪M
把需要保護的共享變量放入一個類
在這里插入圖片描述
4.4 變量的線程安全分析
成員變量和靜態(tài)變量是否線程安全?
如果它們沒有共享肄满,則線程安全
-
如果它們被共享了谴古,根據(jù)它們的狀態(tài)是否能夠改變,又分兩種情況
- 如果只有讀操作稠歉,則線程安全
- 如果有讀寫操作掰担,則這段代碼是臨界區(qū),需要考慮線程安全
局部變量是否線程安全怒炸?
局部變量是線程安全的
-
但局部變量引用的對象則未必
如果該對象沒有逃離方法的作用訪問带饱,它是線程安全的
-
如果該對象逃離方法的作用范圍,需要考慮線程安全
局部變量線程安全分析
在這里插入圖片描述每個線程調(diào)用test1()方法時局部變量i阅羹,會在每個線程的棧幀內(nèi)存中被創(chuàng)建多份勺疼,因此不存在共享
在這里插入圖片描述如圖
在這里插入圖片描述局部變量的引用稍有不同
先看一個成員變量的例子
在這里插入圖片描述在這里插入圖片描述其中一種情況是,如果線程2還未add捏鱼,線程1remove就會報錯 :
在這里插入圖片描述分析 :
無論哪個線程中的method2引用的都是同一個對象中的list成員變量
-
method3與method2分析相同
在這里插入圖片描述將list修改成局部變量
在這里插入圖片描述分析 :
list是局部變量执庐,每個線程調(diào)用時會創(chuàng)建其不同實例,沒有共享
而method2的蠶食是從method1中傳遞過來的导梆,與method1中引用同一個對象
-
method3的參數(shù)分析與method2相同
在這里插入圖片描述方法訪問修飾符帶來的思考轨淌,如果把method2和method3的方法修改為public會不會代理線程安全問題迂烁?
情況1 :有其它線程調(diào)用method2和method3
-
情況2 :在情況1的基礎(chǔ)上,為ThreadSafe類添加子類递鹉,子類覆蓋method2或method3方法盟步。
[圖片上傳失敗...(image-6b1671-1581171046271)]從這里例子中可以看出private或final提高【安全】的意義所在,請體會開閉原則中的【閉】
常見線程安全類 String
Integer
StringBuffer
Random
Vector
HashTable
java.util.concurrent包下的類
這里說它們是線程安全的是指躏结,多個線程調(diào)用它們同一個實例的某個方法時址芯,是線程安全的。也可以理解為它們的每個方法是原子的
-
但注意它們多個方法的組合不是原子的窜觉,見后面分析
線程安全類方法的組合
分析下面代碼是否線程安全?
在這里插入圖片描述在這里插入圖片描述不可變類線程安全性
String北专、Integer等都是不可變類禀挫,因為其內(nèi)部的狀態(tài)不可以改變,因此它們的方法都是線程安全的拓颓。
在這里插入圖片描述在這里插入圖片描述其中foo的行為是不確定的语婴,可能導(dǎo)致不安全的發(fā)生,被稱之為外星方法
在這里插入圖片描述請比較JDK中String類的實現(xiàn)驶睦,為什么是final修飾的砰左?因為防止子類去實現(xiàn),這樣會引起線程安全問題场航。也是符合開閉原則
4.6 Monitor概念
Java對象頭
以32位虛擬機為例
普通對象
數(shù)組對象
其中Mark Word結(jié)構(gòu)為
Monitor
Monitor被翻譯為監(jiān)視器或管程
每個Java對象都可以關(guān)聯(lián)一個Monitor對象缠导,如果使用synchronized給對象上鎖(重量級)之后,該對象頭的Mark Word中就被設(shè)置指向Monitor對象的指針
Monitor結(jié)構(gòu)如下
- 剛開始Monitor中Owner為null
- 當(dāng)Thread-2指向synchronized(obj)就會將Monitor的所有者Owner置為Thread-2溉痢,Monitor中只能有一個Owner
- 在Thread-2上鎖的過程中僻造,如果Thread-3,Thread-4孩饼,Thread-5也來執(zhí)行synchronized(obj)髓削,就會進入EntryList BLOCKED
- Thread-2執(zhí)行完同步代碼塊的內(nèi)容,然后喚醒EntryList中等待的線程來競爭鎖镀娶,競爭的時是非公平的
- 圖中WaitSet中的Thread-0立膛,Thread-1是之前獲得過鎖,但條件不滿足進入WAITING狀態(tài)的線程梯码,后面講wait-notify時會分析
注意 :synchronized必須是進入同一個對象的monitor才有上述的效果
-
不加synchronized的對象不會關(guān)聯(lián)監(jiān)視器宝泵,不遵從以上規(guī)則
原理之synchronized
在這里插入圖片描述對應(yīng)的字節(jié)碼
在這里插入圖片描述
1. 輕量級鎖
輕量級鎖的使用場景 :如果一個對象雖然有多線程訪問,但多線程訪問的時間是錯開的(也就是沒有競爭)忍些,那么可以使用輕量級鎖來優(yōu)化
輕量級鎖來優(yōu)化鲁猩。
輕量級鎖對使用者是透明的,即語法任然是synchronized
假設(shè)有兩個方法同步塊罢坝,利用同一個對象加鎖
-
創(chuàng)建鎖記錄(Lock Record)對象廓握,每個線程都的棧幀都會包含一個鎖記錄的結(jié)構(gòu)搅窿,內(nèi)部可以存儲鎖定對象的Mark Word
在這里插入圖片描述 -
讓鎖記錄中Object reference指向鎖對象,并嘗試用cas替換Object的Mark Word隙券,將Mark Word的值存入鎖記錄
在這里插入圖片描述 -
如果cas替換成功男应,對象頭中存儲了鎖記錄地址和狀態(tài) 00,表示由該線程給對象加鎖娱仔,這時圖示如下
在這里插入圖片描述 -
如果cas失敗沐飘,有兩種情況
- 如果是其他線程已經(jīng)持有了該Object的輕量級鎖,這時表面有競爭牲迫,進入膨脹過程
-
如果是自己執(zhí)行了synchronized鎖重入耐朴,那么再添加一條Lock Record作為重入的計數(shù)
在這里插入圖片描述
-
當(dāng)退出synchronized代碼塊(解鎖時)如果有取值為null的鎖記錄,表示有重入盹憎,這時重置鎖記錄筛峭,表示重入計數(shù)減一
在這里插入圖片描述 -
當(dāng)退出synchronized代碼塊(解鎖時)鎖記錄的值不為null,這時使用cas將Mark Word的值恢復(fù)給對象頭
- 成功陪每,則解鎖成功
- 失敗影晓,說明輕量級鎖進行了鎖膨脹或已經(jīng)升級為重量級鎖,進入重量級鎖解鎖流程
2. 鎖膨脹
如果在嘗試加輕量級鎖的過程中檩禾,CAS操作無法成功挂签,這時一種情況就是有其他線程為此對象加上了輕量級鎖(有競爭),這時需要進行鎖膨脹盼产,將輕量級鎖變?yōu)橹亓考夋i饵婆。
-
當(dāng)Thread-1進行輕量級加鎖時,Thread-0已結(jié)對該對象加了輕量級鎖
在這里插入圖片描述 -
這時Thread-1加輕量級鎖失敗戏售,進入鎖膨脹流程
- 即為Object對象申請Monitor鎖啦辐,讓Object指向重量級鎖地址
-
然后自己進入Monitor的EntryList BLOCKED
在這里插入圖片描述
當(dāng)Thread-0退出同步塊解鎖時,使用cas將Mark Word的值恢復(fù)給對象頭蜈项,失敗芹关。這時會進入重量級解鎖流程,即 按照Monitor地址找到Monitor對象紧卒,設(shè)置Owner為null侥衬,喚醒EntryList中BLOCKED線程。