再有人問你synchronized是什麼,就把這篇文章發給他.

2021-02-20 Hollis

在再有人問你Java內存模型是什麼,就把這篇文章發給他中我們曾經介紹過,Java語言為了解決並發編程中存在的原子性、可見性和有序性問題,提供了一系列和並發處理相關的關鍵字,比如synchronized、volatile、final、concurren包等。

在《深入理解Java虛擬機》中,有這樣一段話:

synchronized關鍵字在需要原子性、可見性和有序性這三種特性的時候都可以作為其中一種解決方案,看起來是「萬能」的。的確,大部分並發控制操作都能使用synchronized來完成。

海明威在他的《午後之死》說過的:「冰山運動之雄偉壯觀,是因為他只有八分之一在水面上。」

對於程式設計師來說,synchronized只是個關鍵字而已,用起來很簡單。之所以我們可以在處理多線程問題時可以不用考慮太多,就是因為這個關鍵字幫我們屏蔽了很多細節。

那麼,本文就圍繞synchronized展開,主要介紹其用法、原理,以及如何提供原子性、可見性和有序性保障的等。

synchronized是Java提供的一個並發控制的關鍵字。主要有兩種用法,分別是同步方法和同步代碼塊。

也就是說,synchronized既可以修飾方法也可以修飾代碼塊。代碼如下:


public class SynchronizedDemo {
     
    public synchronized void doSth(){
        System.out.println("Hello World");
    }

    
    public void doSth1(){
        synchronized (SynchronizedDemo.class){
            System.out.println("Hello World");
        }
    }
}

被synchronized修飾的代碼塊及方法,在同一時間,只能被單個線程訪問。

synchronized,是Java中用於解決並發情況下數據同步訪問的一個很重要的關鍵字。當我們想要保證一個共享資源在同一時間只會被一個線程訪問到時,我們可以在代碼中使用synchronized關鍵字對類或者對象加鎖。

在深入理解多線程(一)——Synchronized的實現原理中我曾經介紹過其實現原理,為了保證知識的完整性,這裡再簡單介紹一下,詳細的內容請去原文閱讀。

我們對上面的代碼進行反編譯,可以得到如下代碼:

public synchronized void doSth();
    descriptor: ()V
    flags: ACC_PUBLIC, ACC_SYNCHRONIZED
    Code:
      stack=2, locals=1, args_size=1
         0: getstatic     #2                  
         3: ldc           #3                  
         5: invokevirtual #4                  
         8: return

  public void doSth1();
    descriptor: ()V
    flags: ACC_PUBLIC
    Code:
      stack=2, locals=3, args_size=1
         0: ldc           #5                  
         2: dup
         3: astore_1
         4: monitorenter
         5: getstatic     #2                  
         8: ldc           #3                  
        10: invokevirtual #4                  
        13: aload_1
        14: monitorexit
        15: goto          23
        18: astore_2
        19: aload_1
        20: monitorexit
        21: aload_2
        22: athrow
        23: return

通過反編譯後代碼可以看出:

對於同步方法,JVM採用ACC_SYNCHRONIZED標記符來實現同步。 

對於同步代碼塊。JVM採用monitorenter、monitorexit兩個指令來實現同步。

在The Java® Virtual Machine Specification中有關於同步方法和同步代碼塊的實現原理的介紹,我翻譯成中文如下:

方法級的同步是隱式的。同步方法的常量池中會有一個ACC_SYNCHRONIZED標誌。當某個線程要訪問某個方法的時候,會檢查是否有ACC_SYNCHRONIZED,如果有設置,則需要先獲得監視器鎖,然後開始執行方法,方法執行之後再釋放監視器鎖。這時如果其他線程來請求執行方法,會因為無法獲得監視器鎖而被阻斷住。值得注意的是,如果在方法執行過程中,發生了異常,並且方法內部並沒有處理該異常,那麼在異常被拋到方法外面之前監視器鎖會被自動釋放。

同步代碼塊使用monitorenter和monitorexit兩個指令實現。可以把執行monitorenter指令理解為加鎖,執行monitorexit理解為釋放鎖。 每個對象維護著一個記錄著被鎖次數的計數器。未被鎖定的對象的該計數器為0,當一個線程獲得鎖(執行monitorenter)後,該計數器自增變為 1 ,當同一個線程再次獲得該對象的鎖的時候,計數器再次自增。當同一個線程釋放鎖(執行monitorexit指令)的時候,計數器再自減。當計數器為0的時候。鎖將被釋放,其他線程便可以獲得鎖。

無論是ACC_SYNCHRONIZED還是monitorenter、monitorexit都是基於Monitor實現的,在Java虛擬機(HotSpot)中,Monitor是基於C++實現的,由ObjectMonitor實現。

ObjectMonitor類中提供了幾個方法,如enter、exit、wait、notify、notifyAll等。sychronized加鎖的時候,會調用objectMonitor的enter方法,解鎖的時候會調用exit方法。(關於Monitor詳見深入理解多線程(四)—— Moniter的實現原理)

原子性是指一個操作是不可中斷的,要全部執行完成,要不就都不執行。

我們在Java的並發編程中的多線程問題到底是怎麼回事兒中分析過:

線程是CPU調度的基本單位。CPU有時間片的概念,會根據不同的調度算法進行線程調度。當一個線程獲得時間片之後開始執行,在時間片耗盡之後,就會失去CPU使用權。所以在多線程場景下,由於時間片在線程間輪換,就會發生原子性問題。

在Java中,為了保證原子性,提供了兩個高級的字節碼指令monitorenter和monitorexit。

前面介紹過,這兩個字節碼指令,在Java中對應的關鍵字就是synchronized。

通過monitorenter和monitorexit指令,可以保證被synchronized修飾的代碼在同一時間只能被一個線程訪問,在鎖未釋放之前,無法被其他線程訪問到。

因此,在Java中可以使用synchronized來保證方法和代碼塊內的操作是原子性的。

線程1在執行monitorenter指令的時候,會對Monitor進行加鎖,加鎖後其他線程無法獲得鎖,除非線程1主動解鎖。即使在執行過程中,由於某種原因,比如CPU時間片用完,線程1放棄了CPU,但是,他並沒有進行解鎖。而由於synchronized的鎖是可重入的,下一個時間片還是只能被他自己獲取到,還是會繼續執行代碼。直到所有代碼執行完。這就保證了原子性。

可見性是指當多個線程訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。

我們在再有人問你Java內存模型是什麼,就把這篇文章發給他中分析過:

Java內存模型規定了所有的變量都存儲在主內存中,每條線程還有自己的工作內存,線程的工作內存中保存了該線程中是用到的變量的主內存副本拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量的傳遞均需要自己的工作內存和主存之間進行數據同步進行。所以,就可能出現線程1改了某個變量的值,但是線程2不可見的情況。

前面我們介紹過,被synchronized修飾的代碼,在開始執行時會加鎖,執行完成後會進行解鎖。

而為了保證可見性,有一條規則是這樣的:對一個變量解鎖之前,必須先把此變量同步回主存中。這樣解鎖後,後續線程就可以訪問到被修改後的值。

所以,synchronized關鍵字鎖住的對象,其值是具有可見性的。

我們在再有人問你Java內存模型是什麼,就把這篇文章發給他中分析過:

除了引入了時間片以外,由於處理器優化和指令重排等,CPU還可能對輸入代碼進行亂序執行,比如load->add->save 有可能被優化成load->save->add 。這就是可能存在有序性問題。

這裡需要注意的是,synchronized是無法禁止指令重排和處理器優化的。也就是說,synchronized無法避免上述提到的問題。

那麼,為什麼還說synchronized也提供了有序性保證呢?

這就要再把有序性的概念擴展一下了。

Java程序中天然的有序性可以總結為一句話:如果在本線程內觀察,所有操作都是天然有序的。如果在一個線程中觀察另一個線程,所有操作都是無序的。

以上這句話也是《深入理解Java虛擬機》中的原句,但是怎麼理解呢?周志明並沒有詳細的解釋。這裡我簡單擴展一下,這其實和as-if-serial語義有關。

as-if-serial語義的意思指:不管怎麼重排序(編譯器和處理器為了提高並行度),單線程程序的執行結果都不能被改變。編譯器和處理器無論如何優化,都必須遵守as-if-serial語義。

這裡不對as-if-serial語義詳細展開了,簡單說就是,as-if-serial語義保證了單線程中,指令重排是有一定的限制的,而只要編譯器和處理器都遵守了這個語義,那麼就可以認為單線程程序是按照順序執行的。當然,實際上還是有重排的,只不過我們無須關心這種重排的幹擾。

所以呢,由於synchronized修飾的代碼,同一時間只能被同一線程訪問。那麼也就是單線程執行的。所以,可以保證其有序性。

前面介紹了synchronized的用法、原理以及對並發編程的作用。是一個很好用的關鍵字。

synchronized其實是藉助Monitor實現的,在加鎖時會調用objectMonitor的enter方法,解鎖的時候會調用exit方法。事實上,只有在JDK1.6之前,synchronized的實現才會直接調用ObjectMonitor的enter和exit,這種鎖被稱之為重量級鎖。

所以,在JDK1.6中出現對鎖進行了很多的優化,進而出現輕量級鎖,偏向鎖,鎖消除,適應性自旋鎖,鎖粗化(自旋鎖在1.4就有,只不過默認的是關閉的,jdk1.6是默認開啟的),這些操作都是為了在線程之間更高效的共享數據 ,解決競爭問題。

關於自旋鎖、鎖粗化和鎖消除可以參考深入理解多線程(五)—— Java虛擬機的鎖優化技術,關於輕量級鎖和偏向鎖,已經在排期規劃中,我後面會有文章單獨介紹,將獨家發布在我的博客(http://www.hollischuang.com)和公眾號(Hollis)中,敬請期待。

好啦,關於synchronized關鍵字,我們介紹了其用法、原理、以及如何保證的原子性、順序性和可見性,同時也擴展的留下了鎖優化相關的資料及思考。後面我們會繼續介紹volatile關鍵字以及他和synchronized的區別等。敬請期待。

相關焦點

  • 問懵逼:談談 synchronized 及鎖升級是怎麼回事?
    本篇是並發編程中的第一篇,為什麼說是第一篇呢,因為並發編程涉及的東西太多太多,晦澀難懂,隨便一個知識點拉出來都可以寫一篇文章,如此算來寫完並發編程一個系列最起碼要十篇。我將知識點進行了總結歸納,排類分類,用通俗易懂的方式來跟大家說清楚、講明白。
  • 再有人問你Pokemon go怎麼玩,就把這篇文章糊他臉上!
    圖鑑(Pokédex):收錄了所有你抓到的或者遇到的精靈信息。精靈蛋(Eggs):可以在補給站刷出精靈蛋,一旦你把精靈蛋放入孵蛋器開始孵蛋,並且走了對應的公裡數,蛋將會孵化出精靈。補充:目前已經有人從2km的蛋中孵出了皮卡丘…此外10km的蛋中可以出化石寵,真是要了老命。。。戰鬥威力 Combat Power (CP):通過CP來衡量一隻精靈的戰鬥力,精靈的CP決定了他在戰鬥中的表現。
  • 大話Synchronized及鎖升級
    synchronized 可以確保可見性,根據happens-before規定,在一個線程執行完 synchronized 代碼後,所有代碼中對變量值的變化都能立即被其它線程所看到。順序性的話就是禁止指令重排,代碼塊中的代碼從上往下依次執行,歸根到底再一句話,並發問題中的三個特性synchronized都能保證,也就是synchronized是萬金油,用他準沒錯!
  • 【132期】面試再被問到Spring容器IOC初始化過程,就拿這篇文章砸他~
    1、閱讀源碼分析是非常無聊的,但既然你進來了,肯定也是對這個東西進行了解,也希望這篇總結能對你有所啟發。2、前方高能,文章可能會非常的長,圖文並茂。3、閱讀前建議你對相關設計模式、軟體設計 6 大原則有所了解,小編會在行文中進行穿插。4、小編在讀大四,學識尚淺,喜歡專研,如果你發現文章觀點有所錯誤或者與你見解有差異,歡迎評論區指出和交流!
  • 手帳有術丨「手帳是什麼?」「看看這篇文章就知道了!」
    經過市場的消化和手帳er各種主觀能動的使用以後,出現了更有趣更豐富的形式,也變成了一種生活方式。我們可以用它進行時間管理,更可以發揮想像力的去創造。)(▲註:建議點開大圖)關於筆的詳細介紹,可以看一看這一篇:好物推薦丨手帳坑之I have a pen關於國產本子測評的可以看看這篇
  • 面試再被問到 ConcurrentHashMap,把這篇文章甩給他!
    但是我們也不要忘記《算法導論》給我們的教訓:hash槽的的個數不應該是 2^n,這可能導致hash槽分配不均,這需要對hash值重新再hash一次。,哈希衝突會非常嚴重,因為只要低位一樣,無論高位是什麼數,其哈希值總是一樣。
  • 死磕Synchronized底層實現
    但是很多文章要麼作者根本沒看代碼,僅僅是根據網上其他文章總結、照搬而成,難免有些錯誤;要麼很多點都是一筆帶過,對於為什麼這樣實現沒有一個說法,讓像我這樣的讀者意猶未盡。本系列文章將對HotSpot的synchronized鎖實現進行全面分析,內容包括偏向鎖、輕量級鎖、重量級鎖的加鎖、解鎖、鎖升級流程的原理及源碼分析,希望給在研究synchronized路上的同學一些幫助。
  • 17 張圖帶你了解 synchronized 關鍵字
    在 store() 方法執行之前,先觀察這個鎖的狀態,如果是上鎖狀態,就進入阻塞,代碼不運行;如果這把鎖是解鎖狀態,那就先將這把鎖狀態變為上鎖,之後接著運行自己的代碼。運行完成之後再將鎖狀態設置為解鎖。對於 get() 方法也是如此。
  • 面試專題 | 其實你一直不了解 Synchronized 原理
    但是很多文章要麼作者根本沒看代碼,僅僅是根據網上其他文章總結、照搬而成,難免有些錯誤;要麼很多點都是一筆帶過,對於為什麼這樣實現沒有一個說法,讓像我這樣的讀者意猶未盡。本系列文章將對HotSpot的synchronized鎖實現進行全面分析,內容包括偏向鎖、輕量級鎖、重量級鎖的加鎖、解鎖、鎖升級流程的原理及源碼分析,希望給在研究synchronized路上的同學一些幫助。
  • 看完這篇文章,你打開了瀏覽器……
    網際網路是人類最偉大發明之一,徹底地改變了我們的生活,但我們似乎已經忘記了自己的「本心」,很少再去瀏覽什麼特定的網站。只是偶爾夜深人靜時,「鐵汁給個網站,好人一生平安」的評論和彈幕仍然昭示著你我心中最純真的渴望。
  • 很火的抖音手繪動畫視頻,是用什麼軟體製作的?這篇文章來教你!
    很多朋友問我,手繪動畫如何製作?需要什麼軟體?這篇文章我們詳細聊一聊手繪動畫如何製作?手繪素材,大家可以關注我微信公眾號,回復手繪素材,發給你。更多抖音手繪動畫乾貨交流,可以加我私人微信。我是財億,創業路上一起多財多億。
  • 如果有人問我憑什麼相信你?請把這條微信溫柔地甩在他臉上!
    一個30多歲的男子走向她,對她說:「我是你媽媽的朋友,我帶你去找你媽媽。」莉莉問:「我憑什麼相信你?」小美和閨蜜在逛街,迎面走來一個婦人和小孩,對她們說:「姑娘,我們從外地來沒錢了,能不能借我100元,過幾天還你。」小美問:「我憑什麼相信你?」小王坐班車從蒙自回個舊,在車站有人問他:「小夥子,我剛好順路去個舊,載你一程,50元。」
  • 「許多年後「假如有人問我,當年你為社會做過的貢獻是什麼?」
    最近,有讀者朋友問我,是如何做到日更四個公眾號(濤聲一久、新新新觀察、神州生氣、時代奇聞錄)的,如何保證四個公眾號每天都有高質量文章發布?說容易也容易,說不容易也不容易,這四個公眾號每天的四條頭條文章其實是我每天從幾百篇文章裡篩選出來的,絕對是百裡挑一。
  • 天天有人問Word文本如何快速對齊,該怎麼操作?這篇文章告訴你!
    > ",再點擊」更多「按鈕,選中」使用通符「單選按鈕,然後點擊「在以下項中查找」按鈕,在彈出的菜單中選擇「當前所選內容「,此時,將會選中文檔中所有2個字的姓名。(2)點擊【開始】-【段落】-【中文版式】-【調整寬度】按鈕,在打開的對話框中設置「新文字寬度」為「3字符」,單擊」確定「按鈕,即可對齊所有姓名。
  • synchronized詳解
    因為靜態成員不屬於任何一個實例對象,是類成員( static 表明這是該類的一個靜態資源,不管 new 了多少個對象,只有一份)。另外,wait/notify等方法也依賴於monitor對象,這就是為什麼只有在同步的塊或者方法中才能調用wait/notify等方法,否則會拋出java.lang.IllegalMonitorStateException的異常的原因。
  • JAVA中synchronized與static synchronized 的區別
    實際上,在類中某方法或某代碼塊中有 synchronized,那麼在生成一個該類實例後,改類也就有一個監視快,放置線程並發訪問改實例synchronized保護快,而static synchronized則是所有該類的實例公用一個監視快了,也也就是兩個的區別了,也就是synchronized相當於 this.synchronized,而static synchronized相當於Something.synchronized
  • 你問我答 之 所有的文章都是你自己寫的嗎?
    經常會有人留言問這個問題,有些人見到我的真人肉身以後除了猛誇我帥以外,也經常要問這個問題,不僅僅問這個問題,還會問:
  • 你真的了解什麼是「傳銷」嗎?看完這篇文章在做判斷!
    經常看老邪文章的朋友應該經常看到在預警資金盤的時候,總是提到涉嫌傳銷,為什麼每次都苦口婆心的提醒呢?因為現在不僅是組織傳銷違法,參與這種行為,也是法律明文規定的違法行為!那麼帶來的後果是什麼?那就是,你賺的錢,和損失的錢,都變成非法的,一旦公安機關破獲案件,你依然會血本無歸,因為非法資金,會被上繳國庫!
  • 還在用Synchronized?Atomic你了解不?
    舉個例子:要將上面的代碼變成線程安全的(每次得出的結果是100),那也很簡單,畢竟我們是學過synchronized鎖的人:public synchronized void increase() {    count++;}無論執行多少次,得出的都是100:
  • Java synchronized 詳解
    下面我們就來一起學習一下synchronized的實現原理及其優化內容吧(至於synchronized與concurrent包中相關鎖的區別,我們會放到以後的sharetime中講解)。能獲得什麼• 明確synchronized的使用方法及其背後的原理,幫助我們更好地使用synchronized關鍵字。