天天看點

synchornized核心原理講解

作者:極速星空4DO

前言

在此之前先有幾個面試題,看大家能答對幾題

1.1: 标準通路ab二個線程,是先列印t1還是t2 ???

csharp複制代碼public class SyncUnit {
    
    public synchronized void t1() {
        System.out.println("t1");
    }

    public synchronized void t2() {
        System.out.println("t2");
    }

    public static void main(String[] args) throws Exception{
        SyncUnit syncUnit = new SyncUnit();
        new Thread(() -> {
            syncUnit.t1();
        }).start();

        Thread.sleep(100);

        new Thread(() -> {
           syncUnit.t2();
        }).start();
    }
}
           

1.2: t1方法暫停3秒鐘,是先列印t1還是t2 ???

csharp複制代碼public class SyncUnit {

    public synchronized void t1(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("t1");
    }

    public synchronized void t2() {
        System.out.println("t2");
    }

    public static void main(String[] args) throws Exception{
        SyncUnit syncUnit = new SyncUnit();
        new Thread(() -> {
            syncUnit.t1();
        }).start();

        Thread.sleep(100);

        new Thread(() -> {
           syncUnit.t2();
        }).start();
    }
           

1.3: 新增一個普通方法hello(),是先列印t1還是hello ????

csharp複制代碼public class SyncUnit {

    public synchronized void t1(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("t1");
    }

    public synchronized void t2() {
        System.out.println("t2");
    }

    public void hello() {
        System.out.println("hello");
    }

    public static void main(String[] args) throws Exception{
        SyncUnit syncUnit = new SyncUnit();
        new Thread(() -> {
            syncUnit.t1();
        }).start();

        Thread.sleep(100);

        new Thread(() -> {
           syncUnit.hello();
        }).start();
    }
}
           

1.4: 現在有二個SyncUnit對象,是先列印t1還是t2 ???

java複制代碼public class SyncUnit {

    public synchronized void t1(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("t1");
    }

    public synchronized void t2() {
        System.out.println("t2");
    }

    public static void main(String[] args) throws Exception{
        SyncUnit syncUnit = new SyncUnit();
        SyncUnit syncUnit1 = new SyncUnit();
        new Thread(() -> {
            syncUnit.t1();
        }).start();
        Thread.sleep(100);
        new Thread(() -> {
           syncUnit1.t2();
        }).start();
    }
           

1.5: 二個靜态同步方法,一個SuncUnit對象,是先列印t1還是t2 ????

csharp複制代碼public class SyncUnit {

    public static synchronized void t1(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("t1");
    }

    public static synchronized void t2() {
        System.out.println("t2");
    }

    public static void main(String[] args) throws Exception{
        SyncUnit syncUnit = new SyncUnit();
        new Thread(() -> {
            syncUnit.t1();
        }).start();
        Thread.sleep(100);
        new Thread(() -> {
           syncUnit.t2();
        }).start();
    }
}
           

1.6: 二個靜态同步方法,二個SyncUnit對象,是先列印t1還是t2

java複制代碼public class SyncUnit {

    public static synchronized void t1(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("t1");
    }

    public static synchronized void t2() {
        System.out.println("t2");
    }

    public static void main(String[] args) throws Exception{
        SyncUnit syncUnit = new SyncUnit();
        SyncUnit syncUnit1 = new SyncUnit();
        new Thread(() -> {
            syncUnit.t1();
        }).start();
        Thread.sleep(100);
        new Thread(() -> {
           syncUnit1.t2();
        }).start();
    }
}
           

1.7: 一個靜态同步方法,普通同步方法,一個SyncUnit對象,是先列印t1還是t2

csharp複制代碼public class SyncUnit {

    public static synchronized void t1(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("t1");
    }

    public synchronized void t2() {
        System.out.println("t2");
    }

    public static void main(String[] args) throws Exception{
        SyncUnit syncUnit = new SyncUnit();
        new Thread(() -> {
            syncUnit.t1();
        }).start();
        Thread.sleep(100);
        new Thread(() -> {
           syncUnit.t2();
        }).start();
    }
}
           

1.8 一個靜态同步方法,普通同步方法,二個SyncUnit對象,是先列印t1還是t2

java複制代碼public class SyncUnit {

    public static synchronized void t1(){
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("t1");
    }

    public synchronized void t2() {
        System.out.println("t2");
    }

    public static void main(String[] args) throws Exception{
        SyncUnit syncUnit = new SyncUnit();
        SyncUnit syncUnit1 = new SyncUnit();
        new Thread(() -> {
            syncUnit.t1();
        }).start();
        Thread.sleep(100);
        new Thread(() -> {
           syncUnit1.t2();
        }).start();
    }
}
           

synchronized用法

synchronized是java提供的一種解決多線程并發安全的一種内置鎖,盡管在jdk1.5之前還被大家吐槽性能問題,但是在1.5之後對synchronized不斷的優化,在單機程式中,當設計多線程并發問題時,我們完全可以使用synchronized解決

  • 同步執行個體方法
  • arduino複制代碼
  • public synchronized void method() { //方法邏輯 }
  • 當synchronized修飾的是一個普通方法的時候,相當于對this對象加鎖,一個執行個體是可以建立多個對象的,是以可以擁有多把鎖,就比如下面這個例子,我們建立了二個對象,那就是二把不同的鎖,是以在調用t1()的時候,t2()方法由于是不同的鎖,是以會直接執行方法
js複制代碼public class SyncUnit {

        public synchronized void t1(){
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("t1");
        }

        public synchronized void t2() {
            System.out.println("t2");
        }

        public static void main(String[] args) throws Exception{
            SyncUnit syncUnit = new SyncUnit();
            SyncUnit syncUnit1 = new SyncUnit();
            new Thread(() -> {
                syncUnit.t1();
            }).start();
            Thread.sleep(100);
            new Thread(() -> {
               syncUnit1.t2();
            }).start();
        }
    }
           
  • 同步靜态方法
  • arduino複制代碼
  • public static synchronized void method() { //方法邏輯 }
  • 當synchronized修飾的是一個靜态方法的時候,相當于對目前執行個體加鎖,一個類隻有一個執行個體,是以無論你建立多少個對象,都隻有一把鎖,比如下面這個例子,雖然建立了二個不同的對象,但是實際隻有一把鎖,是以是先列印t1(),然後在列印t2(),因為t2()要等待t1()把鎖釋放掉之後才能擷取到鎖
js複制代碼
   public class SyncUnit {

            public static synchronized void t1(){
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("t1");
            }

            public static synchronized void t2() {
                System.out.println("t2");
            }

            public static void main(String[] args) throws Exception{
                SyncUnit syncUnit = new SyncUnit();
                SyncUnit syncUnit1 = new SyncUnit();
                new Thread(() -> {
                    syncUnit.t1();
                }).start();
                Thread.sleep(100);
                new Thread(() -> {
                   syncUnit1.t2();
                }).start();
            }
       }
           
  • 代碼塊
  • js複制代碼
  • public Object object = new Object(); public void method() { synchronized(object) { //方法邏輯 } }

這時候的object是一個對象,就相當于在普通方法上添加synchronized,如果是不同的對象,那麼就是不同的鎖

js複制代碼      public void method() {
          synchronized(Test.class) {
             //方法邏輯
          }
       }
           

這時候就相當于在靜态方法上添加synchronized,也就是對目前執行個體加鎖,一個類隻有一個執行個體

synchronized核心原理

synchornized是基于JVM中的Monitor鎖實作的,Java1.5版本之前的synchornized鎖性能較低,但是從1.6版本之後,對synchornized進行了大量的優化,引入了鎖粗化,鎖消除,偏向鎖,輕量級鎖,适應性自旋等技術來提升synchornized的性能

  • synchornized修飾的是方法

當synchornized修飾的是方法的時候,目前方法會比普通方法多一個ACC_SYNCHRONIZED的辨別符

synchornized核心原理講解

當JVM執行程式的時候,會判斷這個方法是否有ACC_SYNCHRONIZED這個辨別符,如果有,則目前線程優先擷取Monitor對象,同一個時刻隻能有一個線程擷取到,在目前線程釋放Monitor對象之前,其它線程無法擷取到同一個Monitor對象,進而保證了同一時刻隻能有一個線程進入到被synchornized修飾的方法

  • synchornized修飾的是代碼塊

當synchornized修飾的是代碼塊的時候,synchornized關鍵字會被編譯成monitorenter和monitorexit,使得同一時刻隻能有一個線程進入到同步代碼塊中,但是這裡為什麼會有二個monitorexit,是因為程式正常退出的時候需要釋放鎖,在程式異常的時候也要釋放鎖,是以會對應二個

synchornized核心原理講解

無論synchornized修飾的是方法還是代碼塊,底層都是通過JVM調用作業系統的Mutes鎖實作的,當線程被阻塞時會被挂起,等待CPU重新排程,這會導緻線程在作業系統的使用者态和核心态之間切換,影響性能

Monitor鎖原理

synchornized低成是基于Monitor鎖來實作的,而Monitor鎖是基于作業系統的Mutex鎖實作的,Mutex鎖是作業系統級别的重量級鎖,是以性能較低

在Java中,建立的任何一個對象在JVM中都會關聯一個Monitor對象,是以說任何一個對象都可以成為鎖。

在HotSpot JVM中,Monitor是由ObjectMoitor實作的,在ObjectMonitor對象的資料結構中,有幾個重要的屬性

  • _WaitSet:是一個集合,當線程獲到鎖之後,但是還沒有完成業務邏輯,也還沒釋放鎖,這時候調用了Object類的wait()方法,這時候這個線程就會進入_WaitSet這個集合中等待被喚醒,也就是執行nitify()或者notifyAll()方法喚醒
  • _EntryList:是一個集合,當有多個線程來擷取鎖,這時候隻有一個線程能成功拿到鎖,剩下那些沒有拿到鎖的線程就會進入_EntryList集合中,等待下次搶鎖
  • _Owner:當一個線程擷取到鎖之後,就會将該值設定成目前線程,釋放鎖之後,這個值就會重新被設定成null
  • _count:當一個線程擷取到鎖之後,_count的值就會+1,釋放鎖之後就會-1,隻有當減到0之後,才算真正的釋放掉鎖了,其它線程才能來擷取這把鎖,synchornized可重入鎖也是基于這個值來實作的

是以當多個線程同時通路被synchornized修飾的方法或者代碼塊時候,synchornized加鎖和釋放鎖的底層實作流程大緻為:

  • 1:進入_EntryList集合,當某個線程擷取到鎖之後,這個線程就會進入_Owner區域,就會将Monitor對象的_owner變量複制為目前線程。并把_count值+1
  • 2:當線程調用wait()方法時,目前線程會釋放掉持有的Monitor對象,并把_owner指派成null,_count的值-1,同時這個線程就會進入_WaitSet集合等到被喚醒
  • 3:如果擷取到鎖的線程執行完畢,也會釋放Monitor鎖。,_owner被置為null,_count被置為0

偏向鎖

雖然在程式的方法中或代碼塊中添加了synchornized,但是在大部分的情況下,不會存在多線程競争這種情況,并且會出現同一個線程多次擷取同一把鎖的現象,為了提升這種情況下程式的性能,引入了偏向鎖

輕量級鎖

當多線程競争鎖不激烈時,可以通過CAS機制競争鎖,這就是輕量級鎖,引入輕量級鎖的目的是在多線程競争鎖不激烈時,避免由于使用作業系統層面的Mutex重量級鎖導緻性能低下

重量級鎖

重量級鎖主要是基于作業系統的Mutex鎖實作,重量級鎖的執行效率較低,處于重量級鎖時被阻塞的線程不會消耗CPU資源

鎖更新過程

多個線程在争搶synchornized鎖時,在某些情況下,會由無鎖狀态一步步更新為最終的重量級鎖,整個更新過程大緻包括如下幾個步驟

  • 1:線程在競争synchornized時,JVM首先會檢查鎖對象的Mark Word中偏向鎖的标記位是否為1,鎖标記位是否為01,如果二個條件都滿足,則目前鎖處于偏向鎖狀态
  • 2:争搶synchornized鎖線程檢查鎖對象的Mark Work中存儲的線程ID是否是自己的,如果是自己的線程ID,則表示處于偏向鎖狀态,目前線程可以直接進入方法或者代碼塊
  • 3:如果鎖對象的Mark Word的線程ID不是自己的線程ID,那麼就會通過CAS方式來競争鎖資源,如果擷取到鎖資源了,就将Mark Word中存儲的線程ID修改成自己的線程ID,将偏向鎖的标記設定成1,鎖标記位置設定成01,目前鎖處于偏向鎖狀态
  • 4:如果目前線程通過CAS沒有擷取到鎖資源,則說明有其它線程也在争搶資源,此時會撤銷偏向鎖,更新為輕量級鎖,并将Mark Word的鎖标記為都清空
  • 5:目前線程與其它線程還是會通過CAS方式來競争資源,如果某個線程成功擷取到資源,就會将鎖對象的Mark Word中的鎖标志位設定成00,此時進入輕量級鎖狀态
  • 6:競争失敗的線程還是會通過CAS方式來擷取鎖,但是當CAS達到一定的次數以後,就會更新為重量級鎖了

繼續閱讀