摘要:而導致這個問題的原因是線程并行執行操作并不是原子的,存在線程安全問題。如果已經有線程持有了鎖,那這個線程會獨占鎖,直到鎖釋放完畢之前,其他線程都會被阻塞。當鎖處于重量級鎖狀態,其他線程嘗試獲取鎖時,都會被阻塞,也就是狀態。
1. 什么時候需要用Synchronized
Synchronized主要作用是在多個線程操作共享數據的時候,保證對共享數據訪問的線程安全性。比如兩個線程對于i這個共享變量同時做i++遞增操作,那么這個時候對于i這個值來說就存在一個不確定性,也就是說理論上i的值應該是2,但是也可能是1。而導致這個問題的原因是線程并行執行i++操作并不是原子的,存在線程安全問題。所以通常來說解決辦法是通過加鎖來實現線程的串行執行,而synchronized就是java中鎖的實現的關鍵字。
synchronized在并發編程中是一個非常重要的角色,在JDK1.6之前,它是一個重量級鎖的角色,但是在JDK1.6之后對synchronized做了優化,優化以后性能有了較大的提升
2.Synchronized的使用
synchronized有三種使用方法,這三種使用方法分別對應三種不同的作用域,代碼如下:
①修飾普通同步方法
將synchronized修飾在普通同步方法,那么該鎖的作用域是在當前實例對象范圍內,也就是說對于 SyncDemosd=newSyncDemo();這一個實例對象sd來說,多個線程訪問access方法會有鎖的限制。如果access已經有線程持有了鎖,那這個線程會獨占鎖,直到鎖釋放完畢之前,其他線程都會被阻塞。
public SyncDemo{
Object lock =new Object();
//形式1
public synchronized void access(){
//
}
//形式2,作用域等同于形式1
public void access1(){
synchronized(lock){
//
}
}
②修飾靜態同步方法
修飾靜態同步方法或者靜態對象、類,那么這個鎖的作用范圍是類級別。舉個簡單的例子,{SyncDemo sd=SyncDemo();SyncDemo sd2=new SyncDemo();} 兩個不同的實例sd和sd2, 如果sd這個實例訪問access方法并且成功持有了鎖,那么sd2這個對象如果同樣來訪問access方法,那么它必須要等待sd這個對象的鎖釋放以后,sd2這個對象的線程才能訪問該方法,這就是類鎖;也就是說類鎖就相當于全局鎖的概念,作用范圍是類級別。
public SyncDemo{
static Object lock=new Object();
//形式1
public synchronized static void access(){
//
}
//形式2等同于形式1
public void access1(){
synchronized(lock){
//
}
}
//形式3等同于前面兩種
public void access2(){
synchronzied(SyncDemo.class){
//
}
}
}
③.同步方法塊
同步方法塊,是范圍最小的鎖,鎖的是synchronized括號里面配置的對象。這種鎖在實際工作中使用得比較頻繁,畢竟鎖的作用范圍越大,那么對性能的影響就越嚴重。
public SyncDemo{
Object lock=new Object();
public void access(){
//do something
synchronized(lock){
//
}
}
}
3.Synchronized的實現原理分析
synchronized實現的鎖是存儲在Java對象頭里,什么是對象頭呢?在Hotspot虛擬機中,對象在內存中的存儲布局,可以分為三個區域:對象頭(Header)、實例數據(Instance Data)、對齊填充(Padding) 當我們在Java代碼中,使用new創建一個對象實例的時候,(hotspot虛擬機)JVM層面實際上會創建一個 instanceOopDesc對象。instanceOopDesc的定義在Hotspot源碼中的 instanceOop.hpp文件中,另外,arrayOopDesc的定義對應 arrayOop.hpp
從instanceOopDesc代碼中可以看到 instanceOopDesc繼承自oopDesc,oopDesc的定義載Hotspot源碼中的 oop.hpp文件中。
在普通實例對象中,oopDesc的定義包含兩個成員,分別是 _mark和 _metadata,其中_mark表示對象標記、屬于markOop類型,也就是Mark World,它記錄了對象和鎖有關的信。_metadata表示類元信息,類元信息存儲的是對象指向它的類元數據(Klass)的首地址,其中Klass表示普通指針、 _compressed_klass表示壓縮類指針。
Mark Word
前面說的普通對象的對象頭由兩部分組成,分別是markOop以及類元信息,markOop官方稱為Mark Word 。在Hotspot中,markOop的定義在 markOop.hpp文件中,代碼如下
Mark word記錄了對象和鎖有關的信息,當某個對象被synchronized關鍵字當成同步鎖時,那么圍繞這個鎖的一系列操作都和Mark word有關系。Mark Word在32位虛擬機的長度是32bit、在64位虛擬機的長度是64bit。 Mark Word里面存儲的數據會隨著鎖標志位的變化而變化。
鎖標志位的表示意義
1.鎖標識 lock=00 表示輕量級鎖
2.鎖標識 lock=10 表示重量級鎖
3.偏向鎖標識 biased_lock=1表示偏向鎖
4.偏向鎖標識 biased_lock=0且鎖標識=01表示無鎖狀態
4.鎖的升級
前面提到了鎖的幾個概念,偏向鎖、輕量級鎖、重量級鎖。在JDK1.6之前,synchronized是一個重量級鎖,性能比較差。從JDK1.6開始,為了減少獲得鎖和釋放鎖帶來的性能消耗,synchronized進行了優化,引入了 偏向鎖和 輕量級鎖的概念。所以從JDK1.6開始,鎖一共會有四種狀態,鎖的狀態根據競爭激烈程度從低到高分別是:無鎖狀態->偏向鎖狀態->輕量級鎖狀態->重量級鎖狀態。這幾個狀態會隨著鎖競爭的情況逐步升級。為了提高獲得鎖和釋放鎖的效率,鎖可以升級但是不能降級。
偏向鎖
在大多數的情況下,鎖不僅不存在多線程的競爭,而且總是由同一個線程獲得。因此為了讓線程獲得鎖的代價更低引入了偏向鎖的概念。偏向鎖的意思是如果一個線程獲得了一個偏向鎖,如果在接下來的一段時間中沒有其他線程來競爭鎖,那么持有偏向鎖的線程再次進入或者退出同一個同步代碼塊,不需要再次進行搶占鎖和釋放鎖的操作。偏向鎖可以通過 -XX:+UseBiasedLocking開啟或者關閉。
偏向鎖的獲取
偏向鎖的獲取過程非常簡單,當一個線程訪問同步塊獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲偏向鎖的線程ID,表示哪個線程獲得了偏向鎖,結合前面分析的Mark Word來分析一下偏向鎖的獲取邏輯
1首先獲取目標對象的Mark Word,根據鎖的標識為和epoch去判斷當前是否處于可偏向的狀態
2如果為可偏向狀態,則通過CAS操作將自己的線程ID寫入到MarkWord,如果CAS操作成功,則表示當前線程成功獲取到偏向鎖,繼續執行同步代碼塊
3如果是已偏向狀態,先檢測MarkWord中存儲的threadID和當前訪問的線程的threadID是否相等,如果相等,表示當前線程已經獲得了偏向鎖,則不需要再獲得鎖直接執行同步代碼;如果不相等,則證明當前鎖偏向于其他線程,需要撤銷偏向鎖。
偏向鎖的撤銷
當其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放偏向鎖,撤銷偏向鎖的過程需要等待一個全局安全點(所有工作線程都停止字節碼的執行)。
1首先,暫停擁有偏向鎖的線程,然后檢查偏向鎖的線程是否為存活狀態
2如果線程已經死了,直接把對象頭設置為無鎖狀態
3如果還活著,當達到全局安全點時獲得偏向鎖的線程會被掛起,接著偏向鎖升級為輕量級鎖,然后喚醒被阻塞在全局安全點的線程繼續往下執行同步代碼
輕量級鎖
前面我們知道,當存在超過一個線程在競爭同一個同步代碼塊時,會發生偏向鎖的撤銷。偏向鎖撤銷以后對象會可能會處于兩種狀態
1.一種是不可偏向的無鎖狀態,簡單來說就是已經獲得偏向鎖的線程已經退出了同步代碼塊,那么這個時候會撤銷偏向鎖,并升級為輕量級鎖
2.一種是不可偏向的已鎖狀態,簡單來說就是已經獲得偏向鎖的線程正在執行同步代碼塊,那么這個時候會升級到輕量級鎖并且被原持有鎖的線程獲得鎖
輕量級鎖加鎖
1.JVM會先在當前線程的棧幀中創建用于存儲鎖記錄的空間(LockRecord)
2.將對象頭中的Mark Word復制到鎖記錄中,稱為Displaced Mark Word.
3.線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針
4.如果替換成功,表示當前線程獲得輕量級鎖,如果失敗,表示存在其他線程競爭鎖,那么當前線程會嘗試使用CAS來獲取鎖,當自旋超過指定次數(可以自定義)時仍然無法獲得鎖,此時鎖會膨脹升級為重量級鎖
輕量鎖解鎖
1.嘗試CAS操作將所記錄中的Mark Word替換回到對象頭中
2.如果成功,表示沒有競爭發生
3.如果失敗,表示當前鎖存在競爭,鎖會膨脹成重量級鎖
一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處于重量級鎖狀態,其他線程嘗試獲取鎖時,都會被阻塞,也就是 BLOCKED狀態。當持有鎖的線程釋放鎖之后會喚醒這些現場,被喚醒之后的線程會進行新一輪的競爭
重量級鎖
重量級鎖依賴對象內部的monitor鎖來實現,而monitor又依賴操作系統的MutexLock(互斥鎖),假設Mutex變量的值為1,表示互斥鎖空閑,這個時候某個線程調用lock可以獲得鎖,而Mutex的值為0表示互斥鎖已經被其他線程獲得,其他線程調用lock只能掛起等待。
為什么重量級鎖的開銷比較大呢?原因是當系統檢查到是重量級鎖之后,會把等待想要獲取鎖的線程阻塞,被阻塞的線程不會消耗CPU,但是阻塞或者喚醒一個線程,都需要通過操作系統來實現,也就是相當于從用戶態轉化到內核態,而轉化狀態是需要消耗時間的。
文章版權歸作者所有,未經允許請勿轉載,若此文章存在違規行為,您可以聯系管理員刪除。
轉載請注明本文地址:http://m.specialneedsforspecialkids.com/yun/74435.html
摘要:而導致這個問題的原因是線程并行執行操作并不是原子的,存在線程安全問題。表示自旋鎖,由于線程的阻塞和喚醒需要從用戶態轉為核心態,頻繁的阻塞和喚醒對來說性能開銷很大。 文章簡介 synchronized想必大家都不陌生,用來解決線程安全問題的利器。同時也是Java高級程序員面試比較常見的面試題。這篇文正會帶大家徹底了解synchronized的實現。 內容導航 什么時候需要用Synchr...
摘要:的主要功能和關鍵字一致,均是用于多線程的同步。而僅支持通過查詢當前線程是否持有鎖。由于和使用的是同一把可重入鎖,所以線程可以進入方法,并再次獲得鎖,而不會被阻塞住。公平與非公平公平與非公平指的是線程獲取鎖的方式。 1.簡介 可重入鎖ReentrantLock自 JDK 1.5 被引入,功能上與synchronized關鍵字類似。所謂的可重入是指,線程可對同一把鎖進行重復加鎖,而不會被阻...
摘要:由此可見,自旋鎖和各有優劣,他們分別適用于競爭不多和競爭激烈的場景中。每一個試圖進入同步代碼塊的線程都會被封裝成對象,它們或在對象的中,或在中,等待成為對象的成為的對象即獲取了監視器鎖。 前言 系列文章目錄 前面兩篇文章我們介紹了synchronized同步代碼塊以及wait和notify機制,大致知道了這些關鍵字和方法是干什么的,以及怎么用。 但是,知其然,并不知其所以然。 例如...
摘要:一言以蔽之,被修飾的變量能夠保證每個線程能夠獲取該變量的最新值,從而避免出現數據臟讀的現象。為了實現內存語義時,編譯器在生成字節碼時,會在指令序列中插入內存屏障來禁止特定類型的處理器重排序。volatile原理volatile簡介Java內存模型告訴我們,各個線程會將共享變量從主內存中拷貝到工作內存,然后執行引擎會基于工作內存中的數據進行操作處理。 線程在工作內存進行操作后何時會寫到主內存中...
閱讀 1566·2021-09-22 15:52
閱讀 3472·2021-09-22 14:59
閱讀 2852·2021-09-02 15:12
閱讀 980·2021-08-20 09:35
閱讀 1585·2019-08-30 14:09
閱讀 2717·2019-08-30 13:56
閱讀 1656·2019-08-26 18:27
閱讀 3370·2019-08-26 13:37