摘要:每一個被鎖住的對象都會和一個關聯對象頭的中的指向的起始地址,同時中有一個字段存放擁有該鎖的線程的唯一標識,表示該鎖被這個線程占用。
jdk 6 對鎖進行了優化,讓他看起來不再那么笨重,synchronized有三種形式:偏向鎖,輕量級鎖,重量級鎖.
介紹三種鎖之前,引入幾個接下來會出現的概念
mark work:
對象頭,對象頭中存儲了一些對象的信息,這個是鎖的根本,任何鎖都需要依賴mark word 來維持鎖的運作,對象頭中存儲了當前持有鎖的線程,hashCode,GC的一些信息都存儲在對象頭中.
在JVM中,對象在內存中除了本身的數據外還會有個對象頭,對于普通對象而言,其對象頭中有兩類信息:mark word和類型指針。另外對于數組而言還會有一份記錄數組長度的數據.
類型指針是指向該對象所屬類對象的指針,mark word用于存儲對象的HashCode、GC分代年齡、鎖狀態等信息。在32位系統上mark word長度為32bit,64位系統上長度為64bit。為了能在有限的空間里存儲下更多的數據,其存儲格式是不固定的,在32位系統上各狀態的格式如下:
可以看到鎖信息也是存在于對象的mark word中的。當對象狀態為偏向鎖時,mark word存儲的是偏向的線程ID;當狀態為輕量級鎖時,mark word存儲的是指向線程棧中Lock Record的指針;當狀態為重量級鎖時,為指向堆中的monitor對象的指針.
Lock Record:
前面對象頭中提到了Lock Record,接下來說下Lock Record,Lock Record存在于線程棧中,翻譯過來就是鎖記錄,它會拷貝一份對象頭中的mark word信息到自己的線程棧中去,這個拷貝的mark word 稱為Displaced Mark Word ,另外還有一個指針指向對象
monitor:
monitor存在于堆中,什么是Monitor?我們可以把它理解為一個同步工具,也可以描述為一種同步機制,它通常被描述為一個對象。
與一切皆對象一樣,所有的Java對象是天生的Monitor,每一個Java對象都有成為Monitor的潛質,因為在Java的設計中 ,每一個Java對象自打娘胎里出來就帶了一把看不見的鎖,它叫做內部鎖或者Monitor鎖。
Monitor 是線程私有的數據結構,每一個線程都有一個可用monitor record列表,同時還有一個全局的可用列表。每一個被鎖住的對象都會和一個monitor關聯(對象頭的MarkWord中的LockWord指向monitor的起始地址),同時monitor中有一個Owner字段存放擁有該鎖的線程的唯一標識,表示該鎖被這個線程占用。其結構如下:
Owner:初始時為NULL表示當前沒有任何線程擁有該monitor record,當線程成功擁有該鎖后保存線程唯一標識,當鎖被釋放時又設置為NULL
EntryQ:關聯一個系統互斥鎖(semaphore),阻塞所有試圖鎖住monitor record失敗的線程
RcThis:表示blocked或waiting在該monitor record上的所有線程的個數
Nest:用來實現重入鎖的計數
HashCode:保存從對象頭拷貝過來的HashCode值(可能還包含GC age)
Candidate:用來避免不必要的阻塞或等待線程喚醒,因為每一次只有一個線程能夠成功擁有鎖,如果每次前一個釋放鎖的線程喚醒所有正在阻塞或等待的線程,會引起不必要的上下文切換(從阻塞到就緒然后因為競爭鎖失敗又被阻塞)從而導致性能嚴重下降。Candidate只有兩種可能的值0表示沒有需要喚醒的線程1表示要喚醒一個繼任線程來競爭鎖
(摘自:Java中synchronized的實現原理與應用)
說完幾個關鍵概念之后來說一下鎖的問題:
偏向鎖
偏向鎖是鎖的級別中最低的鎖,舉個例子: 在此demo中,獲得操作list的一直都是main線程,沒有第二個線程參與操作,此時的鎖就是偏向鎖,偏向鎖很輕,jdk 1.6默認開啟,當第一個線程進入的時候,對象頭中的threadid為0,表示未偏向任何線程,也叫做匿名偏向量
public class SyncDemo1 { public static void main(String[] args) { SyncDemo1 syncDemo1 = new SyncDemo1(); for (int i = 0; i < 100; i++) { syncDemo1.addString("test:" + i); } } private Listlist = new ArrayList<>(); public synchronized void addString(String s) { list.add(s); } }
當第一個線程進入的時候發現是匿名偏向狀態,則會用cas指令把mark words中的threadid替換為當前線程的id如果替換成功,則證明成功拿到鎖,失敗則鎖膨脹;
當線程第二次進入同步塊時,如果發現線程id和對象頭中的偏向線程id一致,則經過一些比較之后,在當前線程棧的lock record中添加一個空的Displaced Mark Word,由于操作的是私有線程棧,所以不需要cas操作,synchronized帶來的開銷基本可以忽略;
當其他線程進入同步塊中時,發現偏向線程不是當前線程,則進入到撤銷偏向鎖的邏輯,當達到全局安全點時,鎖開始膨脹為輕量級鎖,原來的線程仍然持有鎖,如果發現偏向線程掛了,那么就把對象的頭改為無鎖狀態,鎖膨脹
輕量鎖
當鎖膨脹為輕量級鎖時,首先判斷是否有線程持有鎖(判斷mark work),如果是,則在當前線程棧中創建一個lock record 復制mark word 并且cas的把當前線程棧的lock record 的地址放到對象頭中,如果成功,則說明獲取到輕量級鎖,如果失敗,則說明鎖已經被占用了,此時記錄線程的重入次數(把lock record 的mark word 設置為null),鎖會自旋可以進行自適應性自旋,確保在競爭不激烈的情況下仍然可以不膨脹為重量級鎖從而減少消耗,如果cas失敗,則說明線程出現競爭,需要膨脹為重量級的鎖,代碼如下:
void ObjectSynchronizer::slow_enter(Handle obj, BasicLock* lock, TRAPS) { markOop mark = obj->mark(); assert(!mark->has_bias_pattern(), "should not see bias pattern here"); // 如果是無鎖狀態 if (mark->is_neutral()) { //設置Displaced Mark Word并替換對象頭的mark word lock->set_displaced_header(mark); if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(), mark)) { TEVENT (slow_enter: release stacklock) ; return ; } } else if (mark->has_locker() && THREAD->is_lock_owned((address)mark->locker())) { assert(lock != mark->locker(), "must not re-lock the same lock"); assert(lock != (BasicLock*)obj->mark(), "don"t relock with same BasicLock"); // 如果是重入,則設置Displaced Mark Word為null lock->set_displaced_header(NULL); return; } ... // 走到這一步說明已經是存在多個線程競爭鎖了 需要膨脹為重量級鎖 lock->set_displaced_header(markOopDesc::unused_mark()); ObjectSynchronizer::inflate(THREAD, obj())->enter(THREAD); }
重量鎖
重量級鎖就是我們傳統意義上的鎖了,當線程發生競爭,鎖膨脹為重量級鎖,對象的mark word 指向堆中的 monitor,此時會將線程封裝為一個objectwaiter對象插入到monitor中的contextList中去,然后暫停當前線程,當持有鎖的線程釋放線程之前,會把contextList里面的所有線程對象插入到EntryList中去,會從EntryList中挑選一個線程喚醒,被選中的線程叫做Heir presumptive即假定繼承人(應該是這樣翻譯),就是圖中的Ready Thread,假定繼承人被喚醒后會嘗試獲得鎖,但synchronized是非公平的,所以假定繼承人不一定能獲得鎖(這也是它叫"假定"繼承人的原因)。
如果線程獲得鎖后調用Object#wait方法,則會將線程加入到WaitSet中,當被Object#notify喚醒后,會將線程從WaitSet移動到cxq或EntryList中去。需要注意的是,當調用一個鎖對象的wait或notify方法時,如當前鎖的狀態是偏向鎖或輕量級鎖則會先膨脹成重量級鎖。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://specialneedsforspecialkids.com/yun/74342.html
摘要:本文從內存模型角度,探討的實現原理。通過共享內存或者消息通知這兩種方法,可以實現通信或同步。基于共享內存的線程通信是隱式的,線程同步是顯式的而基于消息通知的線程通信是顯式的,線程同步是隱式的。鎖規則鎖的解鎖,于于鎖的獲取或加鎖。 一、前言 在java多線程編程中,volatile可以用來定義輕量級的共享變量,它比synchronized的使用成本更低,因為它不會引起線程上下文的切換和調...
摘要:自選鎖鎖膨脹后,虛擬機為了避免線程真實地在操作系統層面掛起,虛擬機還會在做最后的努力自選鎖。 showImg(https://segmentfault.com/img/remote/1460000016159660?w=500&h=333); 作為一款公用平臺,JDK 本身也為并發程序的性能絞盡腦汁,在 JDK 內部也想盡一切辦法提供并發時的系統吞吐量。這里,我將向大家簡單介紹幾種 J...
摘要:今天給大家總結一下,面試中出鏡率很高的幾個多線程面試題,希望對大家學習和面試都能有所幫助。指令重排在單線程環境下不會出先問題,但是在多線程環境下會導致一個線程獲得還沒有初始化的實例。使用可以禁止的指令重排,保證在多線程環境下也能正常運行。 下面最近發的一些并發編程的文章匯總,通過閱讀這些文章大家再看大廠面試中的并發編程問題就沒有那么頭疼了。今天給大家總結一下,面試中出鏡率很高的幾個多線...
摘要:為了拓展同步代碼塊中的監視器鎖,開始,出現了接口,它實現了可定時可輪詢與可中斷的鎖獲取操作,公平隊列,以及非塊結構的鎖。 前言 系列文章目錄 前面幾篇我們學習了synchronized同步代碼塊,了解了java的內置鎖,并學習了監視器鎖的wait/notify機制。在大多數情況下,內置鎖都能很好的工作,但它在功能上存在一些局限性,例如無法實現非阻塞結構的加鎖規則等。為了拓展同步代...
閱讀 1407·2021-11-24 10:20
閱讀 3649·2021-11-24 09:38
閱讀 2294·2021-09-27 13:37
閱讀 2196·2021-09-22 15:25
閱讀 2270·2021-09-01 18:33
閱讀 3488·2019-08-30 15:55
閱讀 1783·2019-08-30 15:54
閱讀 2081·2019-08-30 12:50