成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

13張圖,深入理解Synchronized

開發 后端
本文帶讀者們由淺入深理解Synchronized,讓讀者們也能與面試官瘋狂對線。

 前言

本文帶讀者們由淺入深理解Synchronized,讓讀者們也能與面試官瘋狂對線。

在并發編程中Synchronized一直都是元老級的角色,Jdk 1.6以前大家都稱呼它為重量級鎖,相對于J U C包提供的Lock,它會顯得笨重,不過隨著Jdk 1.6對Synchronized進行各種優化后,Synchronized性能已經非常快了。

內容大綱

Synchronized使用方式

Synchronized是Java提供的同步關鍵字,在多線程場景下,對共享資源代碼段進行讀寫操作(必須包含寫操作,光讀不會有線程安全問題,因為讀操作天然具備線程安全特性),可能會出現線程安全問題,我們可以使用Synchronized鎖定共享資源代碼段,達到互斥(mutualexclusion)效果,保證線程安全。

共享資源代碼段又稱為臨界區(critical section),保證臨界區互斥,是指執行臨界區(critical section)的只能有一個線程執行,其他線程阻塞等待,達到排隊效果。

Synchronized的食用方式有三種

  •  修飾普通函數,監視器鎖(monitor)便是對象實例(this)
  •  修飾靜態靜態函數,視器鎖(monitor)便是對象的Class實例(每個對象只有一個Class實例)
  •  修飾代碼塊,監視器鎖(monitor)是指定對象實例

普通函數

普通函數使用Synchronized的方式很簡單,在訪問權限修飾符與函數返回類型間加上Synchronized。

多線程場景下,thread與threadTwo兩個線程執行incr函數,incr函數作為共享資源代碼段被多線程讀寫操作,我們將它稱為臨界區,為了保證臨界區互斥,使用Synchronized修飾incr函數即可。 

  1. public class SyncTest {  
  2.     private int j = 0;    
  3.     /**  
  4.      * 自增方法  
  5.      */  
  6.     public synchronized void incr(){  
  7.         //臨界區代碼--start  
  8.         for (int i = 0; i < 10000; i++) {  
  9.             j++;  
  10.         }  
  11.         //臨界區代碼--end  
  12.     }  
  13.     public int getJ() {  
  14.         return j;  
  15.     }  
  16. public class SyncMain {  
  17.     public static void main(String[] agrs) throws InterruptedException {  
  18.         SyncTest syncTest = new SyncTest();  
  19.         Thread thread = new Thread(() -> syncTest.incr());  
  20.         Thread threadTwo = new Thread(() -> syncTest.incr());  
  21.         thread.start();  
  22.         threadTwo.start();  
  23.         thread.join();  
  24.         threadTwo.join();  
  25.         //最終打印結果是20000,如果不使用synchronized修飾,就會導致線程安全問題,輸出不確定結果  
  26.         System.out.println(syncTest.getJ());  
  27.     }  

代碼十分簡單,incr函數被synchronized修飾,函數邏輯是對j進行10000次累加,兩個線程執行incr函數,最后輸出j結果。

被synchronized修飾函數我們簡稱同步函數,線程執行稱同步函數前,需要先獲取監視器鎖,簡稱鎖,獲取鎖成功才能執行同步函數,同步函數執行完后,線程會釋放鎖并通知喚醒其他線程獲取鎖,獲取鎖失敗「則阻塞并等待通知喚醒該線程重新獲取鎖」,同步函數會以this作為鎖,即當前對象,以上面的代碼段為例就是syncTest對象。

  •  線程thread執行syncTest.incr()前
  •  線程thread獲取鎖成功
  •  線程threadTwo執行syncTest.incr()前
  •  線程threadTwo獲取鎖失敗
  •  線程threadTwo阻塞并等待喚醒
  •  線程thread執行完syncTest.incr(),j累積到10000
  •  線程thread釋放鎖,通知喚醒threadTwo線程獲取鎖
  •  線程threadTwo獲取鎖成功
  •  線程threadTwo執行完syncTest.incr(),j累積到20000
  •  線程threadTwo釋放鎖

靜態函數

靜態函數顧名思義,就是靜態的函數,它使用Synchronized的方式與普通函數一致,唯一的區別是鎖的對象不再是this,而是Class對象。

多線程執行Synchronized修飾靜態函數代碼段如下。 

  1. public class SyncTest {  
  2.     private static int j = 0;      
  3.     /**  
  4.      * 自增方法  
  5.      */  
  6.     public static synchronized void incr(){  
  7.         //臨界區代碼--start  
  8.         for (int i = 0; i < 10000; i++) {  
  9.             j++;  
  10.         }  
  11.         //臨界區代碼--end  
  12.     }  
  13.     public static int getJ() {  
  14.         return j;  
  15.     }  
  16.  
  17. public class SyncMain {  
  18.     public static void main(String[] agrs) throws InterruptedException {  
  19.         Thread thread = new Thread(() -> SyncTest.incr());  
  20.         Thread threadTwo = new Thread(() -> SyncTest.incr());  
  21.         thread.start();  
  22.         threadTwo.start();  
  23.         thread.join();  
  24.         threadTwo.join();  
  25.         //最終打印結果是20000,如果不使用synchronized修飾,就會導致線程安全問題,輸出不確定結果  
  26.         System.out.println(SyncTest.getJ());  
  27.     }  

Java的靜態資源可以直接通過類名調用,靜態資源不屬于任何實例對象,它只屬于Class對象,每個Class在J V M中只有唯一的一個Class對象,所以同步靜態函數會以Class對象作為鎖,后續獲取鎖、釋放鎖流程都一致。

代碼塊

前面介紹的普通函數與靜態函數粒度都比較大,以整個函數為范圍鎖定,現在想把范圍縮小、靈活配置,就需要使用代碼塊了,使用{}符號定義范圍給Synchronized修飾。

下面代碼中定義了syncDbData函數,syncDbData是一個偽同步數據的函數,耗時2秒,并且邏輯不涉及共享資源讀寫操作(非臨界區),另外還有兩個函數incr與incrTwo,都是在自增邏輯前執行了syncDbData函數,只是使用Synchronized的姿勢不同,一個是修飾在函數上,另一個是修飾在代碼塊上。 

  1. public class SyncTest {  
  2.     private static int j = 0 
  3.     /**  
  4.      * 同步庫數據,比較耗時,代碼資源不涉及共享資源讀寫操作。  
  5.      */  
  6.     public void syncDbData() {  
  7.         System.out.println("db數據開始同步------------");  
  8.         try {  
  9.             //同步時間需要2秒  
  10.             Thread.sleep(2000);  
  11.         } catch (InterruptedException e) {  
  12.             e.printStackTrace();  
  13.         }  
  14.         System.out.println("db數據開始同步完成------------");  
  15.     }  
  16.     //自增方法  
  17.     public synchronized void incr() {  
  18.         //start--臨界區代碼  
  19.         //同步庫數據  
  20.         syncDbData();  
  21.         for (int i = 0; i < 10000; i++) {  
  22.             j++;  
  23.         }  
  24.         //end--臨界區代碼  
  25.     }  
  26.     //自增方法  
  27.     public void incrTwo() {  
  28.         //同步庫數據  
  29.         syncDbData();  
  30.         synchronized (this) {  
  31.             //start--臨界區代碼  
  32.             for (int i = 0; i < 10000; i++) {  
  33.                 j++;  
  34.             }  
  35.             //end--臨界區代碼  
  36.         }  
  37.     }  
  38.     public int getJ() {  
  39.         return j;  
  40.     }  
  41.  
  42. public class SyncMain {  
  43.     public static void main(String[] agrs) throws InterruptedException {  
  44.         //incr同步方法執行  
  45.         SyncTest syncTest = new SyncTest();  
  46.         Thread thread = new Thread(() -> syncTest.incr());  
  47.         Thread threadTwo = new Thread(() -> syncTest.incr());  
  48.         thread.start();  
  49.         threadTwo.start();  
  50.         thread.join();  
  51.         threadTwo.join();  
  52.         //最終打印結果是20000  
  53.         System.out.println(syncTest.getJ());  
  54.         //incrTwo同步塊執行  
  55.         thread = new Thread(() -> syncTest.incrTwo());  
  56.         threadTwo = new Thread(() -> syncTest.incrTwo());  
  57.         thread.start();  
  58.         threadTwo.start();  
  59.         thread.join();  
  60.         threadTwo.join();  
  61.         //最終打印結果是40000  
  62.         System.out.println(syncTest.getJ());  
  63.     }  

先看看incr同步方法執行,流程和前面沒區別,只是Synchronized鎖定的范圍太大,把syncDbData()也納入臨界區中,多線程場景執行,會有性能上的浪費,因為syncDbData()完全可以讓多線程并行或并發執行。

我們通過代碼塊的方式,來縮小范圍,定義正確的臨界區,提升性能,目光轉到incrTwo同步塊執行,incrTwo函數使用修飾代碼塊的方式同步,只對自增代碼段進行鎖定。

代碼塊同步方式除了靈活控制范圍外,還能做線程間的協同工作,因為Synchronized ()括號中能接收任何對象作為鎖,所以可以通過Object的wait、notify、notifyAll等函數,做多線程間的通信協同(本文不對線程通信協同做展開,主角是Synchronized,而且也不推薦去用這些方法,因為LockSupport工具類會是更好的選擇)。

  •  wait:當前線程暫停,釋放鎖
  •  notify:釋放鎖,喚醒調用了wait的線程(如果有多個隨機喚醒一個)
  •  notifyAll:釋放鎖,喚醒調用了wait的所有線程

Synchronized原理 

  1. public class SyncTest {  
  2.     private static int j = 0 
  3.     /**  
  4.      * 同步庫數據,比較耗時,代碼資源不涉及共享資源讀寫操作。  
  5.      */  
  6.     public void syncDbData() {  
  7.         System.out.println("db數據開始同步------------");  
  8.         try {  
  9.             //同步時間需要2秒  
  10.             Thread.sleep(2000);  
  11.         } catch (InterruptedException e) {  
  12.             e.printStackTrace();  
  13.         }  
  14.         System.out.println("db數據開始同步完成------------");  
  15.     }  
  16.     //自增方法  
  17.     public synchronized void incr() {  
  18.         //start--臨界區代碼  
  19.         //同步庫數據  
  20.         syncDbData();  
  21.         for (int i = 0; i < 10000; i++) {  
  22.             j++;  
  23.         }  
  24.         //end--臨界區代碼  
  25.     }  
  26.     //自增方法  
  27.     public void incrTwo() {  
  28.         //同步庫數據  
  29.         syncDbData();  
  30.         synchronized (this) {  
  31.             //start--臨界區代碼  
  32.             for (int i = 0; i < 10000; i++) {  
  33.                 j++;  
  34.             }  
  35.             //end--臨界區代碼  
  36.         }  
  37.     }  
  38.     public int getJ() {  
  39.         return j;  
  40.     }  
  41. }  

為了探究Synchronized原理,我們對上面的代碼進行反編譯,輸出反編譯后結果,看看底層是如何實現的(環境Java 11、win 10系統)。 

  1. 只截取了incr與incrTwo函數內容          
  2.  public synchronized void incr();  
  3.    Code:  
  4.       0: aload_0                                          
  5.       1: invokevirtual #11                 // Method syncDbData:()V   
  6.       4: iconst_0                        
  7.       5: istore_1                        
  8.       6: iload_1                                    
  9.       7: sipush        10000            
  10.      10: if_icmpge     27  
  11.      13: getstatic     #12                 // Field j:I  
  12.      16: iconst_1  
  13.      17: iadd  
  14.      18: putstatic     #12                 // Field j:I  
  15.      21: iinc          1, 1  
  16.      24: goto          6  
  17.      27: return   
  18.  public void incrTwo();      
  19.    Code:  
  20.       0: aload_0  
  21.       1: invokevirtual #11                 // Method syncDbData:()V  
  22.       4: aload_0  
  23.       5: dup  
  24.       6: astore_1  
  25.       7: monitorenter                     //獲取鎖  
  26.       8: iconst_0  
  27.       9: istore_2  
  28.      10: iload_2  
  29.      11: sipush        10000  
  30.      14: if_icmpge     31  
  31.      17: getstatic     #12                 // Field j:I  
  32.      20: iconst_1  
  33.      21: iadd  
  34.      22: putstatic     #12                 // Field j:I  
  35.      25: iinc          2, 1  
  36.      28: goto          10  
  37.      31: aload_1  
  38.      32: monitorexit                      //正常退出釋放鎖   
  39.      33: goto          41  
  40.      36: astore_3  
  41.      37: aload_1  
  42.      38: monitorexit                      //異步退出釋放鎖    
  43.      39: aload_3  
  44.      40: athrow  
  45.      41: return 

ps:對上面指令感興趣的讀者,可以百度或google一下“JVM 虛擬機字節碼指令表”

先看incrTwo函數,incrTwo是代碼塊方式同步,在反編譯后的結果中,我們發現存在monitorenter與monitorexit指令(獲取鎖、釋放鎖)。

monitorenter指令插入到同步代碼塊的開始位置,monitorexit指令插入到同步代碼塊的結束位置,J V M需要保證每一個 monitorenter都有monitorexit與之對應。

任何對象都有一個監視器鎖(monitor)關聯,線程執行monitorenter指令時嘗試獲取monitor的所有權。

  •  如果monitor的進入數為0,則該線程進入monitor,然后將進入數設置為1,該線程為monitor的所有者
  •  如果線程已經占有該monitor,重新進入,則monitor的進入數加1
  •  線程執行monitorexit,monitor的進入數-1,執行過多少次monitorenter,最終要執行對應次數的monitorexit
  •  如果其他線程已經占用monitor,則該線程進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權

回過頭看incr函數,incr是普通函數方式同步,雖然在反編譯后的結果中沒有看到monitorenter與monitorexit指令,但是實際執行的流程與incrTwo函數一樣,通過monitor來執行,只不過它是一種隱式的方式來實現,最后放一張流程圖。

Synchronized優化

Jdk 1.5以后對Synchronized關鍵字做了各種的優化,經過優化后Synchronized已經變得原來越快了,這也是為什么官方建議使用Synchronized的原因,具體的優化點如下。

  •  鎖粗化
  •  鎖消除
  •  鎖升級

鎖粗化

互斥的臨界區范圍應該盡可能小,這樣做的目的是為了使同步的操作數量盡可能縮小,縮短阻塞時間,如果存在鎖競爭,那么等待鎖的線程也能盡快拿到鎖。

但是加鎖解鎖也需要消耗資源,如果存在一系列的連續加鎖解鎖操作,可能會導致不必要的性能損耗,鎖粗化就是將「多個連續的加鎖、解鎖操作連接在一起」,擴展成一個范圍更大的鎖,避免頻繁的加鎖解鎖操作。

J V M會檢測到一連串的操作都對同一個對象加鎖(for循環10000次執行j++,沒有鎖粗化就要進行10000次加鎖/解鎖),此時J V M就會將加鎖的范圍粗化到這一連串操作的外部(比如for循環體外),使得這一連串操作只需要加一次鎖即可。

鎖消除

Java虛擬機在JIT編譯時(可以簡單理解為當某段代碼即將第一次被執行時進行編譯,又稱即時編譯),通過對運行上下文的掃描,經過逃逸分析(對象在函數中被使用,也可能被外部函數所引用,稱為函數逃逸),去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的時間消耗。

代碼中使用Object作為鎖,但是Object對象的生命周期只在incrFour()函數中,并不會被其他線程所訪問到,所以在J I T編譯階段就會被優化掉(此處的Object屬于沒有逃逸的對象)。

鎖升級

Java中每個對象都擁有對象頭,對象頭由Mark World 、指向類的指針、以及數組長度三部分組成,本文,我們只需要關心Mark World 即可,Mark World  記錄了對象的HashCode、分代年齡和鎖標志位信息。

Mark World簡化結構

鎖狀態 存儲內容 鎖標記
無鎖 對象的hashCode、對象分代年齡、是否是偏向鎖(0) 01
偏向鎖 偏向線程ID、偏向時間戳、對象分代年齡、是否是偏向鎖(1) 01
輕量級鎖 指向棧中鎖記錄的指針 00
重量級鎖 指向互斥量(重量級鎖)的指針 10

讀者們只需知道,鎖的升級變化,體現在鎖對象的對象頭Mark World部分,也就是說Mark World的內容會隨著鎖升級而改變。

Java1.5以后為了減少獲取鎖和釋放鎖帶來的性能消耗,引入了偏向鎖和輕量級鎖,Synchronized的升級順序是 「無鎖-->偏向鎖-->輕量級鎖-->重量級鎖,只會升級不會降級」

偏向鎖

在大多數情況下,鎖總是由同一線程多次獲得,不存在多線程競爭,所以出現了偏向鎖,其目標就是在只有一個線程執行同步代碼塊時,降低獲取鎖帶來的消耗,提高性能(可以通過J V M參數關閉偏向鎖:-XX:-UseBiasedLocking=false,關閉之后程序默認會進入輕量級鎖狀態)。

線程執行同步代碼或方法前,線程只需要判斷對象頭的Mark Word中線程ID與當前線程ID是否一致,如果一致直接執行同步代碼或方法,具體流程如下

  •  無鎖狀態,存儲內容「是否為偏向鎖(0)」,鎖標識位01
    •   CAS設置當前線程ID到Mark Word存儲內容中
    •   是否為偏向鎖0 => 是否為偏向鎖1
    •    執行同步代碼或方法
  •  偏向鎖狀態,存儲內容「是否為偏向鎖(1)、線程ID」,鎖標識位01
    •    對比線程ID是否一致,如果一致執行同步代碼或方法,否則進入下面的流程
    •    如果不一致,CAS將Mark Word的線程ID設置為當前線程ID,設置成功,執行同步代碼或方法,否則進入下面的流程
    •    CAS設置失敗,證明存在多線程競爭情況,觸發撤銷偏向鎖,當到達全局安全點,偏向鎖的線程被掛起,偏向鎖升級為輕量級鎖,然后在安全點的位置恢復繼續往下執行。

輕量級鎖

輕量級鎖考慮的是競爭鎖對象的線程不多,持有鎖時間也不長的場景。因為阻塞線程需要C P U從用戶態轉到內核態,代價較大,如果剛剛阻塞不久這個鎖就被釋放了,那這個代價就有點得不償失,所以干脆不阻塞這個線程,讓它自旋一段時間等待鎖釋放。

當前線程持有的鎖是偏向鎖的時候,被另外的線程所訪問,偏向鎖就會升級為輕量級鎖,其他線程會通過自旋的形式嘗試獲取鎖,不會阻塞,從而提高性能。輕量級鎖的獲取主要有兩種情況:① 當關閉偏向鎖功能時;② 多個線程競爭偏向鎖導致偏向鎖升級為輕量級鎖。

  •  無鎖狀態,存儲內容「是否為偏向鎖(0)」,鎖標識位01
    •   關閉偏向鎖功能時
    •   CAS設置當前線程棧中鎖記錄的指針到Mark Word存儲內容
    •   鎖標識位設置為00
    •   執行同步代碼或方法
    •   釋放鎖時,還原來Mark Word內容
  •  輕量級鎖狀態,存儲內容「線程棧中鎖記錄的指針」,鎖標識位00(存儲內容的線程是指"持有輕量級鎖的線程")
    •   CAS設置當前線程棧中鎖記錄的指針到Mark Word存儲內容,設置成功獲取輕量級鎖,執行同步塊代碼或方法,否則執行下面的邏輯
    •   設置失敗,證明多線程存在一定競爭,線程自旋上一步的操作,自旋一定次數后還是失敗,輕量級鎖升級為重量級鎖
    •    Mark Word存儲內容替換成重量級鎖指針,鎖標記位10

重量級鎖

輕量級鎖膨脹之后,就升級為重量級鎖,重量級鎖是依賴操作系統的MutexLock(互斥鎖)來實現的,需要從用戶態轉到內核態,這個成本非常高,這就是為什么Java1.6之前Synchronized效率低的原因。

升級為重量級鎖時,鎖標志位的狀態值變為10,此時Mark Word中存儲內容的是重量級鎖的指針,等待鎖的線程都會進入阻塞狀態,下面是簡化版的鎖升級過程。

 

 

責任編輯:龐桂玉 來源: Java編程
相關推薦

2021-04-25 10:45:59

Docker架構Job

2022-07-04 08:01:01

鎖優化Java虛擬機

2020-11-13 08:42:24

Synchronize

2019-07-24 08:49:36

Docker容器鏡像

2010-06-01 15:25:27

JavaCLASSPATH

2016-12-08 15:36:59

HashMap數據結構hash函數

2020-07-21 08:26:08

SpringSecurity過濾器

2023-10-19 11:12:15

Netty代碼

2021-02-17 11:25:33

前端JavaScriptthis

2009-09-25 09:14:35

Hibernate日志

2013-09-22 14:57:19

AtWood

2020-09-23 10:00:26

Redis數據庫命令

2019-06-25 10:32:19

UDP編程通信

2017-01-10 08:48:21

2024-02-21 21:14:20

編程語言開發Golang

2025-05-06 00:43:00

MySQL日志文件MIXED 3

2017-08-15 13:05:58

Serverless架構開發運維

2025-06-05 05:51:33

2022-08-02 08:32:21

Spring項目網關

2015-11-04 09:57:18

JavaScript原型
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 日韩精品在线免费观看视频 | 亚洲视频在线看 | 日韩精品在线观看一区二区三区 | 免费一区 | 欧美综合久久 | 国产精品久久二区 | 欧美日韩91 | 日本成年免费网站 | 久久精品视频在线观看 | 精品伊人 | 韩日一区二区 | 国产成在线观看免费视频 | 高清久久| 伊人成人免费视频 | 在线成人免费视频 | 日本精品一区二区在线观看 | 国产专区在线 | 成人h片在线观看 | 97久久精品午夜一区二区 | 国产一区二区三区视频免费观看 | 精品自拍视频在线观看 | 亚洲第一av| 亚洲成人黄色 | 久草在线 | 婷婷丁香激情 | 亚洲精品无 | 国产精品99久久久精品免费观看 | 国产激情综合五月久久 | 高清欧美性猛交xxxx黑人猛交 | 日本午夜精品 | 国产一区二区在线观看视频 | 中文一区| 国产精品一区网站 | 热99在线 | 欧美在线a | 天天干狠狠干 | 精品毛片在线观看 | 欧美在线视频一区二区 | 999视频在线播放 | 中文字幕亚洲一区 | 谁有毛片|