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

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程语言 > java >内容正文

java

synchronized原理_Java并发编程 -- synchronized保证线程安全的原理

發布時間:2023/12/3 java 35 豆豆
生活随笔 收集整理的這篇文章主要介紹了 synchronized原理_Java并发编程 -- synchronized保证线程安全的原理 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

線程安全是并發編程中的重要關注點,應該注意到的是,造成線程安全問題的主要誘因有兩點,一是存在共享數據(也稱臨界資源),二是存在多條線程共同操作共享數據。因此為了解決這個問題,我們可能需要這樣一個方案,當存在多個線程操作共享數據時,需要保證同一時刻有且只有一個線程在操作共享數據,其他線程必須等到該線程處理完數據后再進行,這種方式有個高尚的名稱叫互斥鎖,即能達到互斥訪問目的的鎖,也就是說當一個共享數據被當前正在訪問的線程加上互斥鎖后,在同一個時刻,其他線程只能處于等待的狀態,直到當前線程處理完畢釋放該鎖。在 Java 中,關鍵字 synchronized可以保證在同一個時刻,只有一個線程可以執行某個方法或者某個代碼塊(主要是對方法或者代碼塊中存在共享數據的操作),同時我們還應該注意到synchronized另外一個重要的作用,synchronized可保證一個線程的變化(主要是共享數據的變化)被其他線程所看到(保證可見性,完全可以替代Volatile功能),這點確實也是很重要的。

synchronized的三種應用方式

synchronized關鍵字最主要有以下3種應用方式,下面分別介紹

  • 修飾實例方法,作用于當前實例加鎖,進入同步代碼前要獲得當前實例的鎖
  • 修飾靜態方法,作用于當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖
  • 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。

synchronized作用于實例方法

所謂的實例對象鎖就是用synchronized修飾實例對象中的實例方法,注意是實例方法不包括靜態方法,如下

public class AccountingSync implements Runnable{ //共享資源(臨界資源) static int i=0; /** * synchronized 修飾實例方法 */ public synchronized void increase(){ i++; } @Override public void run() { for(int j=0;j<1000000;j++){ increase(); } } public static void main(String[] args) throws InterruptedException { AccountingSync instance=new AccountingSync(); Thread t1=new Thread(instance); Thread t2=new Thread(instance); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(i); } /** * 輸出結果: * 2000000 */}

上述代碼中,我們開啟兩個線程操作同一個共享資源即變量i,由于i++;操作并不具備原子性,該操作是先讀取值,然后寫回一個新值,相當于原來的值加上1,分兩步完成,如果第二個線程在第一個線程讀取舊值和寫回新值期間讀取i的域值,那么第二個線程就會與第一個線程一起看到同一個值,并執行相同值的加1操作,這也就造成了線程安全失敗,因此對于increase方法必須使用synchronized修飾,以便保證線程安全。此時我們應該注意到synchronized修飾的是實例方法increase,在這樣的情況下,當前線程的鎖便是實例對象instance,注意Java中的線程同步鎖可以是任意對象。從代碼執行結果來看確實是正確的,倘若我們沒有使用synchronized關鍵字,其最終輸出結果就很可能小于2000000,這便是synchronized關鍵字的作用。這里我們還需要意識到,當一個線程正在訪問一個對象的 synchronized 實例方法,那么其他線程不能訪問該對象的其他 synchronized 方法,畢竟一個對象只有一把鎖,當一個線程獲取了該對象的鎖之后,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized實例方法,但是其他線程還是可以訪問該實例對象的其他非synchronized方法,當然如果是一個線程 A 需要訪問實例對象 obj1 的 synchronized 方法 f1(當前對象鎖是obj1),另一個線程 B 需要訪問實例對象 obj2 的 synchronized 方法 f2(當前對象鎖是obj2),這樣是允許的,因為兩個實例對象鎖并不同相同,此時如果兩個線程操作數據并非共享的,線程安全是有保障的,遺憾的是如果兩個線程操作的是共享數據,那么線程安全就有可能無法保證了,如下代碼將演示出該現象

public class AccountingSyncBad implements Runnable{ static int i=0; public synchronized void increase(){ i++; } @Override public void run() { for(int j=0;j<1000000;j++){ increase(); } } public static void main(String[] args) throws InterruptedException { //new新實例 Thread t1=new Thread(new AccountingSyncBad()); //new新實例 Thread t2=new Thread(new AccountingSyncBad()); t1.start(); t2.start(); //join含義:當前線程A等待thread線程終止之后才能從thread.join()返回 t1.join(); t2.join(); System.out.println(i); }}

上述代碼與前面不同的是我們同時創建了兩個新實例AccountingSyncBad,然后啟動兩個不同的線程對共享變量i進行操作,但很遺憾操作結果是1452317而不是期望結果2000000,因為上述代碼犯了嚴重的錯誤,雖然我們使用synchronized修飾了increase方法,但卻new了兩個不同的實例對象,這也就意味著存在著兩個不同的實例對象鎖,因此t1和t2都會進入各自的對象鎖,也就是說t1和t2線程使用的是不同的鎖,因此線程安全是無法保證的。解決這種困境的的方式是將synchronized作用于靜態的increase方法,這樣的話,對象鎖就當前類對象,由于無論創建多少個實例對象,但對于的類對象擁有只有一個,所有在這樣的情況下對象鎖就是唯一的。下面我們看看如何使用將synchronized作用于靜態的increase方法。

synchronized作用于靜態方法

當synchronized作用于靜態方法時,其鎖就是當前類的class對象鎖。由于靜態成員不專屬于任何一個實例對象,是類成員,因此通過class對象鎖可以控制靜態 成員的并發操作。需要注意的是如果一個線程A調用一個實例對象的非static synchronized方法,而線程B需要調用這個實例對象所屬類的靜態 synchronized方法,是允許的,不會發生互斥現象,因為訪問靜態 synchronized 方法占用的鎖是當前類的class對象,而訪問非靜態 synchronized 方法占用的鎖是當前實例對象鎖,看如下代碼

public class AccountingSyncClass implements Runnable{ static int i=0; /** * 作用于靜態方法,鎖是當前class對象,也就是 * AccountingSyncClass類對應的class對象 */ public static synchronized void increase(){ i++; } /** * 非靜態,訪問時鎖不一樣不會發生互斥 */ public synchronized void increase4Obj(){ i++; } @Override public void run() { for(int j=0;j<1000000;j++){ increase(); } } public static void main(String[] args) throws InterruptedException { //new新實例 Thread t1=new Thread(new AccountingSyncClass()); //new心事了 Thread t2=new Thread(new AccountingSyncClass()); //啟動線程 t1.start();t2.start(); t1.join();t2.join(); System.out.println(i); }}

由于synchronized關鍵字修飾的是靜態increase方法,與修飾實例方法不同的是,其鎖對象是當前類的class對象。注意代碼中的increase4Obj方法是實例方法,其對象鎖是當前實例對象,如果別的線程調用該方法,將不會產生互斥現象,畢竟鎖對象不同,但我們應該意識到這種情況下可能會發現線程安全問題(操作了共享靜態變量i)。

synchronized同步代碼塊

除了使用關鍵字修飾實例方法和靜態方法外,還可以使用同步代碼塊,在某些情況下,我們編寫的方法體可能比較大,同時存在一些比較耗時的操作,而需要同步的代碼又只有一小部分,如果直接對整個方法進行同步操作,可能會得不償失,此時我們可以使用同步代碼塊的方式對需要同步的代碼進行包裹,這樣就無需對整個方法進行同步操作了,同步代碼塊的使用示例如下:

public class AccountingSync implements Runnable{ static AccountingSync instance=new AccountingSync(); static int i=0; @Override public void run() { //省略其他耗時操作.... //使用同步代碼塊對變量i進行同步操作,鎖對象為instance synchronized(instance){ for(int j=0;j<1000000;j++){ i++; } } } public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(instance); Thread t2=new Thread(instance); t1.start();t2.start(); t1.join();t2.join(); System.out.println(i); }}

從代碼看出,將synchronized作用于一個給定的實例對象instance,即當前實例對象就是鎖對象,每次當線程進入synchronized包裹的代碼塊時就會要求當前線程持有instance實例對象鎖,如果當前有其他線程正持有該對象鎖,那么新到的線程就必須等待,這樣也就保證了每次只有一個線程執行i++;操作。當然除了instance作為對象外,我們還可以使用this對象(代表當前實例)或者當前類的class對象作為鎖,如下代碼:

//this,當前實例對象鎖synchronized(this){ for(int j=0;j<1000000;j++){ i++; }}//class對象鎖synchronized(AccountingSync.class){ for(int j=0;j<1000000;j++){ i++; }}

了解完synchronized的基本含義及其使用方式后,下面我們將進一步深入理解synchronized的底層實現原理。

synchronized底層語義原理

Java 虛擬機中的同步(Synchronization)基于進入和退出管程(Monitor)對象實現, 無論是顯式同步(有明確的 monitorenter 和 monitorexit 指令,即同步代碼塊)還是隱式同步都是如此。在 Java 語言中,同步用的最多的地方可能是被 synchronized 修飾的同步方法。同步方法 并不是由 monitorenter 和 monitorexit 指令來實現同步的,而是由方法調用指令讀取運行時常量池中方法的 ACC_SYNCHRONIZED 標志來隱式實現的,關于這點,稍后詳細分析。下面先來了解一個概念Java對象頭,這對深入理解synchronized實現原理非常關鍵。

如果對上面的執行結果還有疑問,也先不用急,我們先來了解Synchronized的原理,再回頭上面的問題就一目了然了。我們先通過反編譯下面的代碼來看看Synchronized是如何實現對代碼塊進行同步的:

public class SynchronizedDemo { public void method() { synchronized (this) { System.out.println("Method 1 start"); } } }

反編譯結果:

image.png

關于這兩條指令的作用,我們直接參考JVM規范中描述:

monitorenter :

Each object is associated with a monitor. A monitor is locked if and only if it has an owner. The thread that executes monitorenter attempts to gain ownership of the monitor associated with objectref, as follows:? If the entry count of the monitor associated with objectref is zero, the thread enters the monitor and sets its entry count to one. The thread is then the owner of the monitor.? If the thread already owns the monitor associated with objectref, it reenters the monitor, incrementing its entry count.? If another thread already owns the monitor associated with objectref, the thread blocks until the monitor's entry count is zero, then tries again to gain ownership.

這段話的大概意思為:

每個對象有一個監視器鎖(monitor)。當monitor被占用時就會處于鎖定狀態,線程執行monitorenter指令時嘗試獲取monitor的所有權,過程如下:

  • 如果monitor的進入數為0,則該線程進入monitor,然后將進入數設置為1,該線程即為monitor的所有者。
  • 如果線程已經占有該monitor,只是重新進入,則進入monitor的進入數加1.
  • 如果其他線程已經占用了monitor,則該線程進入阻塞狀態,直到monitor的進入數為0,再重新嘗試獲取monitor的所有權。
  • monitorexit:

    The thread that executes monitorexit must be the owner of the monitor associated with the instance referenced by objectref.The thread decrements the entry count of the monitor associated with objectref. If as a result the value of the entry count is zero, the thread exits the monitor and is no longer its owner. Other threads that are blocking to enter the monitor are allowed to attempt to do so.

    這段話的大概意思為:

    執行monitorexit的線程必須是objectref所對應的monitor的所有者。

    指令執行時,monitor的進入數減1,如果減1后進入數為0,那線程退出monitor,不再是這個monitor的所有者。其他被這個monitor阻塞的線程可以嘗試去獲取這個 monitor 的所有權。

    通過這兩段描述,我們應該能很清楚的看出Synchronized的實現原理,Synchronized的語義底層是通過一個monitor的對象來完成,其實wait/notify等方法也依賴于monitor對象,這就是為什么只有在同步的塊或者方法中才能調用wait/notify等方法,否則會拋出java.lang.IllegalMonitorStateException的異常的原因。

    我們再來看一下同步方法的反編譯結果:

    源代碼:

    public class SynchronizedMethod { public synchronized void method() { System.out.println("Hello World!"); } }

    反編譯結果:

    image.png

    從反編譯的結果來看,方法的同步并沒有通過指令monitorenter和monitorexit來完成(理論上其實也可以通過這兩條指令來實現),不過相對于普通方法,其常量池中多了ACC_SYNCHRONIZED標示符。JVM就是根據該標示符來實現方法的同步的:當方法調用時,調用指令將會檢查方法的 ACC_SYNCHRONIZED 訪問標志是否被設置,如果設置了,執行線程將先獲取monitor,獲取成功之后才能執行方法體,方法執行完后再釋放monitor。在方法執行期間,其他任何線程都無法再獲得同一個monitor對象。 其實本質上沒有區別,只是方法的同步是一種隱式的方式來實現,無需通過字節碼來完成。

    關于synchronized 可能需要了解的關鍵點

    synchronized的可重入性

    從互斥鎖的設計上來說,當一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時,將會處于阻塞狀態,但當一個線程再次請求自己持有對象鎖的臨界資源時,這種情況屬于重入鎖,請求將會成功,在java中synchronized是基于原子性的內部鎖機制,是可重入的,因此在一個線程調用synchronized方法的同時在其方法體內部調用該對象另一個synchronized方法,也就是說一個線程得到一個對象鎖后再次請求該對象鎖,是允許的,這就是synchronized的可重入性。如下:

    public class AccountingSync implements Runnable{ static AccountingSync instance=new AccountingSync(); static int i=0; static int j=0; @Override public void run() { for(int j=0;j<1000000;j++){ //this,當前實例對象鎖 synchronized(this){ i++; increase();//synchronized的可重入性 } } } public synchronized void increase(){ j++; } public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(instance); Thread t2=new Thread(instance); t1.start();t2.start(); t1.join();t2.join(); System.out.println(i); }}

    正如代碼所演示的,在獲取當前實例對象鎖后進入synchronized代碼塊執行同步代碼,并在代碼塊中調用了當前實例對象的另外一個synchronized方法,再次請求當前實例鎖時,將被允許,進而執行方法體代碼,這就是重入鎖最直接的體現,需要特別注意另外一種情況,當子類繼承父類時,子類也是可以通過可重入鎖調用父類的同步方法。注意由于synchronized是基于monitor實現的,因此每次重入,monitor中的計數器仍會加1。

    線程中斷與synchronized

    線程中斷

    正如中斷二字所表達的意義,在線程運行(run方法)中間打斷它,在Java中,提供了以下3個有關線程中斷的方法

    //中斷線程(實例方法)public void Thread.interrupt();//判斷線程是否被中斷(實例方法)public boolean Thread.isInterrupted();//判斷是否被中斷并清除當前中斷狀態(靜態方法)public static boolean Thread.interrupted();

    當一個線程處于被阻塞狀態或者試圖執行一個阻塞操作時,使用Thread.interrupt()方式中斷該線程,注意此時將會拋出一個InterruptedException的異常,同時中斷狀態將會被復位(由中斷狀態改為非中斷狀態),如下代碼將演示該過程:

    public class InterruputSleepThread3 { public static void main(String[] args) throws InterruptedException { Thread t1 = new Thread() { @Override public void run() { //while在try中,通過異常中斷就可以退出run循環 try { while (true) { //當前線程處于阻塞狀態,異常必須捕捉處理,無法往外拋出 TimeUnit.SECONDS.sleep(2); } } catch (InterruptedException e) { System.out.println("Interruted When Sleep"); boolean interrupt = this.isInterrupted(); //中斷狀態被復位 System.out.println("interrupt:"+interrupt); } } }; t1.start(); TimeUnit.SECONDS.sleep(2); //中斷處于阻塞狀態的線程 t1.interrupt(); /** * 輸出結果: Interruted When Sleep interrupt:false */ }}

    如上述代碼所示,我們創建一個線程,并在線程中調用了sleep方法從而使用線程進入阻塞狀態,啟動線程后,調用線程實例對象的interrupt方法中斷阻塞異常,并拋出InterruptedException異常,此時中斷狀態也將被復位。這里有些人可能會詫異,為什么不用Thread.sleep(2000);而是用TimeUnit.SECONDS.sleep(2);其實原因很簡單,前者使用時并沒有明確的單位說明,而后者非常明確表達秒的單位,事實上后者的內部實現最終還是調用了Thread.sleep(2000);,但為了編寫的代碼語義更清晰,建議使用TimeUnit.SECONDS.sleep(2);的方式,注意TimeUnit是個枚舉類型。ok~,除了阻塞中斷的情景,我們還可能會遇到處于運行期且非阻塞的狀態的線程,這種情況下,直接調用Thread.interrupt()中斷線程是不會得到任響應的,如下代碼,將無法中斷非阻塞狀態下的線程:

    public class InterruputThread { public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(){ @Override public void run(){ while(true){ System.out.println("未被中斷"); } } }; t1.start(); TimeUnit.SECONDS.sleep(2); t1.interrupt(); /** * 輸出結果(無限執行): 未被中斷 未被中斷 未被中斷 ...... */ }}

    雖然我們調用了interrupt方法,但線程t1并未被中斷,因為處于非阻塞狀態的線程需要我們手動進行中斷檢測并結束程序,改進后代碼如下:

    public class InterruputThread { public static void main(String[] args) throws InterruptedException { Thread t1=new Thread(){ @Override public void run(){ while(true){ //判斷當前線程是否被中斷 if (this.isInterrupted()){ System.out.println("線程中斷"); break; } } System.out.println("已跳出循環,線程中斷!"); } }; t1.start(); TimeUnit.SECONDS.sleep(2); t1.interrupt(); /** * 輸出結果: 線程中斷 已跳出循環,線程中斷! */ }}

    是的,我們在代碼中使用了實例方法isInterrupted判斷線程是否已被中斷,如果被中斷將跳出循環以此結束線程。綜合所述,可以簡單總結一下中斷兩種情況,一種是當線程處于阻塞狀態或者試圖執行一個阻塞操作時,我們可以使用實例方法interrupt()進行線程中斷,執行中斷操作后將會拋出interruptException異常(該異常必須捕捉無法向外拋出)并將中斷狀態復位,另外一種是當線程處于運行狀態時,我們也可調用實例方法interrupt()進行線程中斷,但同時必須手動判斷中斷狀態,并編寫中斷線程的代碼(其實就是結束run方法體的代碼)。有時我們在編碼時可能需要兼顧以上兩種情況,那么就可以如下編寫:

    public void run(){ try { //判斷當前線程是否已中斷,注意interrupted方法是靜態的,執行后會對中斷狀態進行復位 while (!Thread.interrupted()) { TimeUnit.SECONDS.sleep(2); } } catch (InterruptedException e) { }}

    中斷與synchronized

    事實上線程的中斷操作對于正在等待獲取的鎖對象的synchronized方法或者代碼塊并不起作用,也就是對于synchronized來說,如果一個線程在等待鎖,那么結果只有兩種,要么它獲得這把鎖繼續執行,要么它就保存等待,即使調用中斷線程的方法,也不會生效。演示代碼如下

    public class SynchronizedBlocked implements Runnable{ public synchronized void f() { System.out.println("Trying to call f()"); while(true) // Never releases lock Thread.yield(); } /** * 在構造器中創建新線程并啟動獲取對象鎖 */ public SynchronizedBlocked() { //該線程已持有當前實例鎖 new Thread() { public void run() { f(); // Lock acquired by this thread } }.start(); } public void run() { //中斷判斷 while (true) { if (Thread.interrupted()) { System.out.println("中斷線程!!"); break; } else { f(); } } } public static void main(String[] args) throws InterruptedException { SynchronizedBlocked sync = new SynchronizedBlocked(); Thread t = new Thread(sync); //啟動后調用f()方法,無法獲取當前實例鎖處于等待狀態 t.start(); TimeUnit.SECONDS.sleep(1); //中斷線程,無法生效 t.interrupt(); }}

    我們在SynchronizedBlocked構造函數中創建一個新線程并啟動獲取調用f()獲取到當前實例鎖,由于SynchronizedBlocked自身也是線程,啟動后在其run方法中也調用了f(),但由于對象鎖被其他線程占用,導致t線程只能等到鎖,此時我們調用了t.interrupt();但并不能中斷線程。

    等待喚醒機制與synchronized

    所謂等待喚醒機制本篇主要指的是notify/notifyAll和wait方法,在使用這3個方法時,必須處于synchronized代碼塊或者synchronized方法中,否則就會拋出IllegalMonitorStateException異常,這是因為調用這幾個方法前必須拿到當前對象的監視器monitor對象,也就是說notify/notifyAll和wait方法依賴于monitor對象,在前面的分析中,我們知道monitor 存在于對象頭的Mark Word 中(存儲monitor引用指針),而synchronized關鍵字可以獲取 monitor ,這也就是為什么notify/notifyAll和wait方法必須在synchronized代碼塊或者synchronized方法調用的原因。

    synchronized (obj) { obj.wait(); obj.notify(); obj.notifyAll(); }

    需要特別理解的一點是,與sleep方法不同的是wait方法調用完成后,線程將被暫停,但wait方法將會釋放當前持有的監視器鎖(monitor),直到有線程調用notify/notifyAll方法后方能繼續執行,而sleep方法只讓線程休眠并不釋放鎖。同時notify/notifyAll方法調用后,并不會馬上釋放監視器鎖,而是在相應的synchronized(){}/synchronized方法執行結束后才自動釋放鎖。

    Java虛擬機對synchronized的優化

    鎖的狀態總共有四種,無鎖狀態、偏向鎖、輕量級鎖和重量級鎖。隨著鎖的競爭,鎖可以從偏向鎖升級到輕量級鎖,再升級的重量級鎖,但是鎖的升級是單向的,也就是說只能從低到高升級,不會出現鎖的降級,關于重量級鎖,前面我們已詳細分析過,下面我們將介紹偏向鎖和輕量級鎖以及JVM的其他優化手段,這里并不打算深入到每個鎖的實現和轉換過程更多地是闡述Java虛擬機所提供的每個鎖的核心優化思想,畢竟涉及到具體過程比較繁瑣,如需了解詳細過程可以查閱《深入理解Java虛擬機原理》。

    偏向鎖

    偏向鎖是Java 6之后加入的新鎖,它是一種針對加鎖操作的優化手段,經過研究發現,在大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,因此為了減少同一線程獲取鎖(會涉及到一些CAS操作,耗時)的代價而引入偏向鎖。偏向鎖的核心思想是,如果一個線程獲得了鎖,那么鎖就進入偏向模式,此時Mark Word 的結構也變為偏向鎖結構,當這個線程再次請求鎖時,無需再做任何同步操作,即獲取鎖的過程,這樣就省去了大量有關鎖申請的操作,從而也就提供程序的性能。所以,對于沒有鎖競爭的場合,偏向鎖有很好的優化效果,畢竟極有可能連續多次是同一個線程申請相同的鎖。但是對于鎖競爭比較激烈的場合,偏向鎖就失效了,因為這樣場合極有可能每次申請鎖的線程都是不相同的,因此這種場合下不應該使用偏向鎖,否則會得不償失,需要注意的是,偏向鎖失敗后,并不會立即膨脹為重量級鎖,而是先升級為輕量級鎖。下面我們接著了解輕量級鎖。

    輕量級鎖

    倘若偏向鎖失敗,虛擬機并不會立即升級為重量級鎖,它還會嘗試使用一種稱為輕量級鎖的優化手段(1.6之后加入的),此時Mark Word 的結構也變為輕量級鎖的結構。輕量級鎖能夠提升程序性能的依據是“對絕大部分的鎖,在整個同步周期內都不存在競爭”,注意這是經驗數據。需要了解的是,輕量級鎖所適應的場景是線程交替執行同步塊的場合,如果存在同一時間訪問同一鎖的場合,就會導致輕量級鎖膨脹為重量級鎖。

    自旋鎖

    輕量級鎖失敗后,虛擬機為了避免線程真實地在操作系統層面掛起,還會進行一項稱為自旋鎖的優化手段。這是基于在大多數情況下,線程持有鎖的時間都不會太長,如果直接掛起操作系統層面的線程可能會得不償失,畢竟操作系統實現線程之間的切換時需要從用戶態轉換到核心態,這個狀態之間的轉換需要相對比較長的時間,時間成本相對較高,因此自旋鎖會假設在不久將來,當前的線程可以獲得鎖,因此虛擬機會讓當前想要獲取鎖的線程做幾個空循環(這也是稱為自旋的原因),一般不會太久,可能是50個循環或100循環,在經過若干次循環后,如果得到鎖,就順利進入臨界區。如果還不能獲得鎖,那就會將線程在操作系統層面掛起,這就是自旋鎖的優化方式,這種方式確實也是可以提升效率的。最后沒辦法也就只能升級為重量級鎖了。

    鎖消除

    消除鎖是虛擬機另外一種鎖的優化,這種優化更徹底,Java虛擬機在JIT編譯時(可以簡單理解為當某段代碼即將第一次被執行時進行編譯,又稱即時編譯),通過對運行上下文的掃描,去除不可能存在共享資源競爭的鎖,通過這種方式消除沒有必要的鎖,可以節省毫無意義的請求鎖時間,如下StringBuffer的append是一個同步方法,但是在add方法中的StringBuffer屬于一個局部變量,并且不會被其他線程所使用,因此StringBuffer不可能存在共享資源競爭的情景,JVM會自動將其鎖消除。

    重量級鎖

    即synchronized,一直等待線程施放鎖后才可以拿到資源。

    本篇的主要參考資料:

    《Java編程思想》

    《深入理解Java虛擬機》

    《實戰Java高并發程序設計》

    總結

    以上是生活随笔為你收集整理的synchronized原理_Java并发编程 -- synchronized保证线程安全的原理的全部內容,希望文章能夠幫你解決所遇到的問題。

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