初識(shí) synchronized
在并發(fā)編程中,synchronized對(duì)我們來(lái)說(shuō)并不陌生幔嫂,我們都知道纱昧,當(dāng)多個(gè)線程并行的情況下,程序是不安全的缀去,這個(gè)不安全主要發(fā)生在共享變量的不安全侣灶,我們通過(guò)一個(gè)例子來(lái)說(shuō)明:
package com.zwx.concurrent;
public class TestSynchronized {
private static int count;
public static void increment(){
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
}
public static void main(String[] args) throws InterruptedException {
for (int i=0;i<1000;i++){
new Thread(()->TestSynchronized.increment()).start();
}
Thread.sleep(3000);
System.out.println("結(jié)果:" + count);
}
}
這里的輸出結(jié)果我們預(yù)期是1000,然而實(shí)際上并不一定會(huì)輸出1000缕碎,產(chǎn)生這種狀況的原因是存在如下場(chǎng)景:
1褥影、線程1獲取count為0,這時(shí)候他去執(zhí)行count++(非原子操作)
2咏雌、線程2又去獲取count,這時(shí)候因?yàn)榫€程A還沒(méi)有返回結(jié)果凡怎,所以依然獲取到0
3、線程1執(zhí)行count++后得到count=1赊抖,寫回內(nèi)存
4统倒、線程2執(zhí)行count++后得到count=1,寫回內(nèi)存
5氛雪、線程3去獲取count房匆,這時(shí)候獲取到count為1,然而實(shí)際上已經(jīng)執(zhí)行過(guò)2次count++操作了
假如線程是按照上面的1-5個(gè)步驟執(zhí)行的話,就會(huì)導(dǎo)致最后的結(jié)果不會(huì)輸出1000浴鸿,那么如何解決這個(gè)問(wèn)題呢井氢?就是在increment()方法上加上synchronized關(guān)鍵字
synchronized 用法
synchronized 有三種方式來(lái)加鎖,分別是:
- 修飾實(shí)例方法赚楚,作用于當(dāng)前實(shí)例加鎖毙沾,進(jìn)入同步代碼前要獲得當(dāng)前實(shí)例的鎖
public synchronized void test(){
System.out.println("修飾實(shí)例方法");
}
- 修飾靜態(tài)方法,作用于當(dāng)前類對(duì)象加鎖宠页,進(jìn)入同步代碼前要獲得當(dāng)前類對(duì)象的鎖
public static synchronized void test2(){
System.out.println("修飾靜態(tài)方法");
}
- 修飾代碼塊左胞,指定加鎖對(duì)象,對(duì)給定對(duì)象加鎖举户,進(jìn)入同步代碼庫(kù)前要獲得給定對(duì)象的鎖
public void test3(){
synchronized (this){
System.out.println("修飾代碼塊");
}
}
鎖是如何存儲(chǔ)的
我們每個(gè)人在學(xué)習(xí)java中接觸到的最多的一句話之一我想肯定是:一切皆對(duì)象烤宙。鎖就是一個(gè)對(duì)象,那么這個(gè)對(duì)象里面的結(jié)構(gòu)是怎么樣的呢俭嘁,鎖對(duì)象里面都保存了哪些信息呢躺枕?
在Hotspot 虛擬機(jī)中,對(duì)象在內(nèi)存中的存儲(chǔ)布局供填,可以分為三個(gè)區(qū)域:對(duì)象頭(Header)拐云、實(shí)例數(shù)據(jù)(Instance Data)、對(duì)齊填充(Padding)近她。synchronized用的鎖是存在Java對(duì)象頭里的叉瘩,Java對(duì)象頭里面包含兩部分信息:
第一部分官方稱之為“Mark Word” ,用于存儲(chǔ)自身的運(yùn)行時(shí)數(shù)據(jù),如:HashCode,GC分代年齡粘捎,鎖標(biāo)記薇缅、偏向鎖線程ID等;第二部分是類型指針攒磨,即對(duì)象指向它的類元信息泳桦,虛擬機(jī)通過(guò)這個(gè)指針來(lái)確定這個(gè)對(duì)象是哪個(gè)類的實(shí)例(如果java對(duì)象是一個(gè)數(shù)組,那么對(duì)象頭中還必須有一塊用于記錄數(shù)組長(zhǎng)度的數(shù)據(jù))
到這里我們就知道了娩缰,鎖是記錄在對(duì)象頭中的“Mark Word”灸撰,那么“Mark Word”又是如何存儲(chǔ)鎖的信息的呢?
在32位虛擬機(jī)中拼坎,“Mark Word”存儲(chǔ)結(jié)構(gòu)如下圖:
在64位虛擬機(jī)中浮毯,“Mark Word”存儲(chǔ)結(jié)構(gòu)如下圖:
synchronized 鎖升級(jí)
在多線程并發(fā)編程中synchronized 一直是元老級(jí)角色,很多人都會(huì)稱呼它為重量級(jí)鎖演痒。但是隨著Java SE 1.6 對(duì)synchronized 進(jìn)行了各種優(yōu)化之后,有些情況下它就并不那么重趋惨,Java SE 1.6 中為了減少獲得鎖和釋放鎖帶來(lái)的性能消耗而引入的偏向鎖和輕量級(jí)鎖鸟顺。
在Java SE 1.6中,鎖一共有4種狀態(tài),級(jí)別從低到高依次是:無(wú)鎖狀態(tài)讯嫂、偏向鎖狀態(tài)蹦锋、輕量級(jí)鎖狀態(tài)和重量級(jí)鎖狀態(tài),這幾個(gè)狀態(tài)會(huì)隨著競(jìng)爭(zhēng)情況逐漸升級(jí)欧芽。至于鎖的降級(jí)并沒(méi)有一個(gè)標(biāo)準(zhǔn)莉掂,在達(dá)到一定的苛刻條件之后可以進(jìn)行降級(jí),但是一般情況我們可以簡(jiǎn)單的認(rèn)為鎖不可以降級(jí)千扔,這里不做過(guò)多的敘述憎妙。
偏向鎖
HotSpot的作者經(jīng)過(guò)研究發(fā)現(xiàn),大多數(shù)情況下曲楚,鎖不僅不存在多線程競(jìng)爭(zhēng)厘唾,而且總是由同一線程多次獲得,所以為了讓線程獲得鎖的代價(jià)更低而引入了偏向鎖龙誊。
當(dāng)一個(gè)線程訪問(wèn)加了同步鎖的代碼塊時(shí)抚垃,會(huì)在對(duì)象頭中存儲(chǔ)當(dāng)前線程的 ID,后續(xù)這個(gè)線程進(jìn)入和退出這段加了同步鎖的代碼塊時(shí)趟大,不需要再次加鎖和釋放鎖鹤树。而是直接比較對(duì)象頭里面是否存儲(chǔ)了指向當(dāng)前線程的線程ID。如果相等表示偏向鎖是偏向于當(dāng)前線程的逊朽,就不需要再嘗試獲得鎖了罕伯。
偏向鎖的獲取
1、首先獲取鎖對(duì)象頭中的 Mark Word惋耙,判斷當(dāng)前對(duì)象是否處于可偏向狀態(tài)(即當(dāng)前沒(méi)有對(duì)象獲得偏向鎖)捣炬。
2、如果是可偏向狀態(tài)绽榛,則通過(guò)CAS原子操作湿酸,把當(dāng)前線程的ID寫入到 MarkWord,如果CAS成功灭美,表示獲得偏向鎖成功推溃,會(huì)將偏向鎖標(biāo)記設(shè)置為1,且將當(dāng)前線程的ID寫入Mark Word届腐;如果CAS失敗則說(shuō)明當(dāng)前有其他線程獲得了偏向鎖铁坎,同時(shí)也說(shuō)明當(dāng)前環(huán)境存在鎖競(jìng)爭(zhēng),這時(shí)候就需要將已獲得偏向鎖的線程中的偏向鎖撤銷掉(具體參考下面偏向鎖的撤銷)犁苏,并升級(jí)為輕量級(jí)鎖硬萍。
3、如果當(dāng)前線程是已偏向狀態(tài)围详,需要檢查Mark Word中的ThreadID是否和自己相等朴乖,如果相等則不需要再次獲得鎖祖屏,可以直接執(zhí)行同步代碼塊,如果不相等买羞,說(shuō)明當(dāng)前偏向的是其他線程袁勺,需要撤銷偏向鎖并升級(jí)到輕量級(jí)鎖。
偏向鎖的撤銷
偏向鎖的撤銷畜普,需要等待全局安全點(diǎn)(即在這個(gè)時(shí)間點(diǎn)上沒(méi)有正在執(zhí)行的字節(jié)碼)期丰,然后會(huì)暫停擁有偏向鎖的線程,并檢查持有偏向鎖的線程是否活著吃挑,主要有以下兩種情況:
- 如果線程不處于活動(dòng)狀態(tài)钝荡,則將對(duì)象頭設(shè)置成無(wú)鎖狀態(tài)。
- 如果線程仍然活著儒鹿,擁有偏向鎖的棧會(huì)被執(zhí)行化撕,遍歷偏向?qū)ο蟮逆i記錄,棧中的鎖記錄和對(duì)象頭的Mark Word要么重新偏向于其他線程(重偏向需要滿足批量重偏向的條件)约炎,要么恢復(fù)到無(wú)鎖或者標(biāo)記對(duì)象不適合作為偏向鎖植阴。
最后喚醒暫停的線程。
偏向鎖的批量重偏向
一個(gè)線程創(chuàng)建了大量對(duì)象而且執(zhí)行了同步操作后另一個(gè)線程又來(lái)將這些對(duì)象作為鎖對(duì)象進(jìn)行操作圾浅,并且達(dá)到閾值掠手,此時(shí)就會(huì)發(fā)生偏向鎖重偏向的操作(除了這種情況,其他情況只有有線程來(lái)競(jìng)爭(zhēng)鎖狸捕,則偏向鎖狀態(tài)就結(jié)束了)喷鸽。
-XX:BiasedLockingBulkRebiasThreshold 為重偏向閾值JVM參數(shù),默認(rèn)20灸拍,可以通過(guò)-XX:+PrintFlagsFinal打印出默認(rèn)參數(shù)做祝,接下來(lái)我們通過(guò)一個(gè)示例來(lái)演示一下批量重偏向:
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
package com.zwx.concurrent;
import com.zwx.model.User;
import org.openjdk.jol.info.ClassLayout;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
public class BiasedLockDemo {
public static void main(String[] args) throws InterruptedException {
Thread.sleep(5000);//默認(rèn)延遲4s才會(huì)開(kāi)啟偏向鎖,休眠5s確保開(kāi)啟偏向鎖
List<User> list = new ArrayList<>();
new Thread(()->{
for (int i=0;i<20;i++){
//這里必須要new不同的對(duì)象鸡岗,不能共用同一個(gè)對(duì)象
User user = new User();//只是一個(gè)空對(duì)象
synchronized (user){
list.add(user);
System.out.println("t1線程第" + (i+1) + "對(duì)象:" + ClassLayout.parseInstance(user).toPrintable());
}
}
},"t1").start();
try {
Thread.sleep(10000);//確保t1創(chuàng)建對(duì)象完畢
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("------------------------------------------------------");
new Thread(()->{
for (int j=0;j<20;j++){
User user = list.get(j);
synchronized (user){
System.out.println("t2線程第" + (j+1) + "對(duì)象:" + ClassLayout.parseInstance(user).toPrintable());
}
}
},"t2").start();
}
}
運(yùn)行結(jié)果部分截圖(t1線程肯定是101混槐,就不截圖了,t2前面19個(gè)都是000轩性,第20個(gè)達(dá)到閾值了声登,發(fā)生了重偏向):
101三位數(shù)說(shuō)明:
第一位:0-表示非偏向 1-表示偏向
后兩位:00-表示輕量級(jí)鎖 01-表示偏向鎖 10表示重量級(jí)鎖
當(dāng)然,有批量重偏向揣苏,也有批量撤銷悯嗓,在這里就不做過(guò)多敘述,以后有時(shí)間了可以單獨(dú)更深入的寫一寫卸察,感興趣的可以關(guān)注留意脯厨!
偏向鎖及撤銷流程圖
偏向鎖注意事項(xiàng)
偏向鎖在Java SE 1.6和Java SE 1.7里是默認(rèn)啟用的,但是它在應(yīng)用程序啟動(dòng)幾秒鐘之后才激活坑质,如有必要可以使用JVM參數(shù)來(lái)關(guān)閉延遲:-XX:BiasedLockingStartupDelay=0合武。如果你確定應(yīng)用程序里所有的鎖通常情況下都處于競(jìng)爭(zhēng)狀態(tài)个少,可以通過(guò)JVM參數(shù)關(guān)閉偏向鎖:-XX:- UseBiasedLocking=false,那么程序默認(rèn)會(huì)進(jìn)入輕量級(jí)鎖狀態(tài)眯杏。
如果我們的應(yīng)用中大多數(shù)情況存在線程競(jìng)爭(zhēng),那么建議是關(guān)閉偏向鎖壳澳,因?yàn)殚_(kāi)啟反而會(huì)因?yàn)槠蜴i撤銷操作而引起更多的資源消耗岂贩。
輕量級(jí)鎖
輕量級(jí)鎖,一般用于兩個(gè)線程在交替使用鎖的時(shí)候,由于沒(méi)有同時(shí)搶鎖巷波,屬于一種比較和諧的狀態(tài)萎津,就可以使用輕量級(jí)鎖。
輕量級(jí)鎖加鎖
線程在執(zhí)行同步代碼塊之前抹镊,JVM會(huì)先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲(chǔ)鎖記錄的空間锉屈,并將對(duì)象頭中的Mark Word復(fù)制到鎖記錄中,官方稱為Displaced Mark Word垮耳。然后線程嘗試使用 CAS將對(duì)象頭中的Mark Word替換為指向鎖記錄的指針颈渊。如果成功,當(dāng)前線程獲得鎖终佛,如果失敗俊嗽,表示其他線程競(jìng)爭(zhēng)鎖,當(dāng)前線程便嘗試使用自旋來(lái)獲取鎖铃彰。
輕量級(jí)鎖解鎖
輕量級(jí)解鎖時(shí)绍豁,會(huì)使用原子的CAS操作將Displaced Mark Word替換回到對(duì)象頭,如果成功牙捉,則表示沒(méi)有競(jìng)爭(zhēng)發(fā)生竹揍。如果失敗,表示當(dāng)前鎖存在競(jìng)爭(zhēng)邪铲,鎖就會(huì)膨脹成重量級(jí)鎖
輕量級(jí)鎖及膨脹流程圖
自旋鎖
輕量級(jí)鎖在加鎖過(guò)程中芬位,用到了自旋鎖。所謂自旋霜浴,就是指當(dāng)有另外一個(gè)線程來(lái)競(jìng)爭(zhēng)鎖時(shí)晶衷,這個(gè)線程會(huì)在原地循環(huán)等待,而不是把該線程給阻塞阴孟,直到那個(gè)獲得鎖的線程釋放鎖之后晌纫,這個(gè)線程就可以馬上獲得鎖的。
為什么要采用自旋等待呢永丝?
因?yàn)榻^大多數(shù)情況下線程獲得鎖和釋放鎖的過(guò)程都是非常短暫的锹漱,自旋一定次數(shù)之后極有可能碰到獲得鎖的線程釋放鎖,所以慕嚷,輕量級(jí)鎖適用于那些同步代碼塊執(zhí)行很快的場(chǎng)景哥牍,這樣毕泌,線程原地等待很短的時(shí)間就能夠獲得鎖了。
注意:鎖在原地循環(huán)等待的時(shí)候嗅辣,是會(huì)消耗CPU資源的撼泛。所以自旋必須要有一定的條件控制,否則如果一個(gè)線程執(zhí)行同步代碼塊的時(shí)間很長(zhǎng)澡谭,那么等待鎖的線程會(huì)不斷的循環(huán)反而會(huì)消耗CPU資源愿题。默認(rèn)情況下鎖自旋的次數(shù)是 10 次,可以使用-XX:PreBlockSpin參數(shù)來(lái)設(shè)置自旋鎖等待的次數(shù)蛙奖。
自適應(yīng)自旋
在 JDK1.7 開(kāi)始潘酗,引入了自適應(yīng)自旋鎖,修改自旋鎖次數(shù)的JVM參數(shù)被取消雁仲,虛擬機(jī)不再支持由用戶配置自旋鎖次數(shù)仔夺,而是由虛擬機(jī)自動(dòng)調(diào)整。自適應(yīng)意味著自旋的次數(shù)不是固定不變的攒砖,而是根據(jù)前一次在同一個(gè)鎖上自旋的時(shí)間以及鎖的擁有者的狀態(tài)來(lái)決定缸兔。如果在同一個(gè)鎖對(duì)象上,自旋等待剛剛成功獲得過(guò)鎖吹艇,并且持有鎖的線程正在運(yùn)行中灶体,那么虛擬機(jī)就會(huì)認(rèn)為這次自旋也是很有可能再次成功,進(jìn)而它將允許自旋等待持續(xù)相對(duì)更長(zhǎng)的時(shí)間掐暮。如果對(duì)于某個(gè)鎖蝎抽,自旋很少成功獲得過(guò),那在以后嘗試獲取這個(gè)鎖時(shí)將可能省略掉自旋過(guò)程路克,直接阻塞線程樟结,避免浪費(fèi)處理器資源。
重量級(jí)鎖
當(dāng)輕量級(jí)鎖膨脹到重量級(jí)鎖之后精算,意味著線程只能被掛起阻塞來(lái)等待喚醒了瓢宦。每一個(gè)對(duì)象中都有一個(gè)Monitor監(jiān)視器,而Monitor依賴操作系統(tǒng)的 MutexLock(互斥鎖)來(lái)實(shí)現(xiàn)的, 線程被阻塞后便進(jìn)入內(nèi)核(Linux)調(diào)度狀態(tài)灰羽,這個(gè)會(huì)導(dǎo)致系統(tǒng)在用戶態(tài)與內(nèi)核態(tài)之間來(lái)回切換驮履,嚴(yán)重影響鎖的性能。
monitorenter指令是在編譯后插入到同步代碼塊的開(kāi)始位置廉嚼,而monitorexit是插入到方法結(jié)束處和異常處玫镐,JVM要保證每個(gè)monitorenter必須有對(duì)應(yīng)的monitorexit與之配對(duì)。而且當(dāng)一個(gè)monitor被持有后怠噪,它將處于鎖定狀態(tài)恐似。線程執(zhí)行到monitorenter指令時(shí),將會(huì)嘗試獲取對(duì)象所對(duì)應(yīng)的monitor的所有權(quán)傍念,即嘗試獲得對(duì)象的鎖矫夷。我們可以簡(jiǎn)單的理解為葛闷,在加重量級(jí)鎖的時(shí)候會(huì)執(zhí)行monitorenter指令,解鎖時(shí)會(huì)執(zhí)行monitorexit指令双藕。
鎖的優(yōu)缺點(diǎn)對(duì)比
鎖 | 優(yōu)點(diǎn) | 缺點(diǎn) | 適用場(chǎng)景 |
---|---|---|---|
偏向鎖 | 加鎖和解鎖不需要額外的消耗淑趾,和執(zhí)行非同步代碼塊僅存在納秒級(jí)差距 | 如果線程間存在鎖競(jìng)爭(zhēng),會(huì)帶來(lái)額外的鎖撤銷消耗 | 適用于只有一個(gè)線程訪問(wèn)同步代碼塊場(chǎng)景 |
輕量級(jí)鎖 | 競(jìng)爭(zhēng)的線程不會(huì)阻塞忧陪,提高了程序的響應(yīng)速度 | 如果始終得不到鎖治笨,使用自旋會(huì)消耗CPU | 追求響應(yīng)時(shí)間;同步代碼塊執(zhí)行時(shí)間非常短 |
重量級(jí)鎖 | 線程競(jìng)爭(zhēng)不使用自旋赤嚼,不會(huì)消耗CPU | 線程阻塞,響應(yīng)時(shí)間緩慢 | 追求吞吐量顺又;同步代碼塊執(zhí)行時(shí)間較長(zhǎng) |
總結(jié)
synchronized可以解決并發(fā)編程中的三大問(wèn)題:原子性更卒,可見(jiàn)性和有序性,雖然JDK對(duì)其做了優(yōu)化稚照,有些時(shí)候并不那么重了蹂空,但是在某些場(chǎng)景中,我們可以使用[volatile關(guān)鍵字]代替synchronized果录,如果volatile變量修飾符使用恰當(dāng)?shù)脑捝险恚萻ynchronized的使用和執(zhí)行成本更低,因?yàn)樗粫?huì)引起線程上下文的切換和調(diào)度弱恒。