Java并發(fā)編程 -- synchronized保證線程安全的原理

文章轉載致博客 http://blog.csdn.net/javazejian/article/details/72828483
自己稍加完善。

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

synchronized的三種應用方式

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

  • 修飾實例方法,作用于當前實例加鎖,進入同步代碼前要獲得當前實例的鎖

  • 修飾靜態(tài)方法,作用于當前類對象加鎖,進入同步代碼前要獲得當前類對象的鎖

  • 修飾代碼塊,指定加鎖對象,對給定對象加鎖,進入同步代碼庫前要獲得給定對象的鎖。

synchronized作用于實例方法

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

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的域值,那么第二個線程就會與第一個線程一起看到同一個值,并執(zhí)行相同值的加1操作,這也就造成了線程安全失敗,因此對于increase方法必須使用synchronized修飾,以便保證線程安全。此時我們應該注意到synchronized修飾的是實例方法increase,在這樣的情況下,當前線程的鎖便是實例對象instance,注意Java中的線程同步鎖可以是任意對象。從代碼執(zhí)行結果來看確實是正確的,倘若我們沒有使用synchronized關鍵字,其最終輸出結果就很可能小于2000000,這便是synchronized關鍵字的作用。這里我們還需要意識到,當一個線程正在訪問一個對象的 synchronized 實例方法,那么其他線程不能訪問該對象的其他 synchronized 方法,畢竟一個對象只有一把鎖,當一個線程獲取了該對象的鎖之后,其他線程無法獲取該對象的鎖,所以無法訪問該對象的其他synchronized實例方法,但是其他線程還是可以訪問該實例對象的其他非synchronized方法,當然如果是一個線程 A 需要訪問實例對象 obj1 的 synchronized 方法 f1(當前對象鎖是obj1),另一個線程 B 需要訪問實例對象 obj2 的 synchronized 方法 f2(當前對象鎖是obj2),這樣是允許的,因為兩個實例對象鎖并不同相同,此時如果兩個線程操作數(shù)據(jù)并非共享的,線程安全是有保障的,遺憾的是如果兩個線程操作的是共享數(shù)據(jù),那么線程安全就有可能無法保證了,如下代碼將演示出該現(xiàn)象

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);
    }
}

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

synchronized作用于靜態(tài)方法

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

public class AccountingSyncClass implements Runnable{
    static int i=0;

    /**
     * 作用于靜態(tài)方法,鎖是當前class對象,也就是
     * AccountingSyncClass類對應的class對象
     */
    public static synchronized void increase(){
        i++;
    }

    /**
     * 非靜態(tài),訪問時鎖不一樣不會發(fā)生互斥
     */
    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關鍵字修飾的是靜態(tài)increase方法,與修飾實例方法不同的是,其鎖對象是當前類的class對象。注意代碼中的increase4Obj方法是實例方法,其對象鎖是當前實例對象,如果別的線程調(diào)用該方法,將不會產(chǎn)生互斥現(xiàn)象,畢竟鎖對象不同,但我們應該意識到這種情況下可能會發(fā)現(xiàn)線程安全問題(操作了共享靜態(tài)變量i)。

synchronized同步代碼塊

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

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實例對象鎖,如果當前有其他線程正持有該對象鎖,那么新到的線程就必須等待,這樣也就保證了每次只有一個線程執(zhí)行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的底層實現(xiàn)原理。

synchronized底層語義原理

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

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

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

反編譯結果:


image.png

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

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.

這段話的大概意思為:

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

  1. 如果monitor的進入數(shù)為0,則該線程進入monitor,然后將進入數(shù)設置為1,該線程即為monitor的所有者。

  2. 如果線程已經(jīng)占有該monitor,只是重新進入,則進入monitor的進入數(shù)加1.

  3. 如果其他線程已經(jīng)占用了monitor,則該線程進入阻塞狀態(tài),直到monitor的進入數(shù)為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.

這段話的大概意思為:

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

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

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

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

源代碼:

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

反編譯結果:
image.png

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

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

synchronized的可重入性

從互斥鎖的設計上來說,當一個線程試圖操作一個由其他線程持有的對象鎖的臨界資源時,將會處于阻塞狀態(tài),但當一個線程再次請求自己持有對象鎖的臨界資源時,這種情況屬于重入鎖,請求將會成功,在java中synchronized是基于原子性的內(nèi)部鎖機制,是可重入的,因此在一個線程調(diào)用synchronized方法的同時在其方法體內(nèi)部調(diào)用該對象另一個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代碼塊執(zhí)行同步代碼,并在代碼塊中調(diào)用了當前實例對象的另外一個synchronized方法,再次請求當前實例鎖時,將被允許,進而執(zhí)行方法體代碼,這就是重入鎖最直接的體現(xiàn),需要特別注意另外一種情況,當子類繼承父類時,子類也是可以通過可重入鎖調(diào)用父類的同步方法。注意由于synchronized是基于monitor實現(xiàn)的,因此每次重入,monitor中的計數(shù)器仍會加1。

線程中斷與synchronized
線程中斷

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

//中斷線程(實例方法)
public void Thread.interrupt();

//判斷線程是否被中斷(實例方法)
public boolean Thread.isInterrupted();

//判斷是否被中斷并清除當前中斷狀態(tài)(靜態(tài)方法)
public static boolean Thread.interrupted();

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

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

        /**
         * 輸出結果:
           Interruted When Sleep
           interrupt:false
         */
    }
}

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

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();

        /**
         * 輸出結果(無限執(zhí)行):
             未被中斷
             未被中斷
             未被中斷
             ......
         */
    }
}

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

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("已跳出循環(huán),線程中斷!");
            }
        };
        t1.start();
        TimeUnit.SECONDS.sleep(2);
        t1.interrupt();

        /**
         * 輸出結果:
            線程中斷
            已跳出循環(huán),線程中斷!
         */
    }
}

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

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

    }
}

中斷與synchronized

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

public class SynchronizedBlocked implements Runnable{

    public synchronized void f() {
        System.out.println("Trying to call f()");
        while(true) // Never releases lock
            Thread.yield();
    }

    /**
     * 在構造器中創(chuàng)建新線程并啟動獲取對象鎖
     */
    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);
        //啟動后調(diào)用f()方法,無法獲取當前實例鎖處于等待狀態(tài)
        t.start();
        TimeUnit.SECONDS.sleep(1);
        //中斷線程,無法生效
        t.interrupt();
    }
}

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

等待喚醒機制與synchronized

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

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

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

Java虛擬機對synchronized的優(yōu)化

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

偏向鎖

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

輕量級鎖

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

自旋鎖

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

鎖消除

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

重量級鎖

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

本篇的主要參考資料:
《Java編程思想》
《深入理解Java虛擬機》
《實戰(zhàn)Java高并發(fā)程序設計》

最后編輯于
?著作權歸作者所有,轉載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容