前言:如何處理共享數(shù)據(jù)的安全問題?
讓每一個(gè)線程依次的去讀取這個(gè)共享數(shù)據(jù)嫡秕,這樣就不會有任何的數(shù)據(jù)安全問題了丸凭,因?yàn)槊看蚊總€(gè)線程所操作的都是最新的數(shù)據(jù),不會出現(xiàn)臟讀的現(xiàn)象茵瀑。synchronized關(guān)鍵字就是使每個(gè)線程依次排隊(duì)操作共享變量间驮,也就是用來處理共享數(shù)據(jù)的安全性問題。不過這種同步機(jī)制的效率很低马昨。
一竞帽、使用范圍
在Java代碼中,synchronized關(guān)鍵字可以用在代碼塊和方法中:
方法:
1.實(shí)例方法:被鎖的是該類的實(shí)例對象
public synchronized void method() {}
2.靜態(tài)方法:被鎖的是類對象
public static synchronized void method() {}
代碼塊:
1.實(shí)例對象:被鎖的是類的實(shí)例對象
synchronized (this) {}
2.class對象:被鎖的是類對象
synchronized (Synchroinzed.class) {}
3.任意實(shí)例對象Object :被鎖的是配置的實(shí)例對象
String lock = "1111";
synchronized (lock) {}
如果鎖的是類對象的話鸿捧,不管new多少個(gè)實(shí)例對象屹篓,他們都會被鎖住,即線程之間保證同步關(guān)系匙奴。其實(shí)無論對一個(gè)對象進(jìn)行加鎖還是對一個(gè)方法進(jìn)行加鎖堆巧,實(shí)際上都是對對象進(jìn)行加鎖。被加了鎖的對象就叫鎖對象泼菌,在Java中任何一個(gè)對象都能成為鎖對象谍肤。
二、synchronized的執(zhí)行原理
我們給代碼塊添加了synchronized關(guān)鍵字哗伯,查看字節(jié)碼文件谣沸,發(fā)現(xiàn)執(zhí)行同步代碼快就要先執(zhí)行monitorenter指令,退出的時(shí)候執(zhí)行monitorexit指令笋颤。
public class SynchronizedDemo {
public static void main(String[] args) {
synchronized (SynchronizedDemo.class) {}
method();
}
private static void method() {}
}
查看class字節(jié)碼:
任意一個(gè)對象都擁有自己的監(jiān)視器举农,當(dāng)這個(gè)對象由同步塊或者這個(gè)對象的同步方法調(diào)用時(shí)荆针,執(zhí)行方法的線程必須先獲取該對象的監(jiān)視器才能進(jìn)入同步塊和同步方法。如果沒有獲取到監(jiān)視器的線程將會被阻塞在同步塊和同步方法的入口處颁糟,進(jìn)入到BLOCKED(阻塞)狀態(tài)航背。
這樣我們可以得到:任意線程對一對象的訪問,首先要獲得該對象的監(jiān)視器棱貌,如果獲取失敗玖媚,該線程狀態(tài)變?yōu)锽LOCKED,進(jìn)入阻塞隊(duì)列婚脱,當(dāng)該對象的監(jiān)視器占有者釋放后今魔,在阻塞隊(duì)列中的線程就會有機(jī)會重新獲取該監(jiān)視器。
為什么只有一次monitorenter呢障贸?
答:因?yàn)?strong>synchronized具有重入性错森,即同一個(gè)鎖程中,線程不需要再次獲取同一把鎖篮洁。
三涩维、synchronized與JMM中的三大特性
synchronized具有原子性,可見性袁波,有序性瓦阐,具體內(nèi)容可以查看這篇文章。
四锋叨、synchronized的優(yōu)化
前面說了synchronized是保證同一時(shí)刻只有一個(gè)線程能夠獲得對象的監(jiān)視器(monitor),從而進(jìn)入到同步代碼快或者同步方法中宛篇,即表現(xiàn)為互斥性娃磺。那這種方式的效率肯定低下,每次只能過一個(gè)線程叫倍。我們得整點(diǎn)優(yōu)化來縮短獲取鎖的時(shí)間偷卧,這樣就算挨著執(zhí)行,但是每次執(zhí)行的速度很快吆倦。
先說說鎖的四種狀態(tài)听诸,級別從低到高依次是:無鎖狀態(tài)、偏向鎖狀態(tài)蚕泽、輕量級鎖狀態(tài)和重量級鎖狀態(tài)晌梨。 這幾個(gè)狀態(tài)會隨著競爭情況而逐漸升級桥嗤,鎖可以升級但不能降級。這種升級不降級的策略目的是為了提高獲得鎖和釋放鎖的效率仔蝌。
1.偏向鎖
大多數(shù)情況下泛领,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得敛惊。使用偏向鎖就是減少無競爭且只有一個(gè)線程使用鎖的情況下使用輕量級鎖產(chǎn)生的性能消耗渊鞋。
當(dāng)一個(gè)線程訪問同步塊并獲取鎖時(shí),會在對象頭和自己的棧幀中的鎖記錄里存儲偏向線程ID瞧挤,以后該線程在進(jìn)入和退出同步塊時(shí)就不需要進(jìn)行CAS操作來加鎖和解鎖了锡宋,只需要測試一下對象頭的Mark Word中是否存儲指向當(dāng)前線程的偏向鎖。
如果測試成功就表示線程已經(jīng)獲得了鎖特恬。如果測試失敗則還需要測試一下偏向鎖的標(biāo)識是否為1(標(biāo)識當(dāng)前是偏向鎖):如果沒有獲得偏向鎖执俩,使用CAS競爭鎖;如果獲得了偏向鎖鸵鸥,使用CAS將對象頭的偏向鎖指向當(dāng)前線程奠滑。這也就是說偏向鎖只有在初始化的時(shí)候進(jìn)行一次CAS操作即可。
偏向鎖使用了一種等到競爭才釋放鎖的機(jī)制妒穴,即當(dāng)其他線程嘗試競爭偏向鎖時(shí)宋税,持有偏向鎖的線程才會釋放鎖,線程不會主動(dòng)釋放偏向鎖讼油〗苋可如果有兩個(gè)線程進(jìn)行競爭的時(shí)候,偏向鎖就失效了升級稱為輕量級鎖了矮台。這種升級稱為鎖膨脹乏屯。
偏向鎖的撤銷:需要等待全局安全點(diǎn)(此時(shí)沒有正在執(zhí)行的字節(jié)碼),暫停擁有偏向鎖的線程瘦赫,然后檢查持有偏向鎖的線程是否還活著辰晕,如果沒活著,則將對象頭設(shè)置為無鎖确虱。如果線程活著含友,擁有偏向鎖的棧會被執(zhí)行,遍歷偏向?qū)ο蟮逆i記錄校辩,棧中的鎖記錄和對象頭的Mark Word要么重新偏向其他線程窘问,要么恢復(fù)到無鎖狀態(tài)或者標(biāo)記該對象不適合作為偏向鎖,最后喚醒暫停的線程宜咒。
如果某些同步代碼塊大多數(shù)情況下都是由兩個(gè)或以上的線程競爭的話惠赫,偏向鎖就是個(gè)累贅了,對于這種情況故黑,我們一開始關(guān)閉即可儿咱。
通過JVM參數(shù)關(guān)閉偏向鎖:-XX:-UseBiasedLocking=false
庭砍,那么程序默認(rèn)會進(jìn)入輕量級鎖狀態(tài)。
2.輕量級鎖
輕量級鎖是一種非阻塞同步的樂觀鎖概疆,因?yàn)檫@個(gè)過程并沒有掛起阻塞線程逗威,而是讓線程空循環(huán)等待,串行執(zhí)行岔冀。
輕量級鎖由偏向鎖膨脹而來:
- 線程在自己的棧幀中創(chuàng)建鎖記錄LockRecord(開辟位置)
- 將鎖對象的對象頭的MarkWord復(fù)制到線程剛剛創(chuàng)建的鎖記錄中(復(fù)制鎖信息)
- 將鎖記錄中的Owner指針指向鎖對象(讓線程指向鎖)
- 將鎖對象的對象頭的MarkWord替換為指向鎖記錄的指針(讓鎖指向線程)
- 鎖對象對象頭的Mark Word的鎖標(biāo)志位變成00凯旭,即表示輕量級鎖
一般輕量級鎖有自旋鎖和自適應(yīng)自旋鎖兩種:
1.自旋鎖
如果線程1持有鎖,而線程2來競爭的時(shí)候使套,線程2會在原地自旋罐呼,而不是阻塞。也就是說獲得鎖的線程1釋放鎖侦高,那這個(gè)線程2立馬就能獲得鎖嫉柴。線程在原地循環(huán)等待是會消耗cpu的,就相當(dāng)于執(zhí)行一個(gè)啥都沒有的for循環(huán)奉呛。實(shí)驗(yàn)表明计螺,大部分的同步代碼快執(zhí)行時(shí)間都很短,所以才有了自旋鎖瞧壮。
根據(jù)以上不難得出自旋鎖的問題:
- 如果同步代碼快執(zhí)行的很慢登馒,需要消耗大量的時(shí)間,此時(shí)在原地自旋等待的其他線程就十分耗cpu咆槽。
- 本來把一個(gè)線程的鎖釋放后陈轿,當(dāng)前線程是能夠獲得鎖的,可如果好幾個(gè)線程都在競爭秦忿,這就會導(dǎo)致一些線程獲取不到鎖麦射,還在原地循環(huán)等待消耗cpu,甚至一直獲取不到鎖灯谣。
基于問題2潜秋,我們可以給線程空循環(huán)設(shè)置一個(gè)次數(shù),如果線程循環(huán)超過這個(gè)次數(shù)的話使用自旋鎖就不合適了胎许,此時(shí)進(jìn)行鎖膨脹峻呛,將鎖升級為重量級鎖。默認(rèn)情況是10呐萨,可以通過-XX:PreBlockSpin
修改杀饵。
2.自適應(yīng)自旋鎖
自旋鎖的線程空循環(huán)等待的自旋次數(shù)并非固定的莽囤,而是動(dòng)態(tài)的根據(jù)實(shí)際情況來改變谬擦,這就是自適應(yīng)自旋鎖。即線程1剛獲得了一個(gè)鎖朽缎,當(dāng)它釋放鎖的后惨远,線程2獲得鎖谜悟。在線程2運(yùn)行的過程中,線程1又想獲得鎖了北秽,不過線程2沒有釋放鎖葡幸,線程1就自旋等待。JVM認(rèn)為贺氓,由于線程1剛剛獲得過該鎖蔚叨,那么線程1這次自旋也是很有可能能夠再次成功的獲得該鎖,所以會適當(dāng)?shù)难娱L線程1的自旋次數(shù)辙培。對應(yīng)的蔑水,如果對于某一個(gè)鎖,一個(gè)線程自旋后很少有機(jī)會獲得該鎖扬蕊,那么以后該線程要獲取該鎖時(shí)直接忽略掉自旋過程搀别,直接升級為重量級鎖,以免長時(shí)間自旋造成資源浪費(fèi)尾抑。
3.重量級鎖
輕量級鎖膨脹后成為重量級鎖歇父,依賴對象內(nèi)部的monitor鎖來實(shí)現(xiàn)。在jdk1.6之前監(jiān)視器鎖(monitor)可以認(rèn)為直接對應(yīng)底層操作系統(tǒng)中的互斥量(mutex)再愈。也就是說monitor依賴操作系統(tǒng)的MutexLock(互斥鎖)來實(shí)現(xiàn)榜苫,所以重量級鎖也稱為互斥鎖。這種同步方式的成本非常高践磅,包括系統(tǒng)調(diào)用引起的內(nèi)核態(tài)與用戶態(tài)切換单刁、線程阻塞造成的線程切換等。故這種監(jiān)視器鎖就被稱為重量級鎖府适。
為什么說重量級鎖的開銷大羔飞?
答:當(dāng)系統(tǒng)檢查到鎖是重量級鎖后,會把等待想要獲得鎖的線程進(jìn)行阻塞檐春,被阻塞的線程不消耗cpu逻淌,但是阻塞或者喚醒一個(gè)線程時(shí)都需要操作系統(tǒng)來幫忙,即從用戶態(tài)轉(zhuǎn)換到內(nèi)核態(tài)疟暖。這個(gè)轉(zhuǎn)換是要消耗很多時(shí)間的卡儒,有可能比用戶執(zhí)行代碼的時(shí)間還要長。
小結(jié):synchronized并非一開始就給該對象加上重量級鎖俐巴,而是從偏向鎖到輕量級鎖再到重量級鎖的演變骨望。假如我們一開始就知道某個(gè)同步代碼塊競爭很激烈的話,那么我們一開始就要使用重量級鎖欣舵,從而減少鎖轉(zhuǎn)換的開銷擎鸠。如果我們只有一個(gè)線程在運(yùn)行,那偏向鎖則是一個(gè)很好的選擇缘圈。而當(dāng)某個(gè)同步代碼塊競爭不是那么很激烈的時(shí)候劣光,我們就可以考慮使用輕量級鎖袜蚕。