前言
上篇文章講了線程安全問題,要保證原子性,可見性和有序性的操作才能保證線程安全。也講到了synchronized、volatile,本章講講這些是什么。
什么是鎖
首先要知道一個(gè)東西,鎖(Lock)。鎖,大家都知道吧,把東西鎖起來(lái)不讓別人拿到,直到把鎖打開,才可以拿到。把東西比作數(shù)據(jù),把人比作線程,多線程間能對(duì)同個(gè)數(shù)據(jù)進(jìn)行操作是不安全的,但是鎖能保證你當(dāng)前只有一個(gè)線程能對(duì)數(shù)據(jù)操作,直到線程操作完,下個(gè)線程接著操作。這就是鎖的作用,讓多個(gè)線程更好地協(xié)作,避免多個(gè)線程的操作交錯(cuò)導(dǎo)致數(shù)據(jù)異常的問題。接下來(lái)看看鎖的一些特點(diǎn)和問題,一定要耐心看完對(duì)鎖才能進(jìn)一步了解。
鎖的特點(diǎn)
臨界區(qū)
當(dāng)我們給東西上鎖后,進(jìn)行操作,操作完再把鎖打開,這個(gè)上鎖和解鎖的中間,就是臨界區(qū)。放在代碼中是這么解釋的,持有鎖的線程獲取鎖后和釋放鎖前執(zhí)行的代碼叫做臨界區(qū)。(看到后面怎么用鎖,大概就知道是什么意思了)排他性
還記得原子性是什么嗎?操作不可分割,也就是說(shuō)整個(gè)操作只能是一個(gè)線程去執(zhí)行,那這個(gè)操作的范圍是多大呢?就是剛才說(shuō)的臨界區(qū)??偨Y(jié)一點(diǎn):排他性能夠保障一個(gè)共享變量在任一時(shí)刻只能被一個(gè)線程訪問,這就保證了臨界區(qū)代碼一次只能夠被一個(gè)線程執(zhí)行,臨界區(qū)的操作具有不可分割性串行
在沒有鎖的時(shí)候,多線程下并發(fā)對(duì)數(shù)據(jù)進(jìn)行操作,這是很危險(xiǎn)的。所以在有鎖的情況下,只能一個(gè)個(gè)線程訪問數(shù)據(jù),就體現(xiàn)出串行了三種保障
鎖能夠保護(hù)共享變量實(shí)現(xiàn)線程安全,它的作用包括保障原子性、可見性和有序性。調(diào)度策略
鎖的調(diào)度策略分為公平策略和非公平策略,對(duì)應(yīng)的鎖就叫公平鎖和非公平鎖。多線程情況下,會(huì)有多個(gè)線程要訪問數(shù)據(jù),只能有一個(gè)進(jìn)去訪問,其他都在外面等待,但是公平策略情況下,這個(gè)等待是按照先到排前面的規(guī)則來(lái)執(zhí)行;不公平策略情況下,按照搶占式來(lái)?yè)屨?,沒有順序。公平鎖以增加上下文切換為代價(jià),保障了鎖調(diào)度的公平性,增加了線程暫停和喚醒的可能性。
鎖的兩個(gè)問題
泄漏鎖
鎖泄漏是指一個(gè)線程獲得鎖后,由于程序的錯(cuò)誤導(dǎo)致鎖一直無(wú)法被釋放,導(dǎo)致其他線程一直無(wú)法獲得該鎖。活躍性問題
鎖泄漏會(huì)導(dǎo)致活躍性問題,這些問題包括死鎖、和鎖死等。(后面會(huì)講到死鎖和鎖死的差別,別混淆了)
鎖的類型
鎖可分為內(nèi)部鎖(synchronized)、顯式鎖(ReentrantLock)、讀寫鎖(ReentrantReadWriteLock)、輕量級(jí)鎖(volatile)四種。
內(nèi)部鎖(synchronized)
synchronized是個(gè)關(guān)鍵字應(yīng)該都有見過吧,它可以修飾方法(同步方法),也可以修飾代碼塊,也可以修飾靜態(tài)方法給它加鎖。
修飾實(shí)例方法
private var count = 0
@Synchronized
fun log(text: String) {
for (index in 0..4) {
count++
println(Thread.currentThread().name + ":" + text + ":" + count)
}
}
fun getLog(){
thread { log("測(cè)試1") }
thread { log("測(cè)試2") }
}
//測(cè)試結(jié)果
Thread-292:測(cè)試1:1
Thread-292:測(cè)試1:2
Thread-292:測(cè)試1:3
Thread-292:測(cè)試1:4
Thread-292:測(cè)試1:5
Thread-293:測(cè)試2:6
Thread-293:測(cè)試2:7
Thread-293:測(cè)試2:8
Thread-293:測(cè)試2:9
Thread-293:測(cè)試2:10
由于用synchronized修飾了方法,測(cè)試2的線程需要等待測(cè)試1的線程執(zhí)行完才可以執(zhí)行
修飾靜態(tài)方法
companion object{
private var count = 0
@Synchronized
fun log(text: String){
for (index in 0..4) {
count++
println(Thread.currentThread().name + ":" + text + ":" + count)
}
}
}
修飾代碼塊
private var count = 0
fun println1(text: String) {
synchronized(this) {
count++
println(Thread.currentThread().name + ":" + text + ":" + count)
}
}
//或者
private var count = 0
private val lock = Any()
fun println1(text: String) {
synchronized(lock) {
count++
println(Thread.currentThread().name + ":" + text + ":" + count)
}
}
其中,修飾方法時(shí)候,鎖住的是當(dāng)前類的字節(jié)碼文件;修飾代碼塊的時(shí)候,鎖住的是對(duì)象

還是拿圖出來(lái)比較好解釋
在多線程運(yùn)行過程中, 線程會(huì)去先搶對(duì)象的監(jiān)視器(monitor) ,這個(gè)監(jiān)視器是對(duì)象獨(dú)有的,其實(shí)就相當(dāng)于一把鑰匙,搶到了,那你就獲得了當(dāng)前代碼塊兒的執(zhí)行權(quán)。
其他沒有搶到的線程會(huì)進(jìn)入隊(duì)列(SynchronizedQueue)當(dāng)中等待,等待當(dāng)前線程執(zhí)行完后,釋放鎖。最后當(dāng)前線程執(zhí)行完畢后通知出隊(duì)然后繼續(xù)重復(fù)當(dāng)前過程。從 jvm 的角度來(lái)看 monitorenter 和 monitorexit 指令代表著代碼的執(zhí)行與結(jié)束 。
再來(lái)看看這個(gè)鎖的特點(diǎn):
1、監(jiān)視器鎖:因?yàn)槭褂?synchronized 實(shí)現(xiàn)的線程同步是通過監(jiān)視器(monitor)來(lái)實(shí)現(xiàn)的,所以內(nèi)部鎖也叫監(jiān)視器鎖。
2、自動(dòng)獲取/釋放:線程對(duì)同步代碼塊的鎖的申請(qǐng)和釋放由 JVM 內(nèi)部實(shí)施,線程在進(jìn)入同步代碼塊前會(huì)自動(dòng)獲取鎖,并在退出同步代碼塊時(shí)自動(dòng)釋放鎖,這也是同步代碼塊被稱為內(nèi)部鎖的原因。(異常時(shí)也是會(huì)自動(dòng)釋放的)
3、鎖定方法/類/對(duì)象:synchronized 關(guān)鍵字可以用來(lái)修飾方法,鎖住特定類和特定對(duì)象。
4、臨界區(qū):同步代碼塊就是內(nèi)部鎖的臨界區(qū),線程在執(zhí)行臨界區(qū)代碼前必須持有該臨界區(qū)的內(nèi)部鎖。
5、鎖句柄:內(nèi)部鎖鎖的對(duì)象就叫鎖句柄(就是剛才代碼里面的this或者lock對(duì)象),鎖句柄通常會(huì)用 private 和 final 關(guān)鍵字進(jìn)行修飾。因?yàn)殒i句柄變量一旦改變,會(huì)導(dǎo)致執(zhí)行同一個(gè)同步代碼塊的多個(gè)線程實(shí)際上用的是不同的鎖。
6、不會(huì)泄漏:泄漏指的是鎖泄漏,內(nèi)部鎖不會(huì)導(dǎo)致鎖泄漏,因?yàn)?javac 編譯器把同步代碼塊編譯為字節(jié)碼時(shí),對(duì)臨界區(qū)中可能拋出的異常做了特殊處理,這樣臨界區(qū)的代碼出了異常也不會(huì)妨礙鎖的釋放。
7、非公平鎖:內(nèi)部鎖是使用的是非公平策略,是非公平鎖,也就是不會(huì)增加上下文切換開銷。
顯式鎖(ReentrantLock)
ReentrantLock 的使用同 synchronized 有點(diǎn)不同,它的加鎖和解鎖操作都需要手動(dòng)完成
private int count = 0;
private ReentrantLock reentrantLock = new ReentrantLock();
public void print(String text) {
reentrantLock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":" + text + ":" + count);
} catch (Exception e) {
} finally {
reentrantLock.unlock();
}
}
lock() 和 unlock() 分別是加鎖和解鎖操作。ReentrantLock 與 synchronized 不同,當(dāng)異常發(fā)生時(shí) synchronized 會(huì)自動(dòng)釋放鎖,但是 ReentrantLock 并不會(huì)自動(dòng)釋放鎖。因此好的方式是將 unlock 操作放在 finally 代碼塊中,保證任何時(shí)候鎖都能夠被正常釋放掉。
默認(rèn)情況下,synchronized 和 ReentrantLock 都是非公平鎖。但是 ReentrantLock 可以通過傳入 true 來(lái)創(chuàng)建一個(gè)公平鎖。所謂公平鎖就是通過同步隊(duì)列來(lái)實(shí)現(xiàn)多個(gè)線程按照申請(qǐng)鎖的順序獲取鎖。
創(chuàng)建一個(gè)公平鎖:
private int count = 0;
private ReentrantLock reentrantLock = new ReentrantLock(true);
public void print(String text) {
reentrantLock.lock();
try {
System.out.println(Thread.currentThread().getName() + ":" + text + ":" + count);
} catch (Exception e) {
} finally {
reentrantLock.unlock();
}
}
讀寫鎖
private ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
public void test() {
// 讀操作
reentrantReadWriteLock.readLock().lock();
try {
} catch (Exception e) {
} finally {
reentrantReadWriteLock.readLock().unlock();
}
// 寫操作
reentrantReadWriteLock.writeLock().lock();
try {
} catch (Exception e) {
} finally {
reentrantReadWriteLock.writeLock().unlock();
}
}
讀寫鎖和顯式鎖的差別在于,把讀和寫的操作細(xì)分出來(lái),這樣在讀寫時(shí)不會(huì)發(fā)生沖突。
輕量級(jí)鎖(volatile)
volatile,用來(lái)修飾變量的關(guān)鍵字,當(dāng)我們多線程對(duì)一個(gè)變量進(jìn)行修改時(shí),會(huì)有線程安全問題。volatile就是把這個(gè)變量改動(dòng)時(shí)候會(huì)設(shè)立一個(gè)屏障,簡(jiǎn)單點(diǎn)說(shuō),就是你要去取值時(shí)是寫的操作,那么你在取值前去把要把這個(gè)值修改的線程給攔住,你寫完之后,再加一層存儲(chǔ)的屏障,這屏障是為了讓你把修改的值更新到主存去,等更新完后兩個(gè)屏障一起去掉,之后用這個(gè)volatile修飾的變量,每次取值都要去主存中取,這樣就能保住讀的時(shí)候能讀到正取的數(shù)據(jù),也就保住了可見性。
注:volatile不能保證原子性,因?yàn)楫?dāng)一個(gè)變量進(jìn)行x++的操作后實(shí)際是分為temp = x,temp = x + 1,x = temp,這三個(gè)操作,那么在中途另一個(gè)線程插進(jìn)來(lái)了,值就不正確了。