<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          深入理解Synchronized

          共 6592字,需瀏覽 14分鐘

           ·

          2022-04-26 14:04

          前言

          Synchronized想必大家在工作中一定有接觸過,它算是Java并發(fā)場景下實(shí)現(xiàn)多線程安全一種比較直接的操作。有人會說它慢,確實(shí)。在JDK1.6之前,它有另一個名稱叫做:重量級鎖。但是從1.6版本起,它就在不斷被優(yōu)化。現(xiàn)如今已經(jīng)是很成熟的并發(fā)安全技術(shù);所以關(guān)于Synchronized的考察也常常成為面試官青睞的話題。

          本文我們會使用圖解的方式解析Synchronized的使用和原理,讓我們開始吧~

          對象鎖和類鎖

          什么是Synchronized?Synchronized是Java中的一個關(guān)鍵字,中文被稱為“同步鎖”。顧名思義,它是一種鎖,當(dāng)某一時(shí)刻有多個線程對同一段程序進(jìn)行操作時(shí),能夠保證只有一個線程能夠獲取到資源,因此保證了線程安全。

          Synchronized主要有三種使用方式:

          • 修飾普通方法,鎖作用于當(dāng)前對象實(shí)例。

          • 修飾靜態(tài)方法,鎖作用于類的Class實(shí)例。

          • 修飾代碼塊,作用于當(dāng)前對象實(shí)例,需要指定加鎖對象。

          普通方法

          Synchronized是一個關(guān)鍵字,當(dāng)作用于一個普通方法的時(shí)候,這個方法便被加上了同步鎖,意味著某一時(shí)刻只有一個線程可以操作訪問這個方法:

          public class fancySyncTest {
          public synchronized void method1(){
          try {
          System.out.println(Thread.currentThread().getName());
          Thread.sleep(5000);
          } catch (InterruptedException e) {
          e.printStackTrace();
          }
          }

          public static void main(String[] agrs) throws InterruptedException {
          final fancySyncTest fs = new fancySyncTest();

          Thread t1 = new Thread(new Runnable() {
          @Override
          public void run() {
          fs.method1();
          }
          },"線程1獲取到資源");

          Thread t2 = new Thread(new Runnable() {
          @Override
          public void run() {
          fs.method1();
          }
          },"線程2獲取到資源");

          t1.start();
          t2.start();
          }
          }

          這段代碼有方法method1( ),它的功能就是打印出當(dāng)前執(zhí)行這段代碼的線程,并且讓它休眠5秒鐘。然后我們開啟兩個線程,線程t1和t2,它們兩個一起啟動。

          這段代碼的執(zhí)行順序很簡單,線程1或者線程2任意一個線程先去執(zhí)行method1( )的內(nèi)容,然后休眠5秒鐘。完事就釋放鎖,下一個線程會繼續(xù)執(zhí)行:


          整個過程由于只有一個線程獲取到這個方法,去執(zhí)行方法里的內(nèi)容,所以method1( )里的代碼資源是線程安全的。

          于是,當(dāng)前哪個對象調(diào)用了這個方法,那么當(dāng)前這段線程在執(zhí)行的時(shí)候就讓這個對象去訪問這個方法。比如我是讓對象fs去調(diào)用method1( ),那么這把鎖就作用于當(dāng)前的fs對象實(shí)例:

          靜態(tài)方法

          靜態(tài)方法和普通方法的區(qū)別只有一個,就是Synchronized關(guān)鍵字是作用于靜態(tài)方法的。但是僅僅這個區(qū)別,代表著鎖的對象也是不同的。原因在于Java的靜態(tài)關(guān)鍵字它和實(shí)例對象沒有任何關(guān)系,它作用的資源是在類的初始化時(shí)期就加載的,所以它只能由每個類唯一的Class對象調(diào)用。當(dāng)它作用于一個Class對象時(shí),它就會將這一整個類都鎖住,簡稱"類鎖":

          public class fancySyncTest {
          public static synchronized void method1(){
          try {
          System.out.println(Thread.currentThread().getName());
          Thread.sleep(5000);
          } catch (InterruptedException e) {
          e.printStackTrace();
          }
          }

          public static void main(String[] agrs) throws InterruptedException {
          Thread t1 = new Thread(new Runnable() {
          @Override
          public void run() {
          fancySyncTest.method1();
          }
          },"線程1獲取到資源");

          Thread t2 = new Thread(new Runnable() {
          @Override
          public void run() {
          fancySyncTest.method1();
          }
          },"線程2獲取到資源");

          t1.start();
          t2.start();
          }
          }

          可以看出,這段由靜態(tài)關(guān)鍵字修飾的代碼和普通的方法沒什么區(qū)別,唯一的區(qū)別就在于:由于method1( )是靜態(tài)的,所有我們不用創(chuàng)建對象,可以直接由類實(shí)例fancySynTest直接調(diào)用!作為代價(jià),這把鎖鎖住的對象也是直接覆蓋了這個類。也就是說,當(dāng)線程1執(zhí)行的時(shí)候,沒有別的線程可以訪問這個fancySyncTest類:


          Synchronized修飾普通方法和靜態(tài)方法的區(qū)別只有一個:粒度不同。類似于數(shù)據(jù)庫中表鎖和行鎖的區(qū)別。

          代碼塊

          Synchronized可以鎖住普通方法,也可以鎖住一個類,那么它鎖的粒度能否更小呢?是的,它還能鎖住一段簡易的代碼塊。那么Synchronized如何定義一段代碼塊呢?其實(shí)定義一下作用的對象,然后將代碼用括號{ }包裹起來就可以了:

          public class fancySyncTest {
          public synchronized void method1(){
          synchronized (this) {
          // 邏輯代碼
          }
          }
          }

          代碼塊鎖住的對象就是后面括號里的東西。比如這里的synchronized (this),意味著只有當(dāng)前對象才可以訪問這段代碼塊,你也可以定義為其它對象。

          Synchronized原理

          其實(shí),Synchronized只是Java中的一個關(guān)鍵字,那么它底層是如何真正意義地實(shí)現(xiàn)鎖呢?答案就是monitor監(jiān)視器鎖。無論是synchronized代碼塊還是synchronized方法,其線程安全的語義實(shí)現(xiàn)最終依賴的都是monitor,它才是真正意義上的鎖。

          為了得到Synchronized的底層代碼,我們先寫一段簡單demo:

          public class fancySynchronizedTest {
          public synchronized void method1() {

          }
          public void method2() {
          synchronized (this){

          }
          }

          public static void main(String[] args) {
          fancySynchronizedTest test = new fancySynchronizedTest();
          test.method1();
          test.method2();
          }
          }

          然后對它使用java-c 反編譯,得到以下文件:

          public class fancySynchronizedTest {
          public fancySynchronizedTest();
          Code:
          0: aload_0
          1: invokespecial #1 // Method java/lang/Object."":()V
          4: return

          public synchronized void test1();
          Code:
          0: return

          public void test2();
          Code:
          0: aload_0
          1: dup
          2: astore_1
          3: monitorenter //monitor加鎖
          4: aload_1
          5: monitorexit //monitor解鎖
          6: goto 14
          9: astore_2
          10: aload_1
          11: monitorexit //monitor異步退出解鎖
          12: aload_2
          13: athrow
          14: return
          Exception table:
          from to target type
          4 6 9 any
          9 12 9 any

          public static void main(java.lang.String[]);
          Code:
          0: new #2 // class SynchronizedTest
          3: dup
          4: invokespecial #3 // Method "":()V
          7: astore_1
          8: aload_1
          9: invokevirtual #4 // Method test1:()V
          12: aload_1
          13: invokevirtual #5 // Method test2:()V
          16: return
          }

          可以看到,test2( )在加上了synchronized同步代碼塊后,會輸出以下指令:monitorentermonitorexit

          monitorenter存在于同步代碼塊開始的位置,而monitorexit存在于同步代碼塊結(jié)束的位置,它們分別代表著獲取鎖和釋放鎖。每一個monitorenter都必須對應(yīng)一個monitorexit。并且,每一個對象在其堆內(nèi)存的數(shù)據(jù)結(jié)構(gòu)中,它的對象頭都會關(guān)聯(lián)一個完整的monitor結(jié)構(gòu)。

          為了讓大家更熟悉monitor,我們帶大家來閱讀一下monitor的源碼。

          對于我們通常使用的HotSpot虛擬機(jī),monitor是由ObjectMonitor實(shí)現(xiàn)的。其源碼是用c++來實(shí)現(xiàn)的,位于HotSpot虛擬機(jī)源碼ObjectMonitor.hpp文件中,我們可以在社區(qū)版本的JDK上閱讀到,因?yàn)樗情_源的:

          注:該源碼版本為jdk8u的社區(qū)版open jdk

          我們需要關(guān)注該圖上的三個變量:_owner、_WaitSet、_EntryList ,因?yàn)閙onitor就是通過它們,來實(shí)現(xiàn)上鎖與釋放鎖的。我們先來看看這三個變量的定義:

          _EntryList:

          它的定義為:阻塞在路口處的線程的集合

          再來看看_WaitSet:

          它的定義為:處于等待監(jiān)視器的線程的集合

          最后看看變量_owner:

          它的意思是指向持有鎖的線程。

          所以,它的完整過程就是如下所示:


          monitor自身會設(shè)置一個變量count來作為計(jì)數(shù)器維護(hù)這把可重入鎖,當(dāng)沒有線程獲取這把鎖的時(shí)候,它的值為0,如果有一個線程獲取這把鎖,它的值就會+1,并且設(shè)置該線程為鎖的持有者。_owner指向的就是當(dāng)前持鎖線程。如果該線程已經(jīng)占用該鎖,并且重新進(jìn)入,那么count的值就會+1。當(dāng)執(zhí)行到monitorexit的時(shí)候,count的值就會-1,直到monitor值為0的時(shí)候,該持鎖線程會進(jìn)入到WaitSet里面,將狀態(tài)改為等待狀態(tài),讓其他處于EntryList里的阻塞線程重新自旋獲取這把鎖。

          以上就是完整的monitor獲取鎖和釋放鎖的過程。不知道你是否會覺得它和AQS的原理很像?其實(shí)這些底層架構(gòu)設(shè)計(jì)的思想都是相通的。

          鎖的優(yōu)化

          可能是意識到Synchronized作為重量級鎖性能上的不足,從jdk1.6開始Java團(tuán)隊(duì)就對它進(jìn)行了優(yōu)化。通過各種各樣的手段,如自旋鎖、偏向鎖、輕量級鎖等技術(shù)來減少鎖的開銷,那么我們就來解釋一下這些鎖操作以及升級過程。

          自旋鎖

          首先聊聊自旋鎖,自旋鎖顧名思義就是自旋。當(dāng)一個線程在獲取鎖的時(shí)候,如果該鎖已被其它線程獲取到,那么該線程就會去循環(huán)自旋獲取鎖,不停地判斷該鎖是否能夠已經(jīng)被釋放,自選直到獲取到鎖才會退出循環(huán)。通常該自選在源碼中都是通過for(; ;)或者while(true)這樣的操作實(shí)現(xiàn),非常粗暴。

          那么都說是自旋了,自旋就代表著占用cpu資源,使用自旋鎖的目的是為了避免線程在阻塞和喚醒狀態(tài)之間切換而占用資源,畢竟線程從阻塞狀態(tài)切換到喚醒狀態(tài)需要CPU從用戶態(tài)轉(zhuǎn)化為內(nèi)核態(tài),而頻繁的狀態(tài)切換就會導(dǎo)致CPU的資源浪費(fèi),所以引入了自選鎖。但是自旋鎖也必定要設(shè)置一個自旋的閾值,否則因?yàn)樽孕L期占用CPU核心數(shù),也是一種資源的浪費(fèi)。在JDK1.6中默認(rèn)的自旋次數(shù)為10次,也就是說如果一個線程自旋超過10次,那么它就會自動進(jìn)入掛起狀態(tài)從而節(jié)約資源。以下為自旋鎖獲取鎖過程:


          自旋鎖在JDK源碼中有大量的應(yīng)用,之后我們還會接觸到。

          鎖消除

          鎖消除指的是虛擬機(jī)即時(shí)編譯器在運(yùn)行的時(shí),對一些代碼上要求同步,但是被檢測到不可能存在共享數(shù)據(jù)競爭的鎖進(jìn)行削除。那么如何確定這個鎖是否需要進(jìn)行削除?主要來源于逃逸分析的判斷,如果判斷到一段代碼中,在堆上的所有數(shù)據(jù)都不會逃逸出去被其他線程訪問到,那就可以把它們當(dāng)作棧上數(shù)據(jù)對待,認(rèn)為它們是線程私有的,同步加鎖自然就無須進(jìn)行。舉個例子:

              public void method() {
          synchronized (new Object()) {
          //代碼邏輯

          }
          }

          這段代碼里面,我們new了一個Object對象來作為鎖對象,但是這個對象也只有在method( )中被使用,其完整的生命周期都在這個方法中,也就是說JVM在經(jīng)過逃逸分析后會對它進(jìn)行棧上分配,由于在底層變成了私有數(shù)據(jù),那么也就無需加鎖了。其被優(yōu)化后的代碼會變成:

              public void method() {
          //代碼邏輯
          }

          這就是鎖的消除,JVM虛擬機(jī)在判斷一段代碼沒必要加鎖后,會消除該鎖的存在。

          那么我們再來看看什么是鎖的粗化。

          鎖粗化

          鎖粗化指的是在JIT編譯時(shí),發(fā)現(xiàn)如果有一段代碼中頻繁的加鎖釋放鎖,會將前后的鎖合并為一個鎖,避免頻繁加鎖釋放鎖。舉個例子:

              public void method() {
          for(int i = 0;i < 100; i++) {
          synchronized (new Object()) {
          //代碼邏輯

          }
          }
          }

          大伙說,如果按照正常的synchronized步驟走,這個循環(huán)需要進(jìn)行多少次的加鎖解鎖操作,這性能可想而知!

          leader看了你這段代碼估計(jì)想罵娘。。。

          好在現(xiàn)在的JVM已經(jīng)優(yōu)化的足夠好了。當(dāng)你這段代碼在即時(shí)編譯時(shí),JVM檢測到你每一次都是對同一個對象加鎖,那么就會把這一串連續(xù)頻繁的加鎖解鎖操作優(yōu)化成僅僅一次公共的加鎖解鎖操作,這樣性能就提升了很多了:

              public void method() {
          synchronized (new Object()) {
          for(int i = 0;i < 100; i++) {
          //代碼邏輯

          }
          }
          }

          所以,所謂的鎖粗化,就是通過增加鎖的范圍,減少鎖的數(shù)量來減少開銷。

          偏向鎖

          在提偏向鎖之前,我們需要先提一下一個Java對象在內(nèi)存中是如何存儲的。

          一個對象在堆內(nèi)存中由三部分區(qū)域組成,分別為:對象頭(Header)、實(shí)例數(shù)據(jù)(Instance Data)和對其填充(Padding):


          其中對象頭包括了兩部分,一部分是關(guān)于堆對象的布局、類型、GC狀態(tài)、同步狀態(tài)和標(biāo)識哈希碼的基本信息,它被稱為"Mark Word "。

          另一部分是類型指針,是對象指向它的類元數(shù)據(jù)的指針,虛擬機(jī)通過這個指針來確定這個對象是哪個類的實(shí)例。


          那么我們今天的重點(diǎn)在于Mark Word,其結(jié)構(gòu)如下:


          好了,在知道了對象頭的重要結(jié)構(gòu)Mark Word,我們就繼續(xù)聊聊偏向鎖。

          偏向鎖是JVM認(rèn)為沒有發(fā)生并發(fā)的場景下提供的鎖。它是JDK 1.6中的重要引進(jìn),因?yàn)镠otSpot團(tuán)隊(duì)發(fā)現(xiàn)在大多數(shù)情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價(jià)更低,引進(jìn)了偏向鎖。

          所以這就是它為什么叫“偏向鎖”。“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是這個鎖會偏向于第一個獲得它的線程,會在對象頭存儲鎖偏向的線程ID,以后該線程進(jìn)入和退出同步區(qū)域時(shí)只需要檢查是否為偏向鎖,即鎖標(biāo)志位以及ThreadID即可:

          輕量級鎖

          輕量級鎖是JDK 1.6之中加入的新型鎖機(jī)制,它名字中的“輕量級”是相對于使用monitor的傳統(tǒng)鎖而言的,因此傳統(tǒng)的鎖機(jī)制就稱為“重量級”鎖。
          首先需要強(qiáng)調(diào)一點(diǎn)的是,輕量級鎖并不是用來代替重量級鎖的。引入輕量級鎖的目的在于:在多線程交替執(zhí)行同步塊的情況下,盡量避免重量級鎖引起的性能消耗,但是如果多個線程在同一時(shí)刻進(jìn)入臨界區(qū),會導(dǎo)致輕量級鎖膨脹升級重量級鎖,所以輕量級鎖的出現(xiàn)并非是要替代重量級鎖。當(dāng)該鎖為輕量級鎖時(shí),其Mark Word的狀態(tài)變化如下:

          重量級鎖

          在JDK1.6之前,Synchronized就是一把“重量級鎖”。它需要依賴操作系統(tǒng)級別的mutex和condition variable來實(shí)現(xiàn)。重量級鎖會讓搶占鎖的線程從用戶態(tài)轉(zhuǎn)變?yōu)閮?nèi)核態(tài),所以開銷很大:

          鎖升級

          鎖升級,顧名思義就是鎖的等級不斷上升。因?yàn)殒i是會消耗性能的,所以鎖不斷升級,它的性能就會越差。當(dāng)然這一切都是為了滿足安全、復(fù)雜的業(yè)務(wù)場景。

          并且鎖只會升級,不會降級:

          總結(jié)

          本文我們詳細(xì)地介紹了Synchronized的三種使用方式、它作為鎖的特性、詳細(xì)的原理以及七種鎖的優(yōu)化過程。Synchronized作為虛擬機(jī)級別的鎖,無論是業(yè)務(wù)的使用還是面試,都是經(jīng)常被照顧的對象,所以搞懂它就很重要。下一次我們會講一講它異父異母的親兄弟:JDK級別的鎖Lock,以及談?wù)勊鼈兪侨绾谓?jīng)常拿出來被比較的~



          最近熬夜給大家準(zhǔn)備了非常全的一套Java一線大廠面試題。全面覆蓋BATJ等一線互聯(lián)網(wǎng)公司的面試題及解答,由BAT一線互聯(lián)網(wǎng)公司大牛帶你深度剖析面試題背后的原理,不僅授你以魚,更授你以漁,為你面試掃除一切障礙。



          資源,怎么領(lǐng)取?


          掃二維碼,加我微信,備注:面試題


          一定要備注:面試題,不要急哦,工作忙完后就會通過!



          瀏覽 65
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評論
          圖片
          表情
          推薦
          點(diǎn)贊
          評論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  免费无码黄十八禁 | 色婷婷亚洲AV无码精品 | 无码欧美成人18XXXXX日本 | 好看一区二区三区 | 淫色成人视频 |