前言
上篇文章講了線程安全問題,要保證原子性,可見性和有序性的操作才能保證線程安全。也講到了synchronized、volatile,本章講講這些是什么。
什么是鎖
首先要知道一個東西,鎖(Lock)。鎖,大家都知道吧,把東西鎖起來不讓別人拿到,直到把鎖打開,才可以拿到。把東西比作數(shù)據(jù),把人比作線程,多線程間能對同個數(shù)據(jù)進行操作是不安全的,但是鎖能保證你當前只有一個線程能對數(shù)據(jù)操作,直到線程操作完,下個線程接著操作。這就是鎖的作用,讓多個線程更好地協(xié)作,避免多個線程的操作交錯導致數(shù)據(jù)異常的問題。接下來看看鎖的一些特點和問題,一定要耐心看完對鎖才能進一步了解。
鎖的特點
臨界區(qū)
當我們給東西上鎖后,進行操作,操作完再把鎖打開,這個上鎖和解鎖的中間,就是臨界區(qū)。放在代碼中是這么解釋的,持有鎖的線程獲取鎖后和釋放鎖前執(zhí)行的代碼叫做臨界區(qū)。(看到后面怎么用鎖,大概就知道是什么意思了)排他性
還記得原子性是什么嗎?操作不可分割,也就是說整個操作只能是一個線程去執(zhí)行,那這個操作的范圍是多大呢?就是剛才說的臨界區(qū)。總結(jié)一點:排他性能夠保障一個共享變量在任一時刻只能被一個線程訪問,這就保證了臨界區(qū)代碼一次只能夠被一個線程執(zhí)行,臨界區(qū)的操作具有不可分割性串行
在沒有鎖的時候,多線程下并發(fā)對數(shù)據(jù)進行操作,這是很危險的。所以在有鎖的情況下,只能一個個線程訪問數(shù)據(jù),就體現(xiàn)出串行了三種保障
鎖能夠保護共享變量實現(xiàn)線程安全,它的作用包括保障原子性、可見性和有序性。調(diào)度策略
鎖的調(diào)度策略分為公平策略和非公平策略,對應的鎖就叫公平鎖和非公平鎖。多線程情況下,會有多個線程要訪問數(shù)據(jù),只能有一個進去訪問,其他都在外面等待,但是公平策略情況下,這個等待是按照先到排前面的規(guī)則來執(zhí)行;不公平策略情況下,按照搶占式來搶占,沒有順序。公平鎖以增加上下文切換為代價,保障了鎖調(diào)度的公平性,增加了線程暫停和喚醒的可能性。
鎖的兩個問題
泄漏鎖
鎖泄漏是指一個線程獲得鎖后,由于程序的錯誤導致鎖一直無法被釋放,導致其他線程一直無法獲得該鎖。活躍性問題
鎖泄漏會導致活躍性問題,這些問題包括死鎖、和鎖死等。(后面會講到死鎖和鎖死的差別,別混淆了)
鎖的類型
鎖可分為內(nèi)部鎖(synchronized)、顯式鎖(ReentrantLock)、讀寫鎖(ReentrantReadWriteLock)、輕量級鎖(volatile)四種。
內(nèi)部鎖(synchronized)
synchronized是個關(guān)鍵字應該都有見過吧,它可以修飾方法(同步方法),也可以修飾代碼塊,也可以修飾靜態(tài)方法給它加鎖。
修飾實例方法
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("測試1") }
thread { log("測試2") }
}
//測試結(jié)果
Thread-292:測試1:1
Thread-292:測試1:2
Thread-292:測試1:3
Thread-292:測試1:4
Thread-292:測試1:5
Thread-293:測試2:6
Thread-293:測試2:7
Thread-293:測試2:8
Thread-293:測試2:9
Thread-293:測試2:10
由于用synchronized修飾了方法,測試2的線程需要等待測試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)
}
}
其中,修飾方法時候,鎖住的是當前類的字節(jié)碼文件;修飾代碼塊的時候,鎖住的是對象
還是拿圖出來比較好解釋
在多線程運行過程中, 線程會去先搶對象的監(jiān)視器(monitor) ,這個監(jiān)視器是對象獨有的,其實就相當于一把鑰匙,搶到了,那你就獲得了當前代碼塊兒的執(zhí)行權(quán)。
其他沒有搶到的線程會進入隊列(SynchronizedQueue)當中等待,等待當前線程執(zhí)行完后,釋放鎖。最后當前線程執(zhí)行完畢后通知出隊然后繼續(xù)重復當前過程。從 jvm 的角度來看 monitorenter 和 monitorexit 指令代表著代碼的執(zhí)行與結(jié)束 。
再來看看這個鎖的特點:
1、監(jiān)視器鎖:因為使用 synchronized 實現(xiàn)的線程同步是通過監(jiān)視器(monitor)來實現(xiàn)的,所以內(nèi)部鎖也叫監(jiān)視器鎖。
2、自動獲取/釋放:線程對同步代碼塊的鎖的申請和釋放由 JVM 內(nèi)部實施,線程在進入同步代碼塊前會自動獲取鎖,并在退出同步代碼塊時自動釋放鎖,這也是同步代碼塊被稱為內(nèi)部鎖的原因。(異常時也是會自動釋放的)
3、鎖定方法/類/對象:synchronized 關(guān)鍵字可以用來修飾方法,鎖住特定類和特定對象。
4、臨界區(qū):同步代碼塊就是內(nèi)部鎖的臨界區(qū),線程在執(zhí)行臨界區(qū)代碼前必須持有該臨界區(qū)的內(nèi)部鎖。
5、鎖句柄:內(nèi)部鎖鎖的對象就叫鎖句柄(就是剛才代碼里面的this或者lock對象),鎖句柄通常會用 private 和 final 關(guān)鍵字進行修飾。因為鎖句柄變量一旦改變,會導致執(zhí)行同一個同步代碼塊的多個線程實際上用的是不同的鎖。
6、不會泄漏:泄漏指的是鎖泄漏,內(nèi)部鎖不會導致鎖泄漏,因為 javac 編譯器把同步代碼塊編譯為字節(jié)碼時,對臨界區(qū)中可能拋出的異常做了特殊處理,這樣臨界區(qū)的代碼出了異常也不會妨礙鎖的釋放。
7、非公平鎖:內(nèi)部鎖是使用的是非公平策略,是非公平鎖,也就是不會增加上下文切換開銷。
顯式鎖(ReentrantLock)
ReentrantLock 的使用同 synchronized 有點不同,它的加鎖和解鎖操作都需要手動完成
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 不同,當異常發(fā)生時 synchronized 會自動釋放鎖,但是 ReentrantLock 并不會自動釋放鎖。因此好的方式是將 unlock 操作放在 finally 代碼塊中,保證任何時候鎖都能夠被正常釋放掉。
默認情況下,synchronized 和 ReentrantLock 都是非公平鎖。但是 ReentrantLock 可以通過傳入 true 來創(chuàng)建一個公平鎖。所謂公平鎖就是通過同步隊列來實現(xiàn)多個線程按照申請鎖的順序獲取鎖。
創(chuàng)建一個公平鎖:
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();
}
}
讀寫鎖和顯式鎖的差別在于,把讀和寫的操作細分出來,這樣在讀寫時不會發(fā)生沖突。
輕量級鎖(volatile)
volatile,用來修飾變量的關(guān)鍵字,當我們多線程對一個變量進行修改時,會有線程安全問題。volatile就是把這個變量改動時候會設(shè)立一個屏障,簡單點說,就是你要去取值時是寫的操作,那么你在取值前去把要把這個值修改的線程給攔住,你寫完之后,再加一層存儲的屏障,這屏障是為了讓你把修改的值更新到主存去,等更新完后兩個屏障一起去掉,之后用這個volatile修飾的變量,每次取值都要去主存中取,這樣就能保住讀的時候能讀到正取的數(shù)據(jù),也就保住了可見性。
注:volatile不能保證原子性,因為當一個變量進行x++的操作后實際是分為temp = x,temp = x + 1,x = temp,這三個操作,那么在中途另一個線程插進來了,值就不正確了。