您好,登錄后才能下訂單哦!
本篇文章給大家分享的是有關sychronized關鍵字的作用是什么,小編覺得挺實用的,因此分享給大家學習,希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
public class Test { Object lock = new Object(); int i = 0; public void f() { sychronized (lock) { i++; } } }
適用于方法體比較大或者耗時,但需要同步的代碼塊比較短的場景。
public class Test { int i = 0; public sychronized void f() { i++; } }
當sychronized關鍵字修飾一個非靜態方法的時候,其鎖對象是所修飾方法所屬的實例。也就是說若a實例有m1和m2兩個方法都被sychronized關鍵字修飾,那么他們是沒法在多個線程中同時執行的,存在對a實例的鎖競爭。
public class Test { public sychronized void f() { System.out.println("test sychronized"); } }
當sychronized修飾靜態方法是,其鎖對象是所修飾方法所屬的class對象。
對sychronized支持的實現是在JVM層面的,每個Java對象都存在一個叫做對象監視器的結構,而同步過程就是依賴于對這個同步監視器的持有權的競爭來實現的。下面介紹一個概念————對象頭。
每個Java對象在JVM中都分為三塊區域:對象頭,實例數據和填充對齊。
實例數據:存放類及其父類的屬性信息
填充對齊:由于虛擬機要求對象起始地址必須是8字節的整數倍。所以不滿整數倍的會有一些額外的空間來補齊,類似于C語言中的結構體。
而對象頭則是存儲了一些Java對象的額外信息,主要包括一些運行時數據(Mark Word)、類型指針、若對象為數組,則還包括數組長度。運行時數據有:hashcode、GC分代年齡、鎖狀態標識、以及根據不同鎖的類型,該結構的內容也會有一些變化。sychronized用的鎖就是存在Java對象頭里的。在64位虛擬機下,Mark Word是64bit。不同鎖的狀態下,其結構如下:
無鎖:(25bit)Unused + (31bit)HashCode + (1bit)cms_free + (4bit)分代年齡 + (1bit)0 + (2bit) 鎖標志位01
偏向鎖:(54bit)ThreadID + (2bit)Epoch + (1bit)cms_free + (4bit)分代年齡 + (1bit)1 + (2bit) 鎖標志位01
輕量級鎖:(62bit) ptr_to_lock_record + (2bit) 鎖標志位00
重量級鎖:(62bit) ptr_to_heavyweight_monitor + (2bit) 鎖標志位10
還有一種GC情況下,其結構為:(62bit) Unused + (2bit) GC標記11
這里我們先討論重量級鎖的情況,也就是sychronized常說的對象鎖,此時Mark Word中的前62bit是一個指向重量級鎖對象的指針,sychronized在JVM中是通過monitorenter和monitorexit指令來實現的,在底層則是通過爭奪重量級鎖對象的方式來實現方法同步和代碼塊同步。鎖對象被定義為ObjectMonitor,其結構如下:
ObjectMonitor() { _header = NULL; _count = 0; // 獲取鎖的次數 _waiters = 0, _recursions = 0; _object = NULL; _owner = NULL; // 當前持有鎖的線程 _WaitSet = NULL; // 處于wait狀態的線程,會被加入到_WaitSet _WaitSetLock = 0 ; _Responsible = NULL ; _succ = NULL ; _cxq = NULL ; FreeNext = NULL ; _EntryList = NULL ; // 處于等待鎖block狀態的線程,會被加入到該列表 _SpinFreq = 0 ; _SpinClock = 0 ; OwnerIsThread = 0 ; }
ObjectMonitor中有兩個隊列,_WaitSet
和 _EntryList
,用來保存ObjectWaiter對象列表( 每個等待鎖的線程都會被封裝成ObjectWaiter對象 )
當多個線程同時訪問一段同步代碼時,首先會進入 _EntryList
集合,當線程獲取到對象的monitor后進入 _Owner
區域并把monitor中的owner變量設置為當前線程同時monitor中的計數器count加1。
若線程調用 wait() 方法,將釋放當前持有的monitor,owner變量恢復為null,count自減1,同時該線程進入 WaitSet集合中等待被喚醒。
若當前線程執行完畢也將釋放monitor(鎖)并復位變量的值,以便其他線程進入獲取monitor(鎖)。
monitor對象存在于每個Java對象的對象頭中(存儲的指針的指向),synchronized鎖便是通過這種方式獲取鎖的,也是為什么Java中任意對象可以作為鎖的原因,同時也是notify/notifyAll/wait等方法存在于頂級對象Object中的原因。
同步語句塊的實現使用的是monitorenter 和 monitorexit 指令,其中monitorenter指令指向同步代碼塊的開始位置,monitorexit指令則指明同步代碼塊的結束位置。
當執行monitorenter指令時,當前線程將試圖獲取對象鎖的持有權,當_count為0時,那線程可以成功取得對象鎖,并將計數器值設置為1,取鎖成功。如果當前線程已經擁有某一對象鎖,那它可以重入這個鎖,重入時計數器的值也會加1。倘若其他線程已經擁有對象鎖的所有權,則當前線程阻塞,直到正在執行的線程執行monitorexit指令完畢,執行線程將釋放鎖并設置計數器值為0,其他線程將有機會持有鎖。
方法級的同步是隱式,即無需通過字節碼指令來控制的,它實現在方法調用和返回操作之中。JVM可以從方法常量池中的方法表結構中的ACC_SYNCHRONIZED 訪問標志區分一個方法是否同步方法。
當方法調用時,調用指令將會檢查方法的ACC_SYNCHRONIZED訪問標志是否被設置,如果設置了,執行線程將先持有monitor(虛擬機規范中用的是管程一詞), 然后再執行方法,最后再方法完成(無論是正常完成還是非正常完成)時釋放monitor。在方法執行期間,執行線程持有了monitor,其他任何線程都無法再獲得同一個monitor。
早起的Java版本中,synchronized屬于重量級鎖,效率低下,因為監視器鎖(monitor)是依賴于底層的操作系統的Mutex Lock來實現的,而操作系統實現線程之間的切換時需要從用戶態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,這也是為什么早期的synchronized效率低的原因。
Java 6之后,為了減少獲得鎖和釋放鎖所帶來的性能消耗,引入了輕量級鎖和偏向鎖。鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖,但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級。
偏向鎖是JDK6中的重要引進,因為HotSpot作者經過研究實踐發現,在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低,引進了偏向鎖。
偏向鎖是在單線程執行代碼塊時使用的機制,如果在多線程并發的環境下(即線程A尚未執行完同步代碼塊,線程B發起了申請鎖的申請),則一定會轉化為輕量級鎖或者重量級鎖。
在JDK5中偏向鎖默認是關閉的,而到了JDK6中偏向鎖已經默認開啟。如果并發數較大同時同步代碼塊執行時間較長,則被多個線程同時訪問的概率就很大,就可以使用參數-XX:-UseBiasedLocking來禁止偏向鎖(但這是個JVM參數,不能針對某個對象鎖來單獨設置)。
引入偏向鎖主要目的是:為了在沒有多線程競爭的情況下盡量減少不必要的輕量級鎖執行路徑。因為輕量級鎖的加鎖解鎖操作是需要依賴多次CAS原子指令的,而偏向鎖只需要在置換ThreadID的時候依賴一次CAS原子指令(由于一旦出現多線程競爭的情況就必須撤銷偏向鎖,所以偏向鎖的撤銷操作的性能損耗也必須小于節省下來的CAS原子指令的性能消耗)。
輕量級鎖是為了在線程交替執行同步塊時提高性能,而偏向鎖則是在只有一個線程執行同步塊時進一步提高性能。
當一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程進入和退出同步塊時不需要花費CAS操作來爭奪鎖資源,只需要檢查是否為偏向鎖、鎖標識為以及ThreadID即可。
偏向鎖的釋放采用了 一種只有競爭才會釋放鎖的機制,線程是不會主動去釋放偏向鎖,需要等待其他線程來競爭。
引入輕量級鎖的主要目的是在沒有多線程競爭的前提下,減少傳統的重量級鎖使用操作系統互斥量產生的性能消耗。其適用場景為線程交替執行同步塊的情況。當關閉偏向鎖功能或者多個線程競爭偏向鎖導致偏向鎖升級為輕量級鎖,則會嘗試獲取輕量級鎖。其步驟如下:
在線程進入同步塊時,如果同步對象鎖狀態為無鎖狀態(鎖標志位為“01”狀態,是否為偏向鎖為“0”),虛擬機首先將在當前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝,官方稱之為 Displaced Mark Word;
拷貝對象頭中的Mark Word復制到鎖記錄(Lock Record)中;
拷貝成功后,虛擬機將使用CAS操作嘗試將對象Mark Word中的Lock Word更新為指向當前線程Lock Record的指針,并將Lock record里的owner指針指向object mark word。如果更新成功,則執行步驟(4),否則執行步驟(5);
如果這個更新動作成功了,那么當前線程就擁有了該對象的鎖,并且對象Mark Word的鎖標志位設置為“00”,即表示此對象處于輕量級鎖定狀態;
如果這個更新操作失敗了,虛擬機首先會檢查對象Mark Word中的Lock Word是否指向當前線程的棧幀,如果是,就說明當前線程已經擁有了這個對象的鎖,那就可以直接進入同步塊繼續執行。否則說明多個線程競爭鎖,進入自旋執行(3),若自旋結束時仍未獲得鎖,輕量級鎖就要膨脹為重量級鎖,鎖標志的狀態值變為“10”,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,當前線程以及后面等待鎖的線程也要進入阻塞狀態。
對于輕量級鎖,其性能提升的依據是 “對于絕大部分的鎖,在整個生命周期內都是不會存在競爭的”,如果打破這個依據則除了互斥的開銷外,還有額外的CAS操作,因此在有多線程競爭的情況下,輕量級鎖比重量級鎖更慢。
輕量級鎖失敗后,虛擬機為了避免線程真實地在操作系統層面掛起,還會進行一項稱為自旋鎖的優化手段。這是基于在大多數情況下,線程持有鎖的時間都不會太長,如果直接掛起操作系統層面的線程可能會得不償失,畢竟操作系統實現線程之間的切換時需要從用戶態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,因此自旋鎖會假設在不久將來,當前的線程可以獲得鎖,因此虛擬機會讓當前想要獲取鎖的線程做幾個空循環(這也是稱為自旋的原因),一般不會太久,可能是50個循環或100循環,在經過若干次循環后,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會將線程在操作系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是可以提升效率的。最后沒辦法也就只能升級為重量級鎖了。
消除鎖是虛擬機另外一種鎖的優化,這種優化更徹底,Java虛擬機在JIT編譯時(可以簡單理解為當某段代碼即將第一次被執行時進行編譯,又稱即時編譯),通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間。
sychronized是在JVM層面提供的支持,而Lock接口的系列實現則是在jdk層面和操作系統層面。
sychronized的實現涉及到鎖的升級,而ReetrantLock的實現則是通過AQS結構,CAS保證原子性,volatile保證可見性
synchronized 不需要用戶去手動釋放鎖,synchronized 代碼執行完后系統會自動讓線程釋放對鎖的占用; ReentrantLock則需要用戶去手動釋放鎖,如果沒有手動釋放鎖,就可能導致死鎖現象。一般通過lock()和unlock()方法配合try/finally語句塊來完成,使釋放更加靈活。
synchronized是不可中斷類型的鎖,除非加鎖的代碼中出現異常或正常執行完成; ReentrantLock則可以中斷,可通過trylock(long timeout,TimeUnit unit)設置超時方法或者將lockInterruptibly()放到代碼塊中,調用interrupt方法進行中斷。
synchronized為非公平鎖 ReentrantLock則即可以選公平鎖也可以選非公平鎖,通過構造方法new ReentrantLock時傳入boolean值進行選擇,為空默認false非公平鎖,true為公平鎖。
synchronized不能綁定; ReentrantLock通過綁定Condition結合await()/singal()方法實現線程的精確喚醒,而不是像synchronized通過Object類的wait()/notify()/notifyAll()方法要么隨機喚醒一個線程要么喚醒全部線程。
synchronzied鎖的是對象,鎖是保存在對象頭里面的,根據對象頭數據來標識是否有線程獲得鎖/爭搶鎖;ReentrantLock鎖的是線程,根據進入的線程和int類型的state標識鎖的獲得/爭搶。
從互斥鎖的設計上來說,當一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時,將會處于阻塞狀態,但當一個線程再次請求自己持有對象鎖的臨界資源時,這種情況屬于重入鎖,請求將會成功,在java中synchronized是基于原子性的內部鎖機制,是可重入的,因此在一個線程調用synchronized方法的同時在其方法體內部調用該對象另一個synchronized方法,也就是說一個線程得到一個對象鎖后再次請求該對象鎖,是允許的,這就是synchronized的可重入性。
當一個線程處于被阻塞狀態或者試圖執行一個阻塞操作時,使用Thread.interrupt()方式中斷該線程,注意此時將會拋出一個InterruptedException的異常,同時中斷狀態將會被復位(由中斷狀態改為非中斷狀態)。
但是,當線程處于運行期且是非阻塞狀態,直接調用interrupt()方法中斷線程,是不會得到任何響應的。
線程的中斷操作對于正在等待獲取的鎖對象的synchronized方法或者代碼塊并不起作用,也就是對于synchronized來說,如果一個線程在等待鎖,那么結果只有兩種,要么它獲得這把鎖繼續執行,要么它就保存等待,即使調用中斷線程的方法,也不會生效。
每個對象都有notify/notifyAll和wait這三個頂級方法,在使用這3個方法時,必須處于synchronized代碼塊或者synchronized方法中,否則就會拋出IllegalMonitorStateException異常,這是因為調用這幾個方法前必須拿到當前對象的監視器monitor對象,也就是說notify/notifyAll和wait方法依賴于monitor對象,在前面的分析中,我們知道monitor 存在于對象頭的Mark Word 中(存儲monitor引用指針),而synchronized關鍵字可以獲取 monitor ,這也就是為什么notify/notifyAll和wait方法必須在synchronized代碼塊或者synchronized方法調用的原因。
以上就是sychronized關鍵字的作用是什么,小編相信有部分知識點可能是我們日常工作會見到或用到的。希望你能通過這篇文章學到更多知識。更多詳情敬請關注億速云行業資訊頻道。
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。