ipa去除时间锁_Java中的锁以及sychronized实现机制(十)
上節講了線程安全和原子性,其實就是并發代碼變成同步,意味著代碼只有一個人在使用,這樣就不會有問題。
(一)Java中的鎖
1.自旋鎖
為了不放棄CPU執行時間,循環的使用CAS技術對數據嘗試進行更新,直至成功。(樂觀鎖的實現)
2.悲觀鎖
假定會發生并發沖突,同步所有對數據的相關操作,從讀數據就開始上鎖。(從讀數據就開始上鎖。)
3.樂觀鎖
假定沒有沖突,在修改數據時如果發生數據和之前獲取的不一致,則讀最新數據,修改后重試修改。(假定能成功,如果不成功也是CAS的操作)
4.獨享鎖(寫)
給資源加上寫鎖,線程可以修改資源,其他線程不能再加鎖(單寫)(互斥鎖)(就 像談戀愛,被女朋友鎖定了,其他人無法獲取你)
5.共享鎖(讀)
給資源加上讀鎖只能讀不能改,其他線程只能加讀鎖,不能加寫鎖(多讀)(就像信息已經放給婚介了,誰都可以獲取我的信息)
6.可重入鎖,不可重入鎖
線程拿到一把鎖之后,可以自由進入同一把鎖所同步的其他代碼
public class ObjectSyncDemo2 {public synchronized void test1(Object arg) {
System.out.println(Thread.currentThread() + " 我開始執行 " + arg);
if (arg == null) {
test1(new Object());
}
System.out.println(Thread.currentThread() + " 我執行結束" + arg);
}
public static void main(String[] args) throws InterruptedException {
new ObjectSyncDemo2().test1(null);
}
}
7.公平鎖,非公平鎖
爭搶鎖的順序,如果按先來后到,則為公平。
公平鎖:線程1拿到鎖,線程2在等待,線程3也在等待,線程1執行完釋放鎖,線程2就拿到這個鎖。
非公平鎖:線程1拿到鎖,線程2在等待,線程3也在等待,線程1執行完釋放鎖,線程3就拿到這個鎖,本來應該線程2拿到鎖的,線程3先拿到了。就是不公平。
(二)同步關鍵字synchronized
① 介紹
最基本的線程通信機制,基于對象監視器實現的。JAVA中的每個對象都與一個監視器相關聯,線程可以鎖定或解鎖。一次只有一個線程可以鎖定監視器。試圖鎖定該監視器的任何其他線程都會被阻塞,知道它們可以獲得該監視器上的鎖定為止。
② 特性
可重入,獨享,悲觀鎖。
③ 鎖的范圍
類鎖,對象鎖,鎖消除,鎖粗化
同步關鍵字,不僅是實現同步,根據JMM規定還能保證可見性(讀取最新主內存數據,結束后寫入主內存)
④ 鎖消除
發生在編譯器級別的一種鎖優化方式。完全不需要加鎖,卻執行了加鎖操作。
// 鎖消除(jit)public class ObjectSyncDemo {
public void test3(Object arg) {
StringBuilder builder = new StringBuilder();
builder.append("a");
builder.append(arg);
builder.append("c");
System.out.println(arg.toString());
}
public void test2(Object arg) {
String a = "a";
String c = "c";
System.out.println(a + arg + c);
}
public void test1(Object arg) {
// jit 優化, 消除了鎖
StringBuffer stringBuffer = new StringBuffer();
stringBuffer.append("a");
stringBuffer.append(arg);
stringBuffer.append("c");
// System.out.println(stringBuffer.toString());
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000000; i++) {
new ObjectSyncDemo().test1("123");
}
}
}
test1()方法中的StringBuffer數以函數內部的局部變量,進作用于方法內部,不可能逃逸出該方法,因此他就不可能被多個線程同時訪問,也就沒有資源的競爭,但是StringBuffer的append操作卻需要執行同步操作
StringBuffer 的源碼
@Overridepublic synchronized StringBuffer append(String str) {
toStringCache = null;
super.append(str);
return this;
}
去除不可能存在共享資源競爭的鎖,通過鎖消除,可以節省毫無意義的請求鎖時間。
④ 鎖粗化
通常情況下,為了保證多線程間的有效并發,會要求每個線程持有鎖的時間盡可能短,但是某些情況下,一個程序對同一個鎖不間斷、高頻地請求、同步與釋放,會消耗掉一定的系統資源,因為鎖的講求、同步與釋放本身會帶來性能損耗,這樣高頻的鎖請求就反而不利于系統性能的優化了,雖然單次同步操作的時間可能很短。鎖粗化就是告訴我們任何事情都有個度,有些情況下我們反而希望把很多次鎖的請求合并成一個請求,以降低短時間內大量鎖請求、同步、釋放帶來的性能損耗。
// 鎖粗化(運行時 jit 編譯優化)// jit 編譯后的匯編內容, jitwatch可視化工具進行查看
public class ObjectSyncDemo {
int i;
public void test1(Object arg) {
synchronized (this) {
i++;
}
synchronized (this) {
i++;
}
}
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 10000000; i++) {
new ObjectSyncDemo().test1("a");
}
}
}
在test1() 同步操作的代碼之間,需要做一些其它的工作,而這些工作只會花費很少的時間,那么我們就可以把這些工作代碼放入鎖內,將兩個同步代碼塊合并成一個,以降低多次鎖請求、同步、釋放帶來的系統性能消耗,合并后的代碼如下
前提中間不需要同步的代碼能夠很快速地完成,如果不需要同步的代碼需要花很長時間,就會導致同步塊的執行需要花費很長的時間,這樣做也不合理。
public void test1(Object arg) {synchronized (this) {
i++;
i++;
}
}
(三)同步關鍵字加鎖原理
① JAVA對象頭
鎖的實現機制與java對象頭息息相關,鎖的所有信息,都記錄在java的對象頭中。用2字(32位JVM中1字=32bit=4baye)存儲對象頭,如果是數組類型使用3字存儲(還需存儲數組長度)。對象頭中記錄了hash值、GC年齡、鎖的狀態、線程擁有者、類元數據的指針。
② 幾種鎖類型
線程的阻塞和喚醒需要CPU從用戶態轉為核心態,頻繁的阻塞和喚醒對CPU來說是一件負擔很重的工作。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。
③ 偏向鎖(A線程獨占鎖,不用上下文切換。對象頭標識)
大多數情況下鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得。偏向鎖的目的是在某個線程獲得鎖之后,消除這個線程鎖重入(CAS)的開銷,看起來讓這個線程得到了偏護。另外,JVM對那種會有多線程加鎖,但不存在鎖競爭的情況也做了優化,聽起來比較拗口,但在現實應用中確實是可能出現這種情況,因為線程之前除了互斥之外也可能發生同步關系,被同步的兩個線程(一前一后)對共享對象鎖的競爭很可能是沒有沖突的。對這種情況,JVM用一個epoch表示一個偏向鎖的時間戳(真實地生成一個時間戳代價還是蠻大的,因此這里應當理解為一種類似時間戳的identifier)
偏向鎖的獲取
當一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時不需要花費CAS操作來加鎖和解鎖,而只需簡單的測試一下對象頭的Mark Word里是否存儲著指向當前線程的偏向鎖,如果測試成功,表示線程已經獲得了鎖,如果測試失敗,則需要再測試下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖),如果沒有設置,則使用CAS競爭鎖,如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。
偏向鎖的撤銷
偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有字節碼正在執行),它會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著,如果線程不處于活動狀態,則將對象頭設置成無鎖狀態,如果線程仍然活著,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word,要么重新偏向于其他線程,要么恢復到無鎖或者標記對象不適合作為偏向鎖,最后喚醒暫停的線程。
偏向鎖的設置
關閉偏向鎖:偏向鎖在Java 6和Java 7里是默認啟用的,但是它在應用程序啟動幾秒鐘之后才激活,如有必要可以使用JVM參數來關閉延遲-XX:BiasedLockingStartupDelay = 0。如果你確定自己應用程序里所有的鎖通常情況下處于競爭狀態,可以通過JVM參數關閉偏向鎖-XX:-UseBiasedLocking=false,那么默認會進入輕量級鎖狀態。
④自旋鎖(一個線程嘗試獲取某個鎖時,如果該鎖已被其他線程占用,就一直循環檢測鎖是否被釋放,而不是進入線程掛起或睡眠狀態。一定要有次數限制)
線程的阻塞和喚醒需要CPU從用戶態轉為核心態,頻繁的阻塞和喚醒對CPU來說是一件負擔很重的工作。同時我們可以發現,很多對象鎖的鎖定狀態只會持續很短的一段時間,例如整數的自加操作,在很短的時間內阻塞并喚醒線程顯然不值得,為此引入了自旋鎖。
所謂“自旋”,就是讓線程去執行一個無意義的循環,循環結束后再去重新競爭鎖,如果競爭不到繼續循環,循環過程中線程會一直處于running狀態,但是基于JVM的線程調度,會出讓時間片,所以其他線程依舊有申請鎖和釋放鎖的機會。
自旋鎖省去了阻塞鎖的時間空間(隊列的維護等)開銷,但是長時間自旋就變成了“忙式等待”,忙式等待顯然還不如阻塞鎖。所以自旋的次數一般控制在一個范圍內,例如10,100等,在超出這個范圍后,自旋鎖會升級為阻塞鎖。
④ 輕量級鎖(A線程擁有鎖,B獲取,競爭,自旋)
加鎖
線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,則自旋獲取鎖,當自旋獲取鎖仍然失敗時,表示存在其他線程競爭鎖(兩條或兩條以上的線程競爭同一個鎖),則輕量級鎖會膨脹成重量級鎖。
解鎖
輕量級解鎖時,會使用原子的CAS操作來將Displaced Mark Word替換回到對象頭,如果成功,則表示同步過程已完成。如果失敗,表示有其他線程嘗試過獲取該鎖,則要在釋放鎖的同時喚醒被掛起的線程。
⑤ 重量級鎖(B線程自旋獲取不到鎖,膨脹重量鎖,阻塞A線程。直到B執行完。)
重量鎖在JVM中又叫對象監視器(Monitor),它很像C中的Mutex,除了具備Mutex(0|1)互斥的功能,它還負責實現了Semaphore(信號量)的功能,也就是說它至少包含一個競爭鎖的隊列,和一個信號阻塞隊列(wait隊列),前者負責做互斥,后一個用于做線程同步。
PS:有數據表明,除去大型互聯網公司,80%的系統不存在多線程的競爭的情況,一定要熟悉這幾種鎖,對以后面試鍍金(面試)真的很有用。
總結
以上是生活随笔為你收集整理的ipa去除时间锁_Java中的锁以及sychronized实现机制(十)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python中读取文件编码_[转载]py
- 下一篇: java线程中的task_Java线程(