摘要:而導(dǎo)致這個問題的原因是線程并行執(zhí)行操作并不是原子的,存在線程安全問題。表示自旋鎖,由于線程的阻塞和喚醒需要從用戶態(tài)轉(zhuǎn)為核心態(tài),頻繁的阻塞和喚醒對來說性能開銷很大。
文章簡介
synchronized想必大家都不陌生,用來解決線程安全問題的利器。同時也是Java高級程序員面試比較常見的面試題。這篇文正會帶大家徹底了解synchronized的實現(xiàn)。
內(nèi)容導(dǎo)航什么時候需要用Synchronized
synchronized的使用
synchronized的實現(xiàn)原理分析
什么時候需要用Synchronized想必大家對synchronized都不陌生,主要作用是在多個線程操作共享數(shù)據(jù)的時候,保證對共享數(shù)據(jù)訪問的線程安全性。
比如在下面這個圖片中,兩個線程對于i這個共享變量同時做i++遞增操作,那么這個時候?qū)τ趇這個值來說就存在一個不確定性,也就是說理論上i的值應(yīng)該是2,但是也可能是1。而導(dǎo)致這個問題的原因是線程并行執(zhí)行i++操作并不是原子的,存在線程安全問題。所以通常來說解決辦法是通過加鎖來實現(xiàn)線程的串行執(zhí)行,而synchronized就是java中鎖的實現(xiàn)的關(guān)鍵字。
synchronized在并發(fā)編程中是一個非常重要的角色,在JDK1.6之前,它是一個重量級鎖的角色,但是在JDK1.6之后對synchronized做了優(yōu)化,優(yōu)化以后性能有了較大的提升(這塊會在后面做詳細的分析)。
先來看一下synchronized的使用
Synchronized的使用synchronized有三種使用方法,這三種使用方法分別對應(yīng)三種不同的作用域,代碼如下
修飾普通同步方法將synchronized修飾在普通同步方法,那么該鎖的作用域是在當(dāng)前實例對象范圍內(nèi),也就是說對于 SyncDemosd=newSyncDemo();這一個實例對象sd來說,多個線程訪問access方法會有鎖的限制。如果access已經(jīng)有線程持有了鎖,那這個線程會獨占鎖,直到鎖釋放完畢之前,其他線程都會被阻塞
public SyncDemo{ Object lock =new Object(); //形式1 public synchronized void access(){ // } //形式2,作用域等同于形式1 public void access1(){ synchronized(lock){ // } } //形式3,作用域等同于前面兩種 public void access2(){ synchronized(this){ // } } }修飾靜態(tài)同步方法
修飾靜態(tài)同步方法或者靜態(tài)對象、類,那么這個鎖的作用范圍是類級別。舉個簡單的例子,
SyncDemo sd=SyncDemo(); SyncDemo sd2=new SyncDemo();}
兩個不同的實例sd和sd2, 如果sd這個實例訪問access方法并且成功持有了鎖,那么sd2這個對象如果同樣來訪問access方法,那么它必須要等待sd這個對象的鎖釋放以后,sd2這個對象的線程才能訪問該方法,這就是類鎖;也就是說類鎖就相當(dāng)于全局鎖的概念,作用范圍是類級別。
這里拋一個小問題,大家看看能不能回答,如果不能也沒關(guān)系,后面會講解;問題是如果sd先訪問access獲得了鎖,sd2對象的線程再訪問access1方法,那么它會被阻塞嗎?
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){ // } } }步方法塊
public SyncDemo{ Object lock=new Object(); public void access(){ //do something synchronized(lock){ // } } }
通過演示3種不同鎖的使用,讓大家對synchronized有了初步的認識。當(dāng)一個線程視圖訪問帶有synchronized修飾的同步代碼塊或者方法時,必須要先獲得鎖。當(dāng)方法執(zhí)行完畢退出以后或者出現(xiàn)異常的情況下會自動釋放鎖。如果大家認真看了上面的三個案例,那么應(yīng)該知道鎖的范圍控制是由對象的作用域決定的。對象的作用域越大,那么鎖的范圍也就越大,因此我們可以得出一個初步的猜想,synchronized和對象有非常大的關(guān)系。那么,接下來就去剖析一下鎖的原理Synchronized的實現(xiàn)原理分析
當(dāng)一個線程嘗試訪問synchronized修飾的代碼塊時,它首先要獲得鎖,那么這個鎖到底存在哪里呢?對象在內(nèi)存中的布局
synchronized實現(xiàn)的鎖是存儲在Java對象頭里,什么是對象頭呢?在Hotspot虛擬機中,對象在內(nèi)存中的存儲布局,可以分為三個區(qū)域:對象頭(Header)、實例數(shù)據(jù)(Instance Data)、對齊填充(Padding)
當(dāng)我們在Java代碼中,使用new創(chuàng)建一個對象實例的時候,(hotspot虛擬機)JVM層面實際上會創(chuàng)建一個 instanceOopDesc對象。
Hotspot虛擬機采用OOP-Klass模型來描述Java對象實例,OOP(Ordinary Object Point)指的是普通對象指針,Klass用來描述對象實例的具體類型。Hotspot采用instanceOopDesc和arrayOopDesc來描述對象頭,arrayOopDesc對象用來描述數(shù)組類型
instanceOopDesc的定義在Hotspot源碼中的 instanceOop.hpp文件中,另外,arrayOopDesc的定義對應(yīng) arrayOop.hpp
class instanceOopDesc : public oopDesc { public: // aligned header size. static int header_size() { return sizeof(instanceOopDesc)/HeapWordSize; } // If compressed, the offset of the fields of the instance may not be aligned. static int base_offset_in_bytes() { // offset computation code breaks if UseCompressedClassPointers // only is true return (UseCompressedOops && UseCompressedClassPointers) ? klass_gap_offset_in_bytes() : sizeof(instanceOopDesc); } static bool contains_field_offset(int offset, int nonstatic_field_size) { int base_in_bytes = base_offset_in_bytes(); return (offset >= base_in_bytes && (offset-base_in_bytes) < nonstatic_field_size * heapOopSize); } }; #endif // SHARE_VM_OOPS_INSTANCEOOP_HPP
從instanceOopDesc代碼中可以看到 instanceOopDesc繼承自oopDesc,oopDesc的定義載Hotspot源碼中的 oop.hpp文件中
class oopDesc { friend class VMStructs; private: volatile markOop _mark; union _metadata { Klass* _klass; narrowKlass _compressed_klass; } _metadata; // Fast access to barrier set. Must be initialized. static BarrierSet* _bs; ... }
在普通實例對象中,oopDesc的定義包含兩個成員,分別是 _mark和 _metadata
_mark表示對象標(biāo)記、屬于markOop類型,也就是接下來要講解的Mark World,它記錄了對象和鎖有關(guān)的信息
_metadata表示類元信息,類元信息存儲的是對象指向它的類元數(shù)據(jù)(Klass)的首地址,其中Klass表示普通指針、 _compressed_klass表示壓縮類指針
Mark Word在前面我們提到過,普通對象的對象頭由兩部分組成,分別是markOop以及類元信息,markOop官方稱為Mark Word
在Hotspot中,markOop的定義在 markOop.hpp文件中,代碼如下
class markOopDesc: public oopDesc { private: // Conversion uintptr_t value() const { return (uintptr_t) this; } public: // Constants enum { age_bits = 4, //分代年齡 lock_bits = 2, //鎖標(biāo)識 biased_lock_bits = 1, //是否為偏向鎖 max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits, hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits, //對象的hashcode cms_bits = LP64_ONLY(1) NOT_LP64(0), epoch_bits = 2 //偏向鎖的時間戳 }; ...
Mark word記錄了對象和鎖有關(guān)的信息,當(dāng)某個對象被synchronized關(guān)鍵字當(dāng)成同步鎖時,那么圍繞這個鎖的一系列操作都和Mark word有關(guān)系。Mark Word在32位虛擬機的長度是32bit、在64位虛擬機的長度是64bit。
Mark Word里面存儲的數(shù)據(jù)會隨著鎖標(biāo)志位的變化而變化,Mark Word可能變化為存儲以下5中情況
鎖標(biāo)志位的表示意義
鎖標(biāo)識 lock=00 表示輕量級鎖
鎖標(biāo)識 lock=10 表示重量級鎖
偏向鎖標(biāo)識 biased_lock=1表示偏向鎖
偏向鎖標(biāo)識 biased_lock=0且鎖標(biāo)識=01表示無鎖狀態(tài)
到目前為止,我們再總結(jié)一下前面的內(nèi)容,synchronized(lock)中的lock可以用Java中任何一個對象來表示,而鎖標(biāo)識的存儲實際上就是在lock這個對象中的對象頭內(nèi)。大家懂了嗎?
其實前面只提到了鎖標(biāo)志位的存儲,但是為什么任意一個Java對象都能成為鎖對象呢?
首先,Java中的每個對象都派生自O(shè)bject類,而每個Java Object在JVM內(nèi)部都有一個native的C++對象 oop/oopDesc進行對應(yīng)。
其次,線程在獲取鎖的時候,實際上就是獲得一個監(jiān)視器對象(monitor) ,monitor可以認為是一個同步對象,所有的Java對象是天生攜帶monitor.
在hotspot源碼的 markOop.hpp文件中,可以看到下面這段代碼。
ObjectMonitor* monitor() const { assert(has_monitor(), "check"); // Use xor instead of &~ to provide one extra tag-bit check. return (ObjectMonitor*) (value() ^ monitor_value); }
多個線程訪問同步代碼塊時,相當(dāng)于去爭搶對象監(jiān)視器修改對象中的鎖標(biāo)識,上面的代碼中ObjectMonitor這個對象和線程爭搶鎖的邏輯有密切的關(guān)系(后續(xù)會詳細分析)
鎖的升級前面提到了鎖的幾個概念,偏向鎖、輕量級鎖、重量級鎖。在JDK1.6之前,synchronized是一個重量級鎖,性能比較差。從JDK1.6開始,為了減少獲得鎖和釋放鎖帶來的性能消耗,synchronized進行了優(yōu)化,引入了 偏向鎖和 輕量級鎖的概念。所以從JDK1.6開始,鎖一共會有四種狀態(tài),鎖的狀態(tài)根據(jù)競爭激烈程度從低到高分別是:無鎖狀態(tài)->偏向鎖狀態(tài)->輕量級鎖狀態(tài)->重量級鎖狀態(tài)。這幾個狀態(tài)會隨著鎖競爭的情況逐步升級。為了提高獲得鎖和釋放鎖的效率,鎖可以升級但是不能降級。
下面就詳細講解synchronized的三種鎖的狀態(tài)及升級原理
在大多數(shù)的情況下,鎖不僅不存在多線程的競爭,而且總是由同一個線程獲得。因此為了讓線程獲得鎖的代價更低引入了偏向鎖的概念。偏向鎖的意思是如果一個線程獲得了一個偏向鎖,如果在接下來的一段時間中沒有其他線程來競爭鎖,那么持有偏向鎖的線程再次進入或者退出同一個同步代碼塊,不需要再次進行搶占鎖和釋放鎖的操作。偏向鎖可以通過 -XX:+UseBiasedLocking開啟或者關(guān)閉
偏向鎖的獲取偏向鎖的獲取過程非常簡單,當(dāng)一個線程訪問同步塊獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲偏向鎖的線程ID,表示哪個線程獲得了偏向鎖,結(jié)合前面分析的Mark Word來分析一下偏向鎖的獲取邏輯
首先獲取目標(biāo)對象的Mark Word,根據(jù)鎖的標(biāo)識為和epoch去判斷當(dāng)前是否處于可偏向的狀態(tài)
如果為可偏向狀態(tài),則通過CAS操作將自己的線程ID寫入到MarkWord,如果CAS操作成功,則表示當(dāng)前線程成功獲取到偏向鎖,繼續(xù)執(zhí)行同步代碼塊
如果是已偏向狀態(tài),先檢測MarkWord中存儲的threadID和當(dāng)前訪問的線程的threadID是否相等,如果相等,表示當(dāng)前線程已經(jīng)獲得了偏向鎖,則不需要再獲得鎖直接執(zhí)行同步代碼;如果不相等,則證明當(dāng)前鎖偏向于其他線程,需要撤銷偏向鎖。
CAS:表示自旋鎖,由于線程的阻塞和喚醒需要CPU從用戶態(tài)轉(zhuǎn)為核心態(tài),頻繁的阻塞和喚醒對CPU來說性能開銷很大。同時,很多對象鎖的鎖定狀態(tài)指會持續(xù)很短的時間,因此引入了自旋鎖,所謂自旋就是一個無意義的死循環(huán),在循環(huán)體內(nèi)不斷的重行競爭鎖。當(dāng)然,自旋的次數(shù)會有限制,超出指定的限制會升級到阻塞鎖。偏向鎖的撤銷
當(dāng)其他線程嘗試競爭偏向鎖時,持有偏向鎖的線程才會釋放偏向鎖,撤銷偏向鎖的過程需要等待一個全局安全點(所有工作線程都停止字節(jié)碼的執(zhí)行)。
首先,暫停擁有偏向鎖的線程,然后檢查偏向鎖的線程是否為存活狀態(tài)
如果線程已經(jīng)死了,直接把對象頭設(shè)置為無鎖狀態(tài)
如果還活著,當(dāng)達到全局安全點時獲得偏向鎖的線程會被掛起,接著偏向鎖升級為輕量級鎖,然后喚醒被阻塞在全局安全點的線程繼續(xù)往下執(zhí)行同步代碼
偏向鎖的獲取流程圖 輕量級鎖前面我們知道,當(dāng)存在超過一個線程在競爭同一個同步代碼塊時,會發(fā)生偏向鎖的撤銷。偏向鎖撤銷以后對象會可能會處于兩種狀態(tài)
一種是不可偏向的無鎖狀態(tài),簡單來說就是已經(jīng)獲得偏向鎖的線程已經(jīng)退出了同步代碼塊,那么這個時候會撤銷偏向鎖,并升級為輕量級鎖
一種是不可偏向的已鎖狀態(tài),簡單來說就是已經(jīng)獲得偏向鎖的線程正在執(zhí)行同步代碼塊,那么這個時候會升級到輕量級鎖并且被原持有鎖的線程獲得鎖
那么升級到輕量級鎖以后的加鎖過程和解鎖過程是怎么樣的呢?
輕量級鎖加鎖JVM會先在當(dāng)前線程的棧幀中創(chuàng)建用于存儲鎖記錄的空間(LockRecord)
將對象頭中的Mark Word復(fù)制到鎖記錄中,稱為Displaced Mark Word.
線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針
如果替換成功,表示當(dāng)前線程獲得輕量級鎖,如果失敗,表示存在其他線程競爭鎖,那么當(dāng)前線程會嘗試使用CAS來獲取鎖,當(dāng)自旋超過指定次數(shù)(可以自定義)時仍然無法獲得鎖,此時鎖會膨脹升級為重量級鎖
輕量鎖解鎖嘗試CAS操作將所記錄中的Mark Word替換回到對象頭中
如果成功,表示沒有競爭發(fā)生
如果失敗,表示當(dāng)前鎖存在競爭,鎖會膨脹成重量級鎖
一旦鎖升級成重量級鎖,就不會再恢復(fù)到輕量級鎖狀態(tài)。當(dāng)鎖處于重量級鎖狀態(tài),其他線程嘗試獲取鎖時,都會被阻塞,也就是 BLOCKED狀態(tài)。當(dāng)持有鎖的線程釋放鎖之后會喚醒這些現(xiàn)場,被喚醒之后的線程會進行新一輪的競爭重量級鎖
重量級鎖依賴對象內(nèi)部的monitor鎖來實現(xiàn),而monitor又依賴操作系統(tǒng)的MutexLock(互斥鎖)
大家如果對MutexLock有興趣,可以抽時間去了解,假設(shè)Mutex變量的值為1,表示互斥鎖空閑,這個時候某個線程調(diào)用lock可以獲得鎖,而Mutex的值為0表示互斥鎖已經(jīng)被其他線程獲得,其他線程調(diào)用lock只能掛起等待
為什么重量級鎖的開銷比較大呢?
原因是當(dāng)系統(tǒng)檢查到是重量級鎖之后,會把等待想要獲取鎖的線程阻塞,被阻塞的線程不會消耗CPU,但是阻塞或者喚醒一個線程,都需要通過操作系統(tǒng)來實現(xiàn),也就是相當(dāng)于從用戶態(tài)轉(zhuǎn)化到內(nèi)核態(tài),而轉(zhuǎn)化狀態(tài)是需要消耗時間的
總結(jié)到目前為止,我們分析了synchronized的使用方法、以及鎖的存儲、對象頭、鎖升級的原理。如果有問題,可以掃描二維碼留言
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/72574.html
摘要:原文地址游客前言金三銀四,很多同學(xué)心里大概都準(zhǔn)備著年后找工作或者跳槽。最近有很多同學(xué)都在交流群里求大廠面試題。 最近整理了一波面試題,包括安卓JAVA方面的,目前大廠還是以安卓源碼,算法,以及數(shù)據(jù)結(jié)構(gòu)為主,有一些中小型公司也會問到混合開發(fā)的知識,至于我為什么傾向于混合開發(fā),我的一句話就是走上編程之路,將來你要學(xué)不僅僅是這些,豐富自己方能與世接軌,做好全棧的裝備。 原文地址:游客kutd...
摘要:結(jié)構(gòu)型模式適配器模式橋接模式裝飾模式組合模式外觀模式享元模式代理模式。行為型模式模版方法模式命令模式迭代器模式觀察者模式中介者模式備忘錄模式解釋器模式模式狀態(tài)模式策略模式職責(zé)鏈模式責(zé)任鏈模式訪問者模式。 主要版本 更新時間 備注 v1.0 2015-08-01 首次發(fā)布 v1.1 2018-03-12 增加新技術(shù)知識、完善知識體系 v2.0 2019-02-19 結(jié)構(gòu)...
摘要:為了避免一篇文章的篇幅過長,于是一些比較大的主題就都分成幾篇來講了,這篇文章是筆者所有文章的目錄,將會持續(xù)更新,以給大家一個查看系列文章的入口。 前言 大家好,筆者是今年才開始寫博客的,寫作的初衷主要是想記錄和分享自己的學(xué)習(xí)經(jīng)歷。因為寫作的時候發(fā)現(xiàn),為了弄懂一個知識,不得不先去了解另外一些知識,這樣以來,為了說明一個問題,就要把一系列知識都了解一遍,寫出來的文章就特別長。 為了避免一篇...
閱讀 720·2023-04-25 20:32
閱讀 2267·2021-11-24 10:27
閱讀 4520·2021-09-29 09:47
閱讀 2241·2021-09-28 09:36
閱讀 3633·2021-09-22 15:27
閱讀 2756·2019-08-30 15:54
閱讀 370·2019-08-30 11:06
閱讀 1271·2019-08-30 10:58