synchronized的加鎖方式與底層原理分析


1.synchronized的三種加鎖方式

  • 對於普通同步方法,鎖是當前實例對象(對象鎖)

    在這種使用方式中,要注意鎖是對象的實例,因為要保證多個線程使用的是同一個實例,否則仍然會有問題。

    比如如下代碼,因為每個線程的實例是不同的,因為他們獲取的都不是同一把鎖

public class Demo1 {
    static int num = 0;

    public synchronized  void  m1(){
        for(int i=0;i<10000;i++){
            num++;
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(()->{Demo1 demo1 = new Demo1();demo1.m1();});
        Thread t2 = new Thread(()->{Demo1 demo1 = new Demo1();demo1.m1();});
        Thread t3 = new Thread(()->{Demo1 demo1 = new Demo1();demo1.m1();});


        t1.start();
        t2.start();
        t3.start();

        t1.join();
        t2.join();
        t3.join();

        System.out.println(num);

    }
}

要想執行結果正確,就必須保證多個線程的實例是相同的,如下所示:

public class Demo1 {
    static int num = 0;

    public synchronized  void  m1(){
        for(int i=0;i<10000;i++){
            num++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        Demo1 demo1 = new Demo1();
        Thread t1 = new Thread(()->demo1.m1());
        Thread t2 = new Thread(()->demo1.m1());
        Thread t3 = new Thread(()->demo1.m1());


        t1.start();
        t2.start();
        t3.start();

        t1.join();
        t2.join();
        t3.join();

        System.out.println(num);

    }
}

  • 對於靜態同步方法,鎖是當前類Class對象(類鎖)
public class Demo1 {
    static int num = 0;

    public synchronized static void  m1(){
        for(int i=0;i<10000;i++){
            num++;
        }
    }

    public static class T1 extends Thread{
        @Override
        public void run() {
            Demo1.m1();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        T1 t1 = new T1();
        T1 t2 = new T1();
        T1 t3 = new T1();


        t1.start();
        t2.start();
        t3.start();

        t1.join();
        t2.join();
        t3.join();

        System.out.println(num);

    }
}

  • 對於同步方法塊,鎖是Synchronized括號里配置的對象

      1.括號里的對象是class對象

public class Demo1 {
    static int num = 0;

    public  void  m1(){
        // class對象鎖
        synchronized (Demo1.class){
            for(int i=0;i<10000;i++){
                num++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(()->{Demo1 demo1 = new Demo1();demo1.m1();});
        Thread t2 = new Thread(()->{Demo1 demo1 = new Demo1();demo1.m1();});
        Thread t3 = new Thread(()->{Demo1 demo1 = new Demo1();demo1.m1();});


        t1.start();
        t2.start();
        t3.start();

        t1.join();
        t2.join();
        t3.join();

        System.out.println(num);

    }
}

      2. 括號里的對象是當前實例

public class Demo1 {
    static int num = 0;

    public  void  m1(){
        // class對象鎖
        synchronized (this){
            for(int i=0;i<10000;i++){
                num++;
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {

        Demo1 demo1 = new Demo1();

        Thread t1 = new Thread(()->demo1.m1());
        Thread t2 = new Thread(()->demo1.m1());
        Thread t3 = new Thread(()->demo1.m1());


        t1.start();
        t2.start();
        t3.start();

        t1.join();
        t2.join();
        t3.join();

        System.out.println(num);

    }
}

2.synchronized的實現原理

      從JVM規范中可以看到Synchonized在JVM里的實現原理,JVM基於進入和退出Monitor對象來實現方法同步和代碼塊同步,但兩者的實現細節不一樣。代碼塊同步是使用monitorentermonitorexit指令實現的,而方法同步是使用另外一種方式實現的,細節在JVM規范里並沒有詳細說明。但是,方法的同步同樣可以使用這兩個指令來實現。

      monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個monitor與之關聯,當且一個monitor被持有后,它將處於鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖。

3.java對象頭

      在JVM中,對象在內存中的布局分為三塊區域:對象頭實例數據對齊填充。如下:

  • 實例變量:存放類的屬性數據信息,包括父類的屬性信息,如果是數組的實例部分還包括數組的長度,這部分內存按4字節對齊。
  • 填充數據:由於虛擬機要求對象起始地址必須是8字節的整數倍。填充數據不是必須存在的,僅僅是為了字節對齊,這點了解即可。
  • 對象頭:synchronized用的鎖是存在Java對象頭里的。如果對象是數組類型,則虛擬機用3個字寬(Word)存儲對象頭,如果對象是非數組類型,則用2字寬存儲對象頭。在32位虛擬機中,1字寬等於4字節,即32bit

      Java對象頭里的Mark Word里默認存儲對象的HashCode、分代年齡和鎖標記位。32位JVM的Mark Word的默認存儲結構如表圖所示:

      在運行期間,Mark Word里存儲的數據會隨着鎖標志位的變化而變化。Mark Word可能變化為存儲以下4種數據

      在64位虛擬機下,Mark Word是64bit大小的,其存儲結構如下圖所示:

4.鎖的三種狀態

1.偏向鎖

      HotSpot的作者經過研究發現,大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程訪問同步塊並獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲着指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖):如果沒有設置,則使用CAS競爭鎖;如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程

2.輕量級鎖

      線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用於存儲鎖記錄的空間並將對象頭中的Mark Word復制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。

3.重量級鎖

      內置鎖在Java中被抽象為監視器鎖(monitor)。在JDK 1.6之前,監視器鎖可以認為直接對應底層操作系統中的互斥量(mutex)。這種同步方式的成本非常高,包括系統調用引起的內核態與用戶態切換、線程阻塞造成的線程切換等。因此,后來稱這種鎖為“重量級鎖”。

鎖的優缺點的對比:

5.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

6.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方法執行結束后才自動釋放鎖。

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

以上內容為自己學習時所記錄的筆記,主要來源於以下資料:
1.java並發編程的藝術
2.http://www.itsoku.com/article/168


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM