日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

synchronized原理_synchronized关键字的作用、原理以及锁优化

發布時間:2024/1/23 编程问答 24 豆豆
生活随笔 收集整理的這篇文章主要介紹了 synchronized原理_synchronized关键字的作用、原理以及锁优化 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

synchronized簡介

synchronized塊是Java提供的一種原子性內置鎖,Java中的每個對象都可以隱式地扮演一個用于同步的鎖的角色。這些Java內置的使用者看不到的鎖被稱為內部鎖(intrinsic locks),也叫作監視器鎖(monitor locks)。

線程在進入synchronized代碼塊會自動獲取內部鎖,這時候其他線程訪問該同步代碼塊時會被阻塞掛起。

拿到內部鎖的線程會在正常退出同步代碼塊或者拋出異常后或者在同步塊內調用該內置鎖資源的wait系列方法時釋放該鎖。

內部鎖在Java中扮演了互斥鎖(mutual exclusion lock,也稱作mutex)的角色,意味著至多只有一個線程可以擁有鎖,當線程A嘗試請求一個被線程B占有的鎖時,線程A必須等待或者阻塞,直到B釋放它。如果B永遠不釋放鎖,A將永遠等下去。

除了用于線程同步、確保線程安全外,關鍵字synchronized還可以保證線程間的可見性和有序性。

synchronized 使用的三種方式

  • 修飾代碼塊: 指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。
  • 修飾實例方法: 作用于當前對象實例加鎖,進入同步代碼前要獲得當前對象實例的鎖
  • 修飾靜態方法: 也就是給當前類加鎖,會作用于類的所有對象實例,因為靜態成員不屬于任何一個實例對象,是類成員( static 表明這是該類的一個靜態資源,不管new了多少個對象,只有一份)。所以如果一個線程 A 調用一個實例對象的非靜態 synchronized 方法,而線程 B 需要調用這個實例對象所屬類的靜態 synchronized 方法,是允許的,不會發生互斥現象,因為訪問靜態 synchronized 方法占用的鎖是當前類的鎖,而訪問非靜態 synchronized 方法占用的鎖是當前實例對象鎖。

實例1:修飾代碼塊:

public class AccountingSync implements Runnable{static final AccountingSync instance = new AccountingSync();static int i = 0;@Overridepublic void run() {for (int j = 0; j < 10000000; j++) {synchronized (instance) {i++;}}} }

上述代碼將synchronized作用于一個給定對象instance,因此,每次當線程進入關鍵字synchronized包裹的代碼段,就都會要求請求instance實例的鎖。如果當前有其他線程正持有這把鎖,那么新到的線程就必須等待。這樣,就保證了每次只能有一個線程執行i++操作。

示例2:修飾實例方法:

public class AccountingSync2 implements Runnable{static final AccountingSync2 instance = new AccountingSync2();static int i = 0;public synchronized void increase(){i++;}@Overridepublic void run() {for (int j = 0; j < 10000000; j++) {increase();}} }public static void main(String[] args) throws InterruptedException {//顯示創建線程,兩個線程都指向同一個Runnable接口實例(instance對象)Thread t1 = new Thread(instance);Thread t2 = new Thread(instance);t1.start();t2.start();t1.join();t2.join();}

上述代碼中,關鍵字 synchronized 作用于一個實例方法。就是說在進入increase()方法前,線程必須獲得當前對象實例的鎖,在本例中就是instance。

這里特定寫出了main()方法,在main()方法,我們顯示的創建了兩個線程,并且將兩個線程都指向同一個Runnable接口實例(instance對象)。這樣才能保證兩個線程在工作時,能夠關注到同一個對象鎖上去,從而保證線程安全。

錯誤示例2.1:

public class AccountingSync2 implements Runnable{static final AccountingSync2 instance = new AccountingSync2();static int i = 0;public synchronized void increase(){i++;}@Overridepublic void run() {for (int j = 0; j < 10000000; j++) {increase();}}public static void main(String[] args) throws InterruptedException {//錯誤用法Thread t1 = new Thread(new AccountingSync2);Thread t2 = new Thread(new AccountingSync2);t1.start();t2.start();t1.join();t2.join();} }

示例3:修飾靜態方法

錯誤示例2.1可以簡單的修改一下就可以正常工作,將increase()方法修改如下:

public static synchronized void increase(){i++;}

這樣,即使兩個線程指向不同的Runnable對象,也可以正確同步。因為方法快請求的是當前類的鎖,而非當前實例。

synchronized底層原理

修改代碼塊

public class SynchronizedDemo {public void method() {synchronized (this) {System.out.println("synchronized 代碼塊");}} }

通過 JDK 自帶的 javap 命令查看 SynchronizedDemo 類的相關字節碼信息:首先切換到類的對應目錄執行 javac SynchronizedDemo.java 命令生成編譯后的 .class 文件,然后執行javap -c -s -v -l SynchronizedDemo.class。

synchronized 同步語句塊經過Javac編譯后,會在同步塊的前后分別形成 monitorenter 和 monitorexit 指令。這兩個字節碼指令都需要一個reference類型的參數來指明要鎖定和解鎖的對象。

根據《Java虛擬機規范》的要求,在執行 monitorenter指令時,首先要去嘗試獲取對象的鎖。如果這個對象沒被鎖定,或者當前線程已經持有了那個對象的鎖,就把鎖的計數器的值增加一,而在執行 monitorexit 指令時會將鎖計數器的值減一。一旦計數器的值為零,鎖隨即就被釋放了。如果獲取對象鎖失敗,那當前線程就應當被阻塞等待,直到請求鎖定的對象被持有它的線程釋放為止。

修飾方法的情況

public class SynchronizedDemo2 {public synchronized void method() {System.out.println("synchronized 方法");} }

synchronized 修飾的方法并沒有 monitorenter 指令和 monitorexit 指令,取得代之的確實是 ACC_SYNCHRONIZED 標識,該標識指明了該方法是一個同步方法,JVM 通過該 ACC_SYNCHRONIZED 訪問標志來辨別一個方法是否聲明為同步方法,從而執行相應的同步調用。

JDK1.6之后的鎖優化

synchronized 是Java語言中的一個重量級的操作,在主流Java虛擬機實現中,Java的線程時映射到操作系統的原生內核線程之上的,如果要阻塞或喚醒一條線程,則需要操作系統來幫忙完成,這就不可避免地陷入用戶態到核心態的轉換中,進行這種狀態轉換需要很多的處理器時間。尤其是對于代碼特別簡單的同步塊(譬如被 synchronized修飾的getter()或setter()方法),狀態轉消耗的時間甚至會比用戶代碼本身執行的時間還要長。

JDK5升級到JDK6,HotPot虛擬機開發團隊在這個版本花了大量資源去實現各種鎖優化技術,如適應性自旋鎖(Adaptive Spinning)、鎖消除(Lock Elimination)、鎖粗化(Lock Coarsening)、輕量級鎖(Lightweight Locking)、偏向鎖(Biased Locking)等,這些技術都是為了提升鎖的效率。

偏向鎖

偏向鎖的目的是消除數據在無競爭情況下的同步原語,進一步提高程序的運行性能。

偏向鎖的核心思想是如果一個線程獲得了鎖,那么鎖就進入偏向模式。當這個線程再次請求鎖時,無須再做任何同步操作。這樣就節省了大量有關鎖申請的操作,從而提高了程序性能。因此,對于幾乎沒有鎖競爭的場合,偏向鎖有比較好的優化效果,因為連續多次極有可能是同一個線程請求相同的鎖。而對于鎖競爭比較激烈的場合,其效果不佳,因為這時偏向模式會失效。使用Java虛擬機參數-XX:+UseBiasedLocking可以開啟偏向鎖。

要理解偏向鎖以及后面的輕量級鎖的原理,必須了解HotSpot虛擬機的對象頭信息。HotSpot虛擬機的對象頭分為兩部分

  • 第一部分用于存儲對象自身的運行時數據,如哈希碼(HashCode)、GC分代年齡(Generational GC Age)等。這部分數據的長度在32位和64位的Java虛擬機中分別占用32個或64個比特,官方稱它為“Mark Word”。這部分是實現偏向鎖和輕量級鎖的關鍵。
  • 第二部分用于存儲指向方法區對象類型數據的指針,如果是數組對象,還會有一個額外的部分用于存儲數組長度。

這里重點介紹Mark Word,它被設計成一個非固定的動態數據結構,以盡可能的減少占用空間。鎖的狀態不同,存儲的內容也不同。以下是不同狀態下對象頭的存儲內容示意圖:

(圖源《深入理解Java虛擬機》)

當鎖對象第一次被線程獲取的時候,虛擬機會把對象頭的標志位設置為“01”、把偏向模式設置為“1”,表示進入偏向模式,同時使用CAS操作把獲得這個鎖的線程的ID記錄在對象的Mark Word中。如果CAS操作成功,持有偏向鎖的線程以后在此進入這個鎖的同步塊時,虛擬機都可以不再進行任何同步操作(例如加鎖、解鎖以及對Mark Word的更新操作等。)

一旦有另外一個線程來嘗試獲取這個鎖,偏向模式馬上就會結束。根據鎖對象目前是否處于被鎖定狀態決定是否撤銷偏向(偏向模式設置為“0”)。撤銷后標志位恢復到未鎖定(標志位為“01”或輕量級鎖定(標志位為“00”)的狀態)。

輕量級鎖

如果偏向鎖失敗,虛擬機也不會立即掛起線程,它還會使用一種稱為輕量級鎖的優化手段。

區別于重量級鎖使用操作系統互斥量來實現,輕量級鎖的加鎖和解鎖都使用CAS來完成。

輕量級鎖的加鎖

線程在執行同步塊之前,JVM會先在當前線程的楨棧中創建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,將對象Mark Word的鎖標志位轉變為“00”,表示此對象處于輕量級鎖狀態。

如果失敗,表示其他線程搶先爭奪到了鎖,輕量級鎖就不再有效,膨脹為重量級鎖,并嘗試使用自旋來獲取鎖。

輕量級鎖解鎖

輕量級鎖解鎖時,會使用CAS操作將Displaced Mark Word 替換回到對象頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖正在競爭,鎖就會膨脹成重量級鎖。

輕量級鎖能提升性能的依據是“對于絕大部分鎖,在整個同步周期內都不存在競爭”這一經驗法則。如果沒有競爭,輕量級鎖便通過CAS操作避免了使用互斥量的開銷;但如果存在鎖競爭,除了互斥量的本身開銷外,還額外發生了CAS操作的開銷。因此在有競爭的情況下,輕量級鎖反而比傳統的重量級鎖更慢。

自旋鎖和自適應自旋

輕量級鎖失敗后,虛擬機為了避免線程真實地在操作系統層面掛起,還會進行一項稱為自旋鎖的優化手段。

掛起和恢復線程的操作都需要轉入內核態中完成,如果加鎖失敗便簡單粗暴的掛起線程可能得不償失。因為虛擬機的開發團隊注意到在許多應用上,共享數據的鎖定狀態只會持續很短的一段時間。為了這段時間去掛起和恢復線程并不值得。

這時,虛擬機會讓沒請求到鎖的線程做幾個空循環(自旋),這時線程并不會放棄處理器的執行時間,,在經過若干次循環后,如果可以得到鎖,那便進入同步塊。

自旋鎖在 JDK1.6 之前其實就已經引入了,不過是默認關閉的,需要通過--XX:+UseSpinning參數來開啟。JDK1.6及1.6之后,就改為默認開啟的了。需要注意的是:自旋等待不能完全替代阻塞,因為它還是要占用處理器時間。如果鎖被占用的時間短,那么效果當然就很好了!反之,自旋等待的時間必須要有限度。如果自旋超過了限定次數任然沒有獲得鎖,就應該掛起線程。自旋次數的默認值是10次,用戶可以修改--XX:PreBlockSpin來更改。

另外,在 JDK1.6 中引入了自適應的自旋鎖。自適應的自旋鎖帶來的改進就是:自旋的時間不在固定了,而是由前一次在同一個鎖上的自旋時間以及鎖的擁有者的狀態來決定。

如果在同一個鎖對象上,自旋等待剛成功獲得過鎖,并且持有鎖的線程正在運行,那么虛擬機就會認為這次自旋也很可能再次成功,進而允許自旋等待更長的時間。另一方面,如果對于某個鎖,自旋很少成功獲得鎖,那以后要獲取這個鎖時可能直接省略掉自旋過程。

鎖消除

鎖消除是一更加徹底的優化,指虛擬機即時編譯器在運行時,對一些代碼要求同步,但是對被檢測到不可能存在共享數據競爭的鎖進行消除。

可是,既然不存在競爭,為什么程序員還要加鎖呢?其實有許多同步措施并不是程序員自己加入的,而是一些內置的API,框架加入的。比如下面的代碼:

public String[] concatString() {Vector<String> vector = new Vector<>();for (int j = 0; j < 100; j++) {vector.add(Integer.toString(i));}return vector.toArray(new String[]{}); }

在這段代碼中,變量vector只在concatString()方法中使用,它是局部變量。局部變量是在線程棧上分配的,屬于線程私有的數據,不會被其他線程訪問到。在這種情況下,Vector內部所有加鎖同步都是沒有必要的,這時虛擬機會將這些無用的鎖操作去除。

鎖消除的主要判定依據是逃逸分析,就是觀察某一個變量是否會逃出某一個作用域,如果判斷到一段代碼中,在堆上所有數據都不會逃逸出去被其他線程訪問到,那就可以把他們當做棧上數據對待,認為它們是線程私有的,同步加鎖自然不用進行。

逃逸分析必須在-server模式下進行,可以使用-XX:+DoEscapeAnalysis參數打開逃逸分析。使用-XX:+EliminateLocks參數可以打開鎖消除。

鎖粗化

原則上,我們在編寫代碼的時候,總是推薦將同步塊的作用范圍限制得盡可能小,這樣可以讓同步操作所需的時間盡可能少,即使存在鎖競爭,等待鎖的線程也能盡可能塊地拿到鎖。

大多數情況下,上面的原則是正確的,但是如果一系列的連續操作都對同一個對象反復加鎖和解鎖,甚至加鎖操作是出現在循環體中,那會導致不必要的性能損耗。

如果虛擬機探測到這樣的操作,就會把加鎖同步的返回擴展(粗化)到整個操作序列的外部,這樣只需要加鎖一次就可以了。

總結

以上是生活随笔為你收集整理的synchronized原理_synchronized关键字的作用、原理以及锁优化的全部內容,希望文章能夠幫你解決所遇到的問題。

如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。