同步鎖synchronized關鍵字
1>>修飾實例方法 對象鎖為this
2>>修飾靜態(tài)方法 對象鎖是當前類的字節(jié)碼文件,即this.getClass();少用-->占內存,垃圾回收無法處理
3>>修飾代碼塊 對象鎖為synchronized(obj) 指定的obj
4>>解決了線程不安全的情況,但是多個線程需要判斷鎖,搶鎖,比較耗資源
2>lock()鎖
1>>重入鎖
lock鎖和synchronized的區(qū)別:
1>synchronized 是內部鎖,自動化的上鎖與釋放鎖,而lock是手動的,需要人為的上鎖和釋放鎖,lock比較靈活,但是代碼相對較多
2>lock接口異常的時候不會自動的釋放鎖,同樣需要手動的釋放鎖,所以一般寫在finally語句塊中,而synchronized則會在異常的時候自動的釋放鎖
3>lock超時獲取鎖:在指定的截止時間之前獲取鎖,如果截止時間到了仍舊無法獲取鎖,則返回
4>lock嘗試非阻塞的獲取鎖:當前線程嘗試獲取鎖,如果這一時刻沒有被其他線程獲取到,則成功獲取并持有鎖。
5>lock能被中斷的獲取鎖:獲取到鎖的線程能夠響應中斷,當獲取到鎖的線程被中斷時癣缅,中斷異常將被拋出,同事釋放鎖哄酝。
volatile關鍵字作用:使變量在多個線程之間可見,強制線程去主內存中取該數(shù)據(jù).
volatile與synchronized區(qū)別:
1>volatile輕量級,只能修飾變量,synchronized重量級,還可以修飾方法.
2>volatile只能保證數(shù)據(jù)的可見性,不能保證線程的安全性(原子性)
3>synchronized不僅保證可見性友存,而且還保證原子性,因為陶衅,只有獲得了鎖的線程才能進入臨界區(qū)屡立,從而保證臨界區(qū)中的所有語句都全部執(zhí)行。多個線程爭搶synchronized鎖對象時搀军,會出現(xiàn)阻塞膨俐。
4>volatile 禁止重排序(重排序:CPU會對代碼執(zhí)行實現(xiàn)優(yōu)化,但不會對有依賴關系的做重排序,代碼可能改變順序,但不會改變結果,重排序的意義是提高并行度,但是在多線程情況下有可能有影響到結果,此時需要用volatile)
CAS算法理解
對CAS的理解勇皇,CAS是一種無鎖算法,CAS有3個操作數(shù)焚刺,內存值V敛摘,舊的預期值A,要修改的新值B乳愉。當且僅當預期值A和內存值V相同時兄淫,將內存值V修改為B,否則什么都不做蔓姚。
java 中的鎖:
隱式鎖:在Java代碼中不能看到加鎖過程的鎖(Synchronized就是隱式鎖)拖叙;
顯式鎖:在Java代碼中能看到加鎖過程的鎖(java.util.concurrent包下的那些鎖
二、 樂觀鎖和悲觀鎖:
1赂乐、樂觀鎖是一種樂觀思想,即認為讀多寫少咖气,遇到并發(fā)寫的可能性低挨措,每次去拿數(shù)據(jù)的時候都認為別人不會修改,所以不會上鎖崩溪,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數(shù)據(jù)浅役,采取在寫時先讀出當前版本號,然后加鎖操作(比較跟上一次的版本號伶唯,如果一樣則更新)觉既,如果失敗則要重復讀?比較?執(zhí)行寫操作。java中的樂觀鎖基本都是通過CAS操作實現(xiàn)的乳幸,CAS是一種更新的原子操作瞪讼,比較當前值跟傳入值是否一樣,一樣則更新粹断,否則失敗符欠。
2、悲觀鎖是就是悲觀思想瓶埋,即認為寫多讀少希柿,遇到并發(fā)寫的可能性高,每次去拿數(shù)據(jù)的時候都認為別人會修改养筒,所以每次在讀寫數(shù)據(jù)的時候都會上鎖曾撤,這樣別人想讀寫這個數(shù)據(jù)就會block(阻塞等待)直到拿到鎖。java中的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嘗試cas樂觀鎖去獲取鎖晕粪,獲取不到挤悉,才會轉換為悲觀鎖,如ReentrantLock兵多。
三尖啡、 重入鎖與非重入鎖:
在一個同步區(qū)域中有一個或多個同步區(qū)域橄仆,這兩個或多個同步區(qū)域的鎖對象是同一個,同一個線程拿到了最外層同步區(qū)域的鎖后能夠進入內層的同步區(qū)域衅斩,這樣的鎖機制就是重入鎖盆顾,反之就是非重入鎖。
四畏梆、 讀寫鎖:
多線程并發(fā)或者并行讀操作的時候您宪,不進行互斥,一旦有寫操作就進行互斥的鎖的機制奠涌。(讀鎖與讀鎖不互斥宪巨,讀鎖與寫鎖互斥,寫鎖與寫鎖互斥)
五溜畅、 偏向鎖捏卓、輕量級鎖、自旋鎖慈格、重量級鎖(JVM 通過monitor指令去調用底層C++):
1怠晴、偏向鎖、輕量級鎖浴捆、自旋鎖屬于樂觀鎖蒜田;
2、重量級鎖屬于悲觀鎖选泻。
重量級鎖(synchronized):
1.synchronized代碼塊反編譯后冲粤,輸出的字節(jié)碼有monitorenter和monitorexit語句
2.每一個對象都會和一個監(jiān)視器monitor關聯(lián)。監(jiān)視器被占用時會被鎖住页眯,其他線程無法來獲取該monitor梯捕。
3.當JVM執(zhí)行某個線程的某個方法內部的monitorenter時,它會嘗試去獲取當前對象對應的monitor的所有權餐茵。其過程如下:
若monior的進入數(shù)為0科阎,線程可以進入monitor,并將monitor的進入數(shù)置為1忿族。當前線程成為monitor的owner(所有者)
若線程已擁有monitor的所有權锣笨,允許它重入monitor,并遞增monitor的進入數(shù)
若其他線程已經占有monitor的所有權道批,那么當前嘗試獲取monitor的所有權的線程會被阻塞错英,直到monitor的進入數(shù)變?yōu)?,才能重新嘗試獲取monitor的所有權隆豹。
4.能執(zhí)行monitorexit指令的線程一定是擁有當前對象的monitor的所有權的線程椭岩。
執(zhí)行monitorexit時會將monitor的進入數(shù)減1。當monitor的進入數(shù)減為0時,當前線程退出monitor判哥,不再擁有monitor的所有權献雅,此時其他被這個monitor阻塞的線程可以嘗試去獲取這個monitor的所有權。
monitor
每一個JAVA對象都會與一個監(jiān)視器monitor關聯(lián)塌计,我們可以把它理解成為一把鎖挺身,當一個線程想要執(zhí)行一段被synchronized圈起來的同步方法或者代碼塊時,該線程得先獲取到synchronized修飾的對象對應的monitor锌仅。
我們的java代碼里不會顯示地去創(chuàng)造這么一個monitor對象章钾,我們也無需創(chuàng)建,事實上可以這么理解:我們是通過synchronized修飾符告訴JVM需要為我們的某個對象創(chuàng)建關聯(lián)的monitor對象热芹。
在hotSpot虛擬機中贱傀,monitor是由ObjectMonitor實現(xiàn)的。其源碼是用c++來實現(xiàn)的伊脓,位于hotSpot虛擬機源碼ObjectMonitor.hpp文件中府寒。
堆中的對象,由對象頭,實例數(shù)據(jù),對齊填充構成
對象頭:
1.對象頭形式
普通對象頭
數(shù)組對象頭
對象頭(Header):包含兩部分,
第一部分(mark word)用于存儲對象自身的運行時數(shù)據(jù)报腔,如哈希碼椰棘、GC分代年齡、鎖狀態(tài)標志榄笙、線程持有的鎖、偏向線程 ID祷蝌、偏向時間戳等茅撞,32 位虛擬機占 32 bit,64 位虛擬機占 64 bit巨朦。官方稱為 ‘Mark Word’米丘;
第二部分(KLASS)是類型指針,即對象指向它的類的元數(shù)據(jù)指針糊啡,虛擬機通過這個指針確定這個對象是哪個類的實例拄查,
另外,如果是 Java 數(shù)組棚蓄,對象頭中還必須有一塊用于記錄數(shù)組長度的數(shù)據(jù)堕扶,因為普通對象可以通過 Java 對象元數(shù)據(jù)確定大小,而數(shù)組對象不可以梭依;
/**
* synchronized關鍵字的底層原理
*
* 由于虛擬機默認開啟指針壓縮稍算,所以整個對象頭的占12個字節(jié)
* 在VM參數(shù)中加入:-XX:-UseCompressedOops關閉指針壓縮。
*
* 由于Intel是采用小端存儲的役拴,所以是數(shù)據(jù)的低位保存在內存的低地址中糊探,
* 而數(shù)據(jù)的高位保存在內存的高地址中
* 地址:以字節(jié)為單位低----------->高
*(object header) 01 00 00 00 (0_0000_0_01 00000000 00000000 00000000) (1)
*(object header) 00 00 00 00 (0_0000000 00000000 00000000 00000000) (0)
*
*前8位分析:0沒有用到 0000表示GC的年齡 0表示是否偏向 01表示鎖的級別和GC狀態(tài)標識
*
*所以的得出結論:
*關于鎖的狀態(tài)就觀察對象頭的第一個字節(jié)的后3位。
*第一位表示是否是偏向鎖狀態(tài),
*第二科平、三位表示鎖的級別和GC的標記褥紫。01無鎖,00輕量級鎖 10重量級鎖 11 GC標記
*綜上所述:
*001:無鎖瞪慧,101偏向鎖髓考, 末尾兩位00輕量級鎖 末尾兩位10重量級鎖
*因為輕量級鎖和重量級鎖, 代表偏向的位用于其他表示了,不在代表偏向鎖
*/
public class Test {
private int k = 0;
private Object myLock = new Object();
public static void main(String[] args) {
Test test = new Test();
System.out.println("計算hashCode之前----------------------");
System.out.println(ClassLayout.parseInstance(test).toPrintable());
System.out.println("計算hashCode之后----------------------");
int hashCode = test.hashCode();
System.out.println("hashCode:"+hashCode);
System.out.println("轉為16進制的hashCode:"+Integer.toHexString(hashCode));
System.out.println(ClassLayout.parseInstance(test).toPrintable());
System.out.println(ClassLayout.parseInstance(test).toPrintable());
}
}
運行結果如下:
對象的狀態(tài)有幾種:
無鎖,偏向鎖,輕量級鎖,重量級鎖,GC標志五個狀態(tài)
鎖的膨脹過程:
無鎖:
程序多線程執(zhí)行過程中,沒有去執(zhí)行synchronized修飾區(qū)域或者方法
偏向鎖:
發(fā)生在程序多線程過程中,由始至終只有一個線程去執(zhí)行過synchronized修飾的區(qū)域或者方法,由于是由始至終只有一個線程去執(zhí)行,所以,沒有發(fā)生競爭,等待,搶鎖的情況,他不會調用操作系統(tǒng)的函數(shù)去實現(xiàn)同步.
輕量級鎖:
發(fā)生在程序多線程執(zhí)行過程中有去執(zhí)行synchronized修飾區(qū)域或方法,且沒有發(fā)生競爭,等待,搶鎖的情況或者發(fā)生了競爭,等待,搶鎖,但是競爭,等待和搶鎖的時間沒有超過一個JVM設定的自旋時間或次數(shù)的時候,他是在虛擬機內部去實現(xiàn)的同步,不會調用操作系統(tǒng)的函數(shù)去實現(xiàn)同步.
重量級鎖:
發(fā)生在程序多線程執(zhí)行過程中有去執(zhí)行synchronized修飾區(qū)域或者方法,且發(fā)生了競爭汞贸、等待绳军、搶鎖且競爭、等待和搶鎖的時間已經超過一個JVM設定的自旋時間或者次數(shù)的時候矢腻,它會調用操作系統(tǒng)的函數(shù)去實現(xiàn)同步(調用操作系統(tǒng)實現(xiàn)同步门驾,需要的步驟很多,導致性能相對于其它鎖實現(xiàn)同步的方式就很低)多柑。
/**
* synchronized關鍵字的底層原理
* 加鎖之后不睡眠則:變成了輕量級鎖
* 因為是JVM默認開啟了偏向鎖延遲開啟的開關
*
* 注意:偏向鎖:101奶是,的第一個1表示的是這個是可偏向狀態(tài),
* 而不是說它已經是一個偏向鎖了竣灌,如何辨別呢聂沙?
* 看其他字節(jié)是否有存儲數(shù)據(jù),如果有就是已經是偏向鎖了初嘹,
* 如果沒有及汉,則還是處于一個可偏向狀態(tài)的無鎖。
* -XX:BiasedLockingStartupDelay=0 可以設置延遲開啟偏向鎖的時間
* @author Peter
*/
public static void main(String[] args) throws InterruptedException {
/*
* JVM啟動需要啟動很多我們不知道的線程屯烦,比如GC坷随,
* 要花費4秒時間,所以延遲開啟偏向鎖的時間默認為4秒
*/
// Thread.sleep(4100);
Test test = new Test();
System.out.println("計算加鎖之前----------------------");
System.out.println(ClassLayout.parseInstance(test).toPrintable());
synchronized (test){
System.out.println("計算加鎖之后----------------------");
System.out.println(ClassLayout.parseInstance(test).toPrintable());
}
}
運行結果如下:
保存偏向線程ID,表示當前的線程就是這個線程,如果下次還是這個線程的話,則直接放行(獲取鎖).
/**
* synchronized關鍵字的底層原理
*
* -XX:BiasedLockingStartupDelay=0
* 演示計算了hashCode的對象不能成為偏向鎖
* 會直接成為輕量級鎖驻龟,因為沒有地方存關于偏向鎖的信息了
* 就直接成為輕量級鎖温眉,并把hashCode放入記錄的線程信息中
*/
public static void main(String[] args) {
Test test=new Test();
//這里計算一下hashCode
test.hashCode();
System.out.println("------------------------------加鎖之前--------------------------------");
System.out.println(ClassLayout.parseInstance(test).toPrintable());
synchronized (test) {
System.out.println("-------------------------------加鎖之后------------------------------");
System.out.println(ClassLayout.parseInstance(test).toPrintable());
}
}
運行結果如下:
/**
* synchronized關鍵字的底層原理
*
* -XX:BiasedLockingStartupDelay=0
* 如果調用wait方法,則立刻變?yōu)橹亓考夋i
* wait方法就是monitor實現(xiàn)的
*wait表示等待,說明會有競爭搶鎖的情況,并且時間不會短,所以直接變?yōu)橹亓考夋i
* synchronized內置的鎖的膨脹過程不可逆
*/
public static void main(String[] args) throws InterruptedException {
final Object testLock=new Object();
System.out.println("------------------------------加鎖之前------------------------");
System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
synchronized (testLock) {
System.out.println("-----------------------加鎖之后等待之前------------------");
System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
testLock.wait(1_000);
System.out.println("--------------------加鎖之后等待之后------------------------");
System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
}
System.out.println("--------------------退出同步代碼塊之后----------------------");
System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
}
運行結果如下:
/**
* synchronized關鍵字的底層原理
*
* 證明:
* 偏向鎖偏向一個線程后翁狐,不會發(fā)生重偏向
* 另一個線程的情況类溢,只會膨脹為輕量級鎖。
* 注意:這里因為是主線程去進入同步代碼區(qū)域
* 所以會膨脹為輕量級鎖
* -XX:BiasedLockingStartupDelay=0
*/
public static void main(String[] args) throws InterruptedException {
final Object testLock=new Object();
final Thread t1 = new Thread("子線程:"){
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+"---------------------加鎖前------------------");
System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
synchronized (testLock) {
System.out.println(threadName+"--------------------加鎖后-----------------");
System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
}
}
};
t1.start();
t1.join();
System.out.println("主線程:------------------加鎖之前--------------------------");
System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
synchronized (testLock) {
System.out.println("主線程:----------------------加鎖之后------------------------");
System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
}
}
運行結果如下:
說明,偏向鎖不會重新偏向.
/**
* synchronized關鍵字的底層原理
* -XX:BiasedLockingStartupDelay=0
* 證明:
* 偏向鎖的含義不是只有兩個線程在交替執(zhí)行露懒。
*
* 我認為:不管是多少個線程去執(zhí)行闯冷,只要是沒有產生競爭關系
* 就不會膨脹為輕量級鎖,但是這個是有一些前提的懈词,后面講解窃躲。
*
* 結論:不是網上說的只要有第三個線程去執(zhí)行了且沒有產生競爭關系
* 時就會膨脹為輕量級鎖。
*/
public static void main(String[] args) throws InterruptedException {
final Object testLock=new Object();
final Thread t1 = new Thread("線程1:"){
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+"-----------------加鎖前------------------------");
System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
synchronized (testLock) {
System.out.println(threadName+"-----------------加鎖后--------------------");
System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
}
}
};
t1.start();
//保證t2在執(zhí)行時不會和t1發(fā)生競爭
t1.join();
Thread t2 = new Thread("線程2:"){
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+"-----------------加鎖前-------------------");
System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
synchronized (testLock) {
System.out.println(threadName+"-------------------加鎖后------------------");
System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
}
}
};
t2.start();
t2.join();
Thread t3 = new Thread("線程3:"){
@Override
public void run() {
String threadName = Thread.currentThread().getName();
System.out.println(threadName+"-----------------加鎖前--------------------");
System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
synchronized (testLock) {
System.out.println(threadName+"------------------加鎖后----------------");
System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
}
}
};
t3.start();
/*t3.join();
System.out.println("主線程:--------------------------加鎖前----------------------------");
System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
synchronized (testLock) {
System.out.println("主線程:-------------------加鎖后----------------------");
System.out.println(ClassLayout.parseInstance(testLock).toPrintable());
}*/
}
運行結果,以上層序運行結果為,均是偏上線程1 的偏向鎖.即無論多少個線程執(zhí)行,只要沒有競爭關系,都不會膨脹為輕量級鎖.
但是,如果其中一個線程是以上線程的主線程,則主線程加鎖之后會直接變成輕量級鎖.
運行描述:
線程1執(zhí)行同步前,為可偏向無鎖狀態(tài)鎖,
線程1執(zhí)行synchroized后,變成偏向鎖,偏向線程1,
線程1退出同步代碼塊,依然是偏向鎖,偏向線程1,
保持線程1存活,
線程2執(zhí)行synchronized,變成輕量級鎖
線程2執(zhí)行同步結束,釋放鎖,變成不可偏向的無鎖狀態(tài).
線程1此時又進入同步代碼塊,鎖直接變成輕量級鎖.
偏向鎖獲取過程
其他優(yōu)化:
1)自旋鎖:
互斥同步時,掛起和恢復線程都需要切換到內核態(tài)完成,這對性能并發(fā)帶來了不少的壓力横腿。同時在許多應用上屎媳,共享數(shù)據(jù)的鎖定狀態(tài)只會持續(xù)很短的一段時間垒酬,為了這段較短的時間而去掛起和恢復線程并不值得昌跌。那么如果有多個線程同時并行執(zhí)行污桦,可以讓后面請求鎖的線程通過自旋(CPU忙循環(huán)執(zhí)行空指令)的方式稍等一會兒针肥,看看持有鎖的線程是否會很快的釋放鎖衰抑,這樣就不需要放棄CPU的執(zhí)行時間適應性自旋
在輕量級鎖獲取過程中象迎,線程執(zhí)行 CAS 操作失敗時,需要通過自旋來獲取重量級鎖呛踊。如果鎖被占的時間比較短砾淌,那么自旋等待的效果就會比較好,而如果鎖占用的時間很長谭网,自旋的線程則會白白浪費 CPU 資源汪厨。解決這個問題的最簡答的辦法就是:指定自旋的次數(shù),如果在限定次數(shù)內還沒獲取到鎖(例如10次)愉择,就按傳統(tǒng)的方式掛起線程進入阻塞狀態(tài)劫乱。JDK1.6 之后引入了自適應性自旋的方式,如果在同一鎖對象上锥涕,一線程自旋等待剛剛成功獲得鎖衷戈,并且持有鎖的線程正在運行中,那么JVM 會認為這次自旋也有可能再次成功獲得鎖层坠,進而允許自旋等待相對更長的時間(例如100次)另一方面殖妇,如果某個鎖自旋很少成功獲得,那么以后要獲得這個鎖時將省略自旋過程破花,以避免浪費 CPU拉一。
2)鎖消除:
鎖消除就是編譯器運行時,對一些被檢測到不可能存在共享數(shù)據(jù)競爭的鎖進行消除旧乞。如果判斷一
段代碼中,堆上的數(shù)據(jù)不會逃逸出去從而被其他線程訪問到磅氨,則可以把他們當做棧上的數(shù)據(jù)對待尺栖,認為它們是線程私有的,不必要加鎖
3)鎖粗化:
鎖粗化就是JVM檢測到一串零碎的操作都對同一個對象加鎖烦租,則會把加鎖同步的范圍粗化到整個操作序列的外部延赌。
批量重偏向問題
子線程創(chuàng)建了同一個類的多個對象并且對這個對象進行了加鎖.
主線程也在這些對象加鎖后,也對這些對象加鎖(沒有發(fā)生競爭加鎖)
因為要執(zhí)行CAS進行線程信息的替換(鎖的升級),那么就會進行多次偏向鎖的撤銷,那么JVM就會認為后面的對象都需要批量重偏向,那么后面的對象就會是加偏向鎖,而不再是輕量級鎖
偏向鎖大量重偏向的門檻(閾值)
intx BiasedLockingBulkRebiasThreshold = 20