亚洲激情专区-91九色丨porny丨老师-久久久久久久女国产乱让韩-国产精品午夜小视频观看

溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點擊 登錄注冊 即表示同意《億速云用戶服務條款》

如何理解Synchronized

發布時間:2021-06-15 17:51:25 來源:億速云 閱讀:163 作者:chen 欄目:編程語言

本篇內容主要講解“如何理解Synchronized ”,感興趣的朋友不妨來看看。本文介紹的方法操作簡單快捷,實用性強。下面就讓小編來帶大家學習“如何理解Synchronized ”吧!

synchronized 這個關鍵字的重要性不言而喻,幾乎可以說是并發、多線程必須會問到的關鍵字了。synchronized  會涉及到鎖、升級降級操作、鎖的撤銷、對象頭等。所以理解 synchronized 非常重要,本篇文章就帶你從 synchronized 的基本用法、再到  synchronized 的深入理解,對象頭等,為你揭開 synchronized 的面紗。

淺析 synchronized

synchronized 是 Java 并發模塊 非常重要的關鍵字,它是 Java  內建的一種同步機制,代表了某種內在鎖定的概念,當一個線程對某個共享資源加鎖后,其他想要獲取共享資源的線程必須進行等待,synchronized  也具有互斥和排他的語義。

什么是互斥?我們想必小時候都玩兒過磁鐵,磁鐵會有正負極的概念,同性相斥異性相吸,相斥相當于就是一種互斥的概念,也就是兩者互不相容。

synchronized 也是一種獨占的關鍵字,但是它這種獨占的語義更多的是為了增加線程安全性,通過獨占某個資源以達到互斥、排他的目的。

在了解了排他和互斥的語義后,我們先來看一下 synchronized 的用法,先來了解用法,再來了解底層實現。

synchronized 的使用

關于 synchronized 想必你應該都大致了解過

  • synchronized 修飾實例方法,相當于是對類的實例進行加鎖,進入同步代碼前需要獲得當前實例的鎖

  • synchronized 修飾靜態方法,相當于是對類對象進行加鎖

  • synchronized 修飾代碼塊,相當于是給對象進行加鎖,在進入代碼塊前需要先獲得對象的鎖

下面我們針對每個用法進行解釋

synchronized 修飾實例方法

synchronized 修飾實例方法,實例方法是屬于類的實例。synchronized 修飾的實例方法相當于是對象鎖。下面是一個  synchronized 修飾實例方法的例子。

public synchronized void method() {    // ... }

像如上述 synchronized 修飾的方法就是實例方法,下面我們通過一個完整的例子來認識一下 synchronized 修飾實例方法

public class TSynchronized implements Runnable{      static int i = 0;      public synchronized void increase(){         i++;         System.out.println(Thread.currentThread().getName());     }       @Override     public void run() {         for(int i = 0;i < 1000;i++) {             increase();         }     }      public static void main(String[] args) throws InterruptedException {          TSynchronized tSynchronized = new TSynchronized();         Thread aThread = new Thread(tSynchronized);         Thread bThread = new Thread(tSynchronized);         aThread.start();         bThread.start();         aThread.join();         bThread.join();         System.out.println("i = " + i);     } }

上面輸出的結果 i = 2000 ,并且每次都會打印當前現成的名字

來解釋一下上面代碼,代碼中的 i 是一個靜態變量,靜態變量也是全局變量,靜態變量存儲在方法區中。increase 方法由 synchronized  關鍵字修飾,但是沒有使用 static 關鍵字修飾,表示 increase 方法是一個實例方法,每次創建一個 TSynchronized 類的同時都會創建一個  increase 方法,increase 方法中只是打印出來了當前訪問的線程名稱。Synchronized 類實現了 Runnable 接口,重寫了 run  方法,run 方法里面就是一個 0 - 1000 的計數器,這個沒什么好說的。在 main 方法中,new 出了兩個線程,分別是 aThread 和  bThread,Thread.join 表示等待這個線程處理結束。這段代碼主要的作用就是判斷 synchronized 修飾的方法能夠具有獨占性。

synchronized 修飾靜態方法

synchronized 修飾靜態方法就是 synchronized 和 static 關鍵字一起使用

public static synchronized void increase(){}

當 synchronized 作用于靜態方法時,表示的就是當前類的鎖,因為靜態方法是屬于類的,它不屬于任何一個實例成員,因此可以通過 class  對象控制并發訪問。

這里需要注意一點,因為 synchronized 修飾的實例方法是屬于實例對象,而 synchronized 修飾的靜態方法是屬于類對象,所以調用  synchronized 的實例方法并不會阻止訪問 synchronized 的靜態方法。

synchronized 修飾代碼塊

synchronized 除了修飾實例方法和靜態方法外,synchronized 還可用于修飾代碼塊,代碼塊可以嵌套在方法體的內部使用。

public void run() {   synchronized(obj){     for(int j = 0;j < 1000;j++){       i++;     }   } }

上面代碼中將 obj 作為鎖對象對其加鎖,每次當線程進入 synchronized 修飾的代碼塊時就會要求當前線程持有obj  實例對象鎖,如果當前有其他線程正持有該對象鎖,那么新到的線程就必須等待。

synchronized 修飾的代碼塊,除了可以鎖定對象之外,也可以對當前實例對象鎖、class 對象鎖進行鎖定

// 實例對象鎖 synchronized(this){     for(int j = 0;j < 1000;j++){         i++;     } }  //class對象鎖 synchronized(TSynchronized.class){     for(int j = 0;j < 1000;j++){         i++;     } }

synchronized 底層原理

在簡單介紹完 synchronized 之后,我們就來聊一下 synchronized 的底層原理了。

我們或許都有所了解(下文會細致分析),synchronized 的代碼塊是由一組 monitorenter/monitorexit  指令實現的。而Monitor 對象是實現同步的基本單元。

啥是 Monitor 對象呢?

Monitor 對象

任何對象都關聯了一個管程,管程就是控制對象并發訪問的一種機制。管程 是一種同步原語,在 Java 中指的就是 synchronized,可以理解為  synchronized 就是 Java 中對管程的實現。

管程提供了一種排他訪問機制,這種機制也就是 互斥。互斥保證了在每個時間點上,最多只有一個線程會執行同步方法。

所以你理解了 Monitor 對象其實就是使用管程控制同步訪問的一種對象。

對象內存布局

在 hotspot 虛擬機中,對象在內存中的布局分為三塊區域:

  • 對象頭(Header)

  • 實例數據(Instance Data)

  • 對齊填充(Padding)

這三塊區域的內存分布如下圖所示

如何理解Synchronized

我們來詳細介紹一下上面對象中的內容。

對象頭 Header

對象頭 Header 主要包含 MarkWord 和對象指針 Klass Pointer,如果是數組的話,還要包含數組的長度。

如何理解Synchronized

在 32 位的虛擬機中 MarkWord ,Klass Pointer 和數組長度分別占用 32 位,也就是 4 字節。

如果是 64 位虛擬機的話,MarkWord ,Klass Pointer 和數組長度分別占用 64 位,也就是 8 字節。

在 32 位虛擬機和 64 位虛擬機的 Mark Word 所占用的字節大小不一樣,32 位虛擬機的 Mark Word 和 Klass Pointer  分別占用 32 bits 的字節,而 64 位虛擬機的 Mark Word 和 Klass Pointer 占用了64 bits 的字節,下面我們以 32  位虛擬機為例,來看一下其 Mark Word 的字節具體是如何分配的。

如何理解Synchronized

如何理解Synchronized

用中文翻譯過來就是

如何理解Synchronized

  • 無狀態也就是無鎖的時候,對象頭開辟 25 bit 的空間用來存儲對象的 hashcode ,4 bit 用于存放分代年齡,1 bit  用來存放是否偏向鎖的標識位,2 bit 用來存放鎖標識位為 01。

  • 偏向鎖 中劃分更細,還是開辟 25 bit 的空間,其中 23 bit 用來存放線程ID,2bit 用來存放 epoch,4bit 存放分代年齡,1  bit 存放是否偏向鎖標識, 0 表示無鎖,1 表示偏向鎖,鎖的標識位還是 01。

  • 輕量級鎖中直接開辟 30 bit 的空間存放指向棧中鎖記錄的指針,2bit 存放鎖的標志位,其標志位為 00。

  • 重量級鎖中和輕量級鎖一樣,30 bit 的空間用來存放指向重量級鎖的指針,2 bit 存放鎖的標識位,為 11

  • GC標記開辟 30 bit 的內存空間卻沒有占用,2 bit 空間存放鎖標志位為 11。

其中無鎖和偏向鎖的鎖標志位都是 01,只是在前面的 1 bit 區分了這是無鎖狀態還是偏向鎖狀態。

關于為什么這么分配的內存,我們可以從 OpenJDK 中的markOop.hpp類中的枚舉窺出端倪

如何理解Synchronized

來解釋一下

  • age_bits 就是我們說的分代回收的標識,占用4字節

  • lock_bits 是鎖的標志位,占用2個字節

  • biased_lock_bits 是是否偏向鎖的標識,占用1個字節。

  • max_hash_bits 是針對無鎖計算的 hashcode 占用字節數量,如果是 32 位虛擬機,就是 32 - 4 - 2 -1 = 25  byte,如果是 64 位虛擬機,64 - 4 - 2 - 1 = 57 byte,但是會有 25 字節未使用,所以 64 位的 hashcode 占用 31  byte。

  • hash_bits 是針對 64 位虛擬機來說,如果最大字節數大于 31,則取 31,否則取真實的字節數

  • cms_bits 我覺得應該是不是 64 位虛擬機就占用 0 byte,是 64 位就占用 1byte

  • epoch_bits 就是 epoch 所占用的字節大小,2 字節。

在上面的虛擬機對象頭分配表中,我們可以看到有幾種鎖的狀態:無鎖(無狀態),偏向鎖,輕量級鎖,重量級鎖,其中輕量級鎖和偏向鎖是 JDK1.6 中對  synchronized 鎖進行優化后新增加的,其目的就是為了大大優化鎖的性能,所以在 JDK 1.6 中,使用 synchronized  的開銷也沒那么大了。其實從鎖有無鎖定來講,還是只有無鎖和重量級鎖,偏向鎖和輕量級鎖的出現就是增加了鎖的獲取性能而已,并沒有出現新的鎖。

所以我們的重點放在對 synchronized 重量級鎖的研究上,當 monitor 被某個線程持有后,它就會處于鎖定狀態。在 HotSpot  虛擬機中,monitor 的底層代碼是由 ObjectMonitor 實現的,其主要數據結構如下(位于 HotSpot 虛擬機源碼  ObjectMonitor.hpp 文件,C++ 實現的)

如何理解Synchronized

這段 C++ 中需要注意幾個屬性:_WaitSet 、 _EntryList 和 _Owner,每個等待獲取鎖的線程都會被封裝稱為  ObjectWaiter 對象。

如何理解Synchronized

_Owner 是指向了 ObjectMonitor 對象的線程,而 _WaitSet 和 _EntryList 就是用來保存每個線程的列表。

那么這兩個列表有什么區別呢?這個問題我和你聊一下鎖的獲取流程你就清楚了。

鎖的兩個列表

當多個線程同時訪問某段同步代碼時,首先會進入 _EntryList 集合,當線程獲取到對象的 monitor 之后,就會進入 _Owner 區域,并把  ObjectMonitor 對象的 _Owner 指向為當前線程,并使 _count + 1,如果調用了釋放鎖(比如 wait)的操作,就會釋放當前持有的  monitor ,owner = null, _count - 1,同時這個線程會進入到 _WaitSet 列表中等待被喚醒。如果當前線程執行完畢后也會釋放  monitor 鎖,只不過此時不會進入 _WaitSet 列表了,而是直接復位 _count 的值。

如何理解Synchronized

Klass Pointer 表示的是類型指針,也就是對象指向它的類元數據的指針,虛擬機通過這個指針來確定這個對象是哪個類的實例。

你可能不是很理解指針是個什么概念,你可以簡單理解為指針就是指向某個數據的地址。

如何理解Synchronized

實例數據 Instance Data

實例數據部分是對象真正存儲的有效信息,也是代碼中定義的各個字段的字節大小,比如一個 byte 占 1 個字節,一個 int 占用 4 個字節。

對齊 Padding

對齊不是必須存在的,它只起到了占位符(%d, %c 等)的作用。這就是 JVM 的要求了,因為 HotSpot JVM 要求對象的起始地址必須是 8  字節的整數倍,也就是說對象的字節大小是 8 的整數倍,不夠的需要使用 Padding 補全。

鎖的升級流程

先來個大體的流程圖來感受一下這個過程,然后下面我們再分開來說

如何理解Synchronized

無鎖

無鎖狀態,無鎖即沒有對資源進行鎖定,所有的線程都可以對同一個資源進行訪問,但是只有一個線程能夠成功修改資源。

如何理解Synchronized

無鎖的特點就是在循環內進行修改操作,線程會不斷的嘗試修改共享資源,直到能夠成功修改資源并退出,在此過程中沒有出現沖突的發生,這很像我們在之前文章中介紹的  CAS 實現,CAS 的原理和應用就是無鎖的實現。無鎖無法全面代替有鎖,但無鎖在某些場合下的性能是非常高的。

偏向鎖

HotSpot  的作者經過研究發現,大多數情況下,鎖不僅不存在多線程競爭,還存在鎖由同一線程多次獲得的情況,偏向鎖就是在這種情況下出現的,它的出現是為了解決只有在一個線程執行同步時提高性能。

如何理解Synchronized

可以從對象頭的分配中看到,偏向鎖要比無鎖多了線程ID 和 epoch,下面我們就來描述一下偏向鎖的獲取過程

偏向鎖獲取過程

首先線程訪問同步代碼塊,會通過檢查對象頭 Mark Word 的鎖標志位判斷目前鎖的狀態,如果是 01,說明就是無鎖或者偏向鎖,然后再根據是否偏向鎖  的標示判斷是無鎖還是偏向鎖,如果是無鎖情況下,執行下一步

線程使用 CAS 操作來嘗試對對象加鎖,如果使用 CAS 替換 ThreadID  成功,就說明是第一次上鎖,那么當前線程就會獲得對象的偏向鎖,此時會在對象頭的 Mark Word 中記錄當前線程 ID 和獲取鎖的時間 epoch  等信息,然后執行同步代碼塊。

全局安全點(Safe Point):全局安全點的理解會涉及到 C 語言底層的一些知識,這里簡單理解 SafePoint 是 Java  代碼中的一個線程可能暫停執行的位置。

等到下一次線程在進入和退出同步代碼塊時就不需要進行 CAS 操作進行加鎖和解鎖,只需要簡單判斷一下對象頭的 Mark Word  中是否存儲著指向當前線程的線程ID,判斷的標志當然是根據鎖的標志位來判斷的。如果用流程圖來表示的話就是下面這樣

如何理解Synchronized

關閉偏向鎖

偏向鎖在Java 6 和Java 7  里是默認啟用的。由于偏向鎖是為了在只有一個線程執行同步塊時提高性能,如果你確定應用程序里所有的鎖通常情況下處于競爭狀態,可以通過JVM參數關閉偏向鎖:-XX:-UseBiasedLocking=false,那么程序默認會進入輕量級鎖狀態。

關于 epoch

偏向鎖的對象頭中有一個被稱為 epoch 的值,它作為偏差有效性的時間戳。

輕量級鎖

輕量級鎖是指當前鎖是偏向鎖的時候,資源被另外的線程所訪問,那么偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能,下面是詳細的獲取過程。

輕量級鎖加鎖過程

  1. 鴻蒙官方戰略合作共建——HarmonyOS技術社區

  2. 緊接著上一步,如果 CAS 操作替換 ThreadID 沒有獲取成功,執行下一步

  3. 如果使用 CAS 操作替換 ThreadID  失敗(這時候就切換到另外一個線程的角度)說明該資源已被同步訪問過,這時候就會執行鎖的撤銷操作,撤銷偏向鎖,然后等原持有偏向鎖的線程到達全局安全點(SafePoint)時,會暫停原持有偏向鎖的線程,然后會檢查原持有偏向鎖的狀態,如果已經退出同步,就會喚醒持有偏向鎖的線程,執行下一步

  4. 檢查對象頭中的 Mark Word 記錄的是否是當前線程 ID,如果是,執行同步代碼,如果不是,執行偏向鎖獲取流程 的第2步。

如果用流程表示的話就是下面這樣(已經包含偏向鎖的獲取)

如何理解Synchronized

重量級鎖

重量級鎖其實就是 synchronized 最終加鎖的過程,在 JDK 1.6 之前,就是由無鎖 -> 加鎖的這個過程。

重量級鎖的獲取流程

  1. 鴻蒙官方戰略合作共建——HarmonyOS技術社區

  2. 接著上面偏向鎖的獲取過程,由偏向鎖升級為輕量級鎖,執行下一步

  3. 會在原持有偏向鎖的線程的棧中分配鎖記錄,將對象頭中的 Mark Word  拷貝到原持有偏向鎖線程的記錄中,原持有偏向鎖的線程獲得輕量級鎖,然后喚醒原持有偏向鎖的線程,從安全點處繼續執行,執行完畢后,執行下一步,當前線程執行第 4  步

  4. 執行完畢后,開始輕量級解鎖操作,解鎖需要判斷兩個條件

  • 判斷對象頭中的 Mark Word 中鎖記錄指針是否指向當前棧中記錄的指針

如何理解Synchronized

  • 拷貝在當前線程鎖記錄的 Mark Word 信息是否與對象頭中的 Mark Word 一致。

如果上面兩個判斷條件都符合的話,就進行鎖釋放,如果其中一個條件不符合,就會釋放鎖,并喚起等待的線程,進行新一輪的鎖競爭。

  1. 鴻蒙官方戰略合作共建——HarmonyOS技術社區

  2. 在當前線程的棧中分配鎖記錄,拷貝對象頭中的 MarkWord 到當前線程的鎖記錄中,執行 CAS 加鎖操作,會把對象頭 Mark Word  中鎖記錄指針指向當前線程鎖記錄,如果成功,獲取輕量級鎖,執行同步代碼,然后執行第3步,如果不成功,執行下一步

  3. 當前線程沒有使用 CAS 成功獲取鎖,就會自旋一會兒,再次嘗試獲取,如果在多次自旋到達上限后還沒有獲取到鎖,那么輕量級鎖就會升級為 重量級鎖

如何理解Synchronized

如果用流程圖表示是這樣的

如何理解Synchronized

根據上面對于鎖升級細致的描述,我們可以總結一下不同鎖的適用范圍和場景。

如何理解Synchronized

synchronized 代碼塊的底層實現

為了便于方便研究,我們把 synchronized 修飾代碼塊的示例簡單化,如下代碼所示

public class SynchronizedTest {      private int i;      public void syncTask(){         synchronized (this){             i++;         }     }  }

我們主要關注一下 synchronized 的字節碼,如下所示

如何理解Synchronized

從這段字節碼中我們可以知道,同步語句塊使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter  指令指向同步代碼塊的開始位置,monitorexit 指令指向同步代碼塊的結束位置。

那么為什么會有兩個 monitorexit 呢?

不知道你注意到下面的異常表了嗎?如果你不知道什么是異常表,那么我建議你讀一下這篇文章

看完這篇Exception 和 Error,和面試官扯皮就沒問題了

synchronized 修飾方法的底層原理

方法的同步是隱式的,也就是說 synchronized 修飾方法的底層無需使用字節碼來控制,真的是這樣嗎?我們來反編譯一波看看結果

public class SynchronizedTest {      private int i;      public synchronized void syncTask(){         i++;     } }

這次我們使用 javap -verbose 來輸出詳細的結果

如何理解Synchronized

從字節碼上可以看出,synchronized 修飾的方法并沒有使用 monitorenter 和 monitorexit  指令,取得代之是ACC_SYNCHRONIZED 標識,該標識指明了此方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED  訪問標志來辨別一個方法是否聲明為同步方法,從而執行相應的同步調用。這就是 synchronized 鎖在同步代碼塊上和同步方法上的實現差別。

到此,相信大家對“如何理解Synchronized ”有了更深的了解,不妨來實際操作一番吧!這里是億速云網站,更多相關內容可以進入相關頻道進行查詢,關注我們,繼續學習!

向AI問一下細節

免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。

AI

泰顺县| 商丘市| 灵璧县| 桃江县| 阳谷县| 南昌县| 星座| 治多县| 西城区| 科尔| 沁源县| 贵州省| 万载县| 福泉市| 巴马| 通渭县| 将乐县| 宜宾市| 张家口市| 孟连| 兴化市| 无棣县| 淮安市| 鲁山县| 启东市| 双江| 贵定县| 大足县| 万安县| 北川| 绿春县| 界首市| 葫芦岛市| 竹山县| 嘉义市| 梁山县| 乌恰县| 花垣县| 泽库县| 康平县| 竹溪县|