併發和多線程-八面玲瓏的synchronized

為什麼說synchronized是八面玲瓏呢,因為它可以混跡在很多“場所”(方法、代碼塊),與各種角色(類、對象)打交道。

也正是因為它的八面玲瓏,所以就顯得比較神秘,也比較複雜,今天就來追蹤下synchronized常去的地方和經常搭訕的角色。核心概念主要是介紹對象鎖和類鎖。

背景

synchronized,作為一種鎖,主要是用於解決在多線程下的同步問題。

上篇中,我們在介紹可見性的時候提到了java的內存模型,有主內存和工作內存。

對應到我們常見的堆、棧的理解是這樣的。

主內存主要包括本地方法區和堆。每個線程都有一個工作內存,工作內存中主要包括兩個部分,一個是屬於該線程私有的棧和對主存部分變量拷貝的寄存器(包括程序計數器PC和cup工作的高速緩存區)。

1.所有的變量都存儲在主內存中(虛擬機內存的一部分),對於所有線程都是共享的。

2.每條線程都有自己的工作內存,工作內存中保存的是主存中某些變量的拷貝,線程對變量的所有操作都必須在工作內存中進行,而不能直接讀寫主內存中的變量。

3.線程之間無法直接訪問對方的工作內存中的變量,線程間變量的傳遞均需要通過主內存來完成。

在JVM中,每個對象和類都會與一個監聽器關聯,為了實現監聽器的排他監視能力,每個對象和類都會關聯一個鎖。當某個線程獲取了某個對象的鎖,則由於排他性,其他線程就會阻塞等待獲取鎖以獲取執行權。

每個對象都只有唯一一個鎖,同一時間,也只有一個線程可以擁有該鎖。

類鎖,其實可以理解為一種特殊的對象鎖,因為在JVM並不存在所謂的類鎖。

當JVM加載某個class時,加在這個Class對象上的就是類鎖。所有該類的實例共享這個類鎖,當某對象獲取類鎖權限時,則對於所有靜態方法具有相同的執行權。

使用synchronized和未使用synchronized的對比

1、 不使用synchronized

併發和多線程-八面玲瓏的synchronized

併發和多線程-八面玲瓏的synchronized

併發和多線程-八面玲瓏的synchronized

併發和多線程-八面玲瓏的synchronized

  • 該代碼實例是多線程環境(兩個線程)
  • 兩個線程共用一個實例,HasSelfPrivateNum類的實例
  • 在main主線程中分別啟動ThreadA和ThreadB
  • 不考慮重排序,首先創建ThreadA並啟動,此時判斷username.equal("a"),成立,此時賦值num=100,並休眠2秒鐘
  • 在線程A休眠期間,因為沒有實現同步,所以ThreadB啟動也進入該方法,判定username.equal("a")不符合(此時username="b"),所以此時num=200
  • 等到ThreadA的2秒睡眠時間過去後,此時發現num已經被賦值200,所以此時也打印出num=200

最後的執行結果如下

併發和多線程-八面玲瓏的synchronized

注意:

這裡有一個可見性的思考。當我們如果沒有接觸或者不瞭解可見性這個概念之前,我們想當然的認為ThreadA和ThreadB都是操作了num變量,那麼對於同一個變量操作肯定最終都是保持一致的,所以都是num=200。

a set over!

b set over!

b num=200

a num=100

2、使用synchronized

上面的例子是沒有使用synchronized的情況,如果加上synchronized關鍵字,這時候相當於在addI()方法上加鎖了,更準確的說是在HasSelfPrivateNum類的實例化對象上獲取了對象鎖。

鑑於一個對象在同一時間只能被一個線程佔有,所以當ThreadA進入方法後,會一直執行知道結束,即使這裡有休眠2秒鐘,ThreadB只能乖乖的等ThreadA執行完才能獲取執行權繼續執行。最終執行結果如下

併發和多線程-八面玲瓏的synchronized

synchronized使用的四種同步場景

synchronized使用場景主要包括如下四種同步場景

  • 實例方法(對象鎖)
  • 靜態方法(類鎖)
  • 實例方法中的代碼塊(對象鎖)
  • 靜態方法中的代碼塊(類鎖)

1、實例方法

參見上面對比例子中“加synchronized”的情況

2、靜態方法

併發和多線程-八面玲瓏的synchronized

併發和多線程-八面玲瓏的synchronized

併發和多線程-八面玲瓏的synchronized

執行結果如下

線程名稱為:A在1528626219469進入printA

線程名稱為:A在1528626222473離開printA

線程名稱為:B在1528626222473進入printB

線程名稱為:B在1528626222474離開printB

這裡的synchronized是加載靜態方法上的,我們知道靜態方法是通過類直接調用的,不需要實例化的。這裡用的就是類鎖,也就是類的Class對象的鎖,所以這裡兩個線程在同一時間也只會有一個獲取到該類鎖從而獲得執行權。

3、實例方法中的同步塊

直接看代碼

併發和多線程-八面玲瓏的synchronized

併發和多線程-八面玲瓏的synchronized

併發和多線程-八面玲瓏的synchronized

執行結果如下

begin time=1528625980467

end end=1528625982471

begin time=1528625982471

end end=1528625984472

這裡的this就是ObjectService類的實例化對象,因為一個對象只有一個對象鎖,所以這裡可以保證同步,只有前一個線程執行完後,後一個線程才有機會執行。

4、靜態方法中的同步塊

參見2和3,只是在靜態方法內部加上synchronized。本質還是類鎖。

文中肯定有理解偏差的地方,寫博客的好處就是,本來已經認為理所當然的地方,當需要一字一句寫出來的時候,就會加深思考一些問題的細節。

好比文中沒有加synchronized的例子,突然想到可見性,又想到主內存和工作內存以及堆棧之類的內存結構,雖然一度被繞暈,查了兩小時的資料,最終也算是找了一套理論勉強把自己說服。

如果您覺得閱讀本文對您有幫助,請點一下“關注”按鈕,您的“關注”將是我最大的寫作動力!


分享到:


相關文章: