Synchronized 實現原理

Java的鎖

鎖的內存語義

  1. 鎖可讓臨界區互斥執行,還可讓釋放鎖的線程向同一個鎖的線程發送消息
  2. 鎖的釋放要遵循Happens-before原則(鎖規則:解鎖必然發生在隨後的加鎖以前)
  3. 鎖在Java中的具體表現是 Synchronized 和 Lock

鎖的釋放

線程A釋放鎖後,會將共享變動操做刷新到主內存中java

鎖的獲取

線程B獲取鎖時,JMM會將該線程的本地內存置爲無效,被監視器保護的臨界區代碼必須從主內存中讀取共享變量數組

鎖的釋放與獲取

  1. 鎖獲取與volatile讀有相同的內存語義
  2. 線程A釋放一個鎖,實質是線程A告知下一個獲取到該鎖的某個線程其已變動該共享變量
  3. 線程B獲取一個鎖,實質是線程B獲得了線程A告知其(在釋放鎖以前)變動共享變量的消息線程
  4. A釋放鎖,隨後線程B競爭到該鎖,實質是線程A經過主內存向線程B發消息告知其變動了共享變量

Synchronized的綜述

  1. 同步機制: synchronized是Java同步機制的一種實現,即互斥鎖機制,它所得到的鎖叫作互斥鎖
  2. 互斥鎖: 指的是每一個對象的鎖一次只能分配給一個線程,同一時間只能由一個線程佔用
  3. 做用: synchronized用於保證同一時刻只能由一個線程進入到臨界區,同時保證共享變量的可見性、原子性和有序性
  4. 使用: 當一個線程試圖訪問同步代碼方法(塊)時,它首先必須獲得鎖,退出或拋出異常時必須釋放鎖

Synchronized的使用

Synchronized的三種應用方式

使用同步代碼塊的好處在於其餘線程仍能夠訪問非synchronized(this)的同步代碼塊

Synchronized的使用規則

/** * 先定義一個測試模板類 * 這裏補充一個知識點:Thread.sleep(long)不會釋放鎖 * 讀者可參見筆者的`併發番@Thread一文通` */ 
public class SynchronizedDemo {
    public static synchronized void staticMethod(){
        System.out.println(Thread.currentThread().getName() + "訪問了靜態同步方法staticMethod");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "結束訪問靜態同步方法staticMethod");
    }
    public static void staticMethod2(){
        System.out.println(Thread.currentThread().getName() + "訪問了靜態同步方法staticMethod2");
        synchronized (SynchronizedDemo.class){
            System.out.println(Thread.currentThread().getName() + "在staticMethod2方法中獲取了SynchronizedDemo.class");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public synchronized void synMethod(){
        System.out.println(Thread.currentThread().getName() + "訪問了同步方法synMethod");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "結束訪問同步方法synMethod");
    }
    public synchronized void synMethod2(){
        System.out.println(Thread.currentThread().getName() + "訪問了同步方法synMethod2");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "結束訪問同步方法synMethod2");
    }
    public void method(){
        System.out.println(Thread.currentThread().getName() + "訪問了普通方法method");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "結束訪問普通方法method");
    }
    private Object lock = new Object();
    public void chunkMethod(){
        System.out.println(Thread.currentThread().getName() + "訪問了chunkMethod方法");
        synchronized (lock){
            System.out.println(Thread.currentThread().getName() + "在chunkMethod方法中獲取了lock");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void chunkMethod2(){
        System.out.println(Thread.currentThread().getName() + "訪問了chunkMethod2方法");
        synchronized (lock){
            System.out.println(Thread.currentThread().getName() + "在chunkMethod2方法中獲取了lock");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void chunkMethod3(){
        System.out.println(Thread.currentThread().getName() + "訪問了chunkMethod3方法");
        //同步代碼塊
        synchronized (this){
            System.out.println(Thread.currentThread().getName() + "在chunkMethod3方法中獲取了this");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    public void stringMethod(String lock){
        synchronized (lock){
            while (true){
                System.out.println(Thread.currentThread().getName());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}
複製代碼

普通方法與同步方法調用互不關聯

當一個線程進入同步方法時,其餘線程能夠正常訪問其餘非同步方法緩存

public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        //調用普通方法
        synDemo.method();
    });
    Thread thread2 = new Thread(() -> {
        //調用同步方法
        synDemo.synMethod();
    });
    thread1.start();
    thread2.start();
}

複製代碼

輸出:安全

Thread-1訪問了同步方法synMethod
    Thread-0訪問了普通方法method
    Thread-0結束訪問普通方法method
    Thread-1結束訪問同步方法synMethod
複製代碼

分析:經過結果可知,普通方法和同步方法是非阻塞執行的數據結構

全部同步方法只能被一個線程訪問

當一個線程執行同步方法時,其餘線程不能訪問任何同步方法多線程

public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        synDemo.synMethod();
        synDemo.synMethod2();
    });
    Thread thread2 = new Thread(() -> {
        synDemo.synMethod2();
        synDemo.synMethod();
    });
    thread1.start();
    thread2.start();
}
複製代碼

輸出:併發

Thread-0訪問了同步方法synMethod
    Thread-0結束訪問同步方法synMethod
    Thread-0訪問了同步方法synMethod2
    Thread-0結束訪問同步方法synMethod2
    Thread-1訪問了同步方法synMethod2
    Thread-1結束訪問同步方法synMethod2
    Thread-1訪問了同步方法synMethod
    Thread-1結束訪問同步方法synMethod
複製代碼

分析:經過結果可知,任務的執行是阻塞的,顯然Thread-1必須等待Thread-0執行完畢以後才能繼續執行app

同一個鎖的同步代碼塊同一時刻只能被一個線程訪問

當同步代碼塊都是同一個鎖時,方法能夠被全部線程訪問,但同一個鎖的同步代碼塊同一時刻只能被一個線程訪問ide

public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        //調用同步塊方法
        synDemo.chunkMethod();
        synDemo.chunkMethod2();
    });
    Thread thread2 = new Thread(() -> {
        //調用同步塊方法
        synDemo.chunkMethod();
        synDemo.synMethod2();
    });
    thread1.start();
    thread2.start();
}
複製代碼

輸出:工具

Thread-0訪問了chunkMethod方法
Thread-1訪問了chunkMethod方法
Thread-0在chunkMethod方法中獲取了lock  
...停頓等待...
Thread-1在chunkMethod方法中獲取了lock
...停頓等待...
Thread-0訪問了chunkMethod2方法
Thread-0在chunkMethod2方法中獲取了lock
...停頓等待...
Thread-1訪問了chunkMethod2方法
Thread-1在chunkMethod2方法中獲取了lock
複製代碼

分析可知:

  1. 即便普通方法有同步代碼塊,但方法的訪問是非阻塞的,任何線程均可以自由進入
  2. 對於同一個鎖的同步代碼塊的訪問必定是阻塞的

線程間同時訪問同一個鎖的多個同步代碼的執行順序不定

線程間同時訪問同一個鎖多個同步代碼的執行順序不定,即便是使用同一個對象鎖

public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        //調用同步塊方法
        synDemo.chunkMethod();
        synDemo.chunkMethod2();
    });
    Thread thread2 = new Thread(() -> {
        //調用同步塊方法
        synDemo.chunkMethod2();
        synDemo.chunkMethod();
    });
    thread1.start();
    thread2.start();
}
---------------------
//輸出:
Thread-0訪問了chunkMethod方法
Thread-1訪問了chunkMethod2方法
Thread-0在chunkMethod方法中獲取了lock
...停頓等待...
Thread-0訪問了chunkMethod2方法
Thread-1在chunkMethod2方法中獲取了lock
...停頓等待...
Thread-1訪問了chunkMethod方法
Thread-0在chunkMethod2方法中獲取了lock
...停頓等待...
Thread-1在chunkMethod方法中獲取了lock

//分析可知:
//現象:對比20行、22行和24行、25行可知,雖然是同一個lock對象,但其不一樣代碼塊的訪問是非阻塞的
//緣由:根源在於鎖的釋放和從新競爭,當Thread-0訪問完chunkMethod方法後會先釋放鎖,這時Thread-1就有機會能獲取到鎖從而優先執行,依次類推到24行、25行時,Thread-0又從新獲取到鎖優先執行了
//注意:但有一點是必須的,對於同一個鎖的同步代碼塊的訪問必定是阻塞的
//補充:同步方法之因此會被所有阻塞,是由於synDemo對象一直被線程在內部把持住就沒釋放過
複製代碼

不一樣鎖之間訪問非阻塞

因爲三種使用方式的鎖對象都不同,所以相互之間不會有任何影響但有兩種狀況除外:

  1. 當同步代碼塊使用的Class對象和類對象一致時屬於同一個鎖
  2. 當同步代碼塊使用的是this,即與同步方法使用鎖屬於同一個鎖
public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> synDemo.chunkMethod() );
    Thread thread2 = new Thread(() -> synDemo.chunkMethod3());
    Thread thread3 = new Thread(() -> staticMethod());
    Thread thread4 = new Thread(() -> staticMethod2());
    thread1.start();
    thread2.start();
    thread3.start();
    thread4.start();
}
---------------------
//輸出:
Thread-1訪問了chunkMethod3方法
Thread-1在chunkMethod3方法中獲取了this
Thread-2訪問了靜態同步方法staticMethod
Thread-0訪問了chunkMethod方法
Thread-0在chunkMethod方法中獲取了lock
Thread-3訪問了靜態同步方法staticMethod2
...停頓等待...
Thread-2結束訪問靜態同步方法staticMethod
Thread-3在staticMethod2方法中獲取了SynchronizedDemo.class
//分析可知:
//現象:雖然是同一個lock對象,但其不一樣代碼塊的訪問是非阻塞的
//緣由:根源在於鎖的釋放和從新競爭,當Thread-0訪問完chunkMethod方法後會先釋放鎖,這時Thread-1就有機會能獲取到鎖從而優先執行,,Thread-0又從新獲取到鎖優先執行了
複製代碼

Synchronized的可重入性

重入鎖:當一個線程再次請求本身持有對象鎖的臨界資源時,這種狀況屬於重入鎖,請求將會成功實現:一個線程獲得一個對象鎖後再次請求該對象鎖,是容許的,每重入一次,monitor進入次數+1

public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> {
        synDemo.synMethod();
        synDemo.synMethod2();
    });
    Thread thread2 = new Thread(() -> {
        synDemo.synMethod2();
        synDemo.synMethod();
    });
    thread1.start();
    thread2.start();
}
---------------------
//輸出:
Thread-0訪問了同步方法synMethod
Thread-0結束訪問同步方法synMethod
Thread-0訪問了同步方法synMethod2
Thread-0結束訪問同步方法synMethod2
Thread-1訪問了同步方法synMethod2
Thread-1結束訪問同步方法synMethod2
Thread-1訪問了同步方法synMethod
Thread-1結束訪問同步方法synMethod
//分析:在代碼塊中繼續調用了當前實例對象的另一個同步方法,再次請求當前實例鎖時,將被容許,進而執行方法體代碼,這就是重入鎖最直接的體現
複製代碼

Synchronized與String鎖

隱患:因爲在JVM中具備String常量池緩存的功能,所以相同字面量是同一個鎖!!!注意:嚴重不推薦將String做爲鎖對象,而應該改用其餘非緩存對象提示:對字面量有疑問的話請先回顧一下String的基礎

public static void main(String[] args) {
    SynchronizedDemo synDemo = new SynchronizedDemo();
    Thread thread1 = new Thread(() -> synDemo.stringMethod("sally"));
    Thread thread2 = new Thread(() -> synDemo.stringMethod("sally"));
    thread1.start();
    thread2.start();
}
---------------------
//輸出:
Thread-0
Thread-0
Thread-0
Thread-0
...死循環...
//分析:輸出結果永遠都是Thread-0的死循環,也就是說另外一個線程,即Thread-1線程根本不會運行
//緣由:同步塊中的鎖是同一個字面量
複製代碼

Synchronized與不可變鎖

隱患:當使用不可變類對象(finalClass)做爲對象鎖時,使用synchronized一樣會有併發問題緣由:因爲不可變特性,看成爲鎖但同步塊內部仍然有計算操做,會生成一個新的鎖對象注意:嚴重不推薦將final Class做爲鎖對象時仍對其有計算操做補充:雖然String也是final Class,但它的緣由倒是字面量常量池

public class SynchronizedDemo {
    static Integer i = 0;   //Integer是final Class
    public static void main(String[] args) throws InterruptedException {
        Runnable runnable = new Runnable() {
            @Override
            public void run() {
                for (int j = 0;j<10000;j++){
                    synchronized (i){
                        i++;
                    }
                }
            }
        };
        Thread thread1 = new Thread(runnable);
        Thread thread2 = new Thread(runnable);
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(i);
    }
}
---------------------
//輸出:
14134
//分析:跟預想中的20000不一致,當使用Integer做爲對象鎖時但還有計算操做就會出現併發問題
複製代碼咱們經過反編譯發現執行i++操做至關於執行了i = Integer.valueOf(i.intValue()+1)經過查看Integer的valueOf方法實現可知,其每次都new了一個新的Integer對象,鎖變了有木有!!!
複製代碼
public static Integer valueOf(int i) {
    if (i >= IntegerCache.low && i <= IntegerCache.high)
        return IntegerCache.cache[i + (-IntegerCache.low)];
    return new Integer(i);  //每次都new一個新的鎖有木有!!!
}
複製代碼

Synchronized與死鎖

死鎖:當線程間須要相互等待對方已持有的鎖時,就造成死鎖,進而產生死循環

public static void main(String[] args) {
    Object lock = new Object();
    Object lock2 = new Object();
    Thread thread1 = new Thread(() -> {
        synchronized (lock){
            System.out.println(Thread.currentThread().getName() + "獲取到lock鎖");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock2){
                System.out.println(Thread.currentThread().getName() + "獲取到lock2鎖");
            }
        }
    });
    Thread thread2 = new Thread(() -> {
        synchronized (lock2){
            System.out.println(Thread.currentThread().getName() + "獲取到lock2鎖");
            try {
                Thread.sleep(2000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            synchronized (lock){
                System.out.println(Thread.currentThread().getName() + "獲取到lock鎖");
            }
        }
    });
    thread1.start();
    thread2.start();
}
---------------------
//輸出:
Thread-1獲取到lock2鎖
Thread-0獲取到lock鎖
.....
//分析:線程0得到lock鎖,線程1得到lock2鎖,但以後因爲兩個線程還要獲取對方已持有的鎖,但已持有的鎖都不會被雙方釋放,線程"假死",沒法往下執行,從而造成死循環,即死鎖,以後一直在作無用的死循環,嚴重浪費系統資源
複製代碼

咱們用 jstack 查看一下這個任務的各個線程運行狀況,能夠發現兩個線程都被阻塞 BLOCKED

咱們很明顯的發現,Java-level=deadlock,即死鎖,兩個線程相互等待對方的鎖

Synchronized實現原理

Synchronization

  1. 在JVM中,同步的實現是經過監視器鎖的進入和退出實現的,要麼顯示得經過monitorenter 和 monitorexit指令實現,要麼隱示地經過方法調用和返回指令實現
  2. 對於Java代碼來講,或許最經常使用的同步實現就是同步方法(代碼塊)。其中同步代碼塊是經過使用 monitorenter 和 monitorexit 實現的,而同步方法倒是使用 ACC_SYNCHRONIZED 標記符隱示的實現,原理是經過方法調用指令檢查該方法在常量池中是否包含 ACC_SYNCHRONIZED 標記符

反編譯

預準備

爲了能直觀瞭解Synchronized的工做原理,咱們經過反編譯SynchronizedDeme類的class文件的方式看看都發生了什麼

public class SynchronizedDemo {
    public static synchronized void staticMethod() throws InterruptedException {
        System.out.println("靜態同步方法開始");
        Thread.sleep(1000);
        System.out.println("靜態同步方法結束");
    }
    public synchronized void method() throws InterruptedException {
        System.out.println("實例同步方法開始");
        Thread.sleep(1000);
        System.out.println("實例同步方法結束");
    }
    public synchronized void method2() throws InterruptedException {
        System.out.println("實例同步方法2開始");
        Thread.sleep(3000);
        System.out.println("實例同步方法2結束");
    }
    public static void main(String[] args) {
        final SynchronizedDemo synDemo = new SynchronizedDemo();
        Thread thread1 = new Thread(() -> {
            try {
               synDemo.method();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread thread2 = new Thread(() -> {
            try {
                synDemo.method2();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        thread1.start();
        thread2.start();
    }
}
複製代碼

生成.class文件

javac -encoding UTF-8 SynchronizedDemo.java

最終咱們將獲得一個 .class 文件,即 SynchronizedDemo.class

javap反編譯

javap -v SynchronizedDemo

複製代碼經過反編譯咱們會獲得常量池、同步方法、同步代碼塊的不一樣編譯結果

常量池圖

常量池除了會包含基本類型和字符串及數組的常量值外,還包含以文本形式出現的符號引用:

類和接口的全限定名

字段的名稱和描述符

方法和名稱和描述符
複製代碼

同步方法圖示

同步方法會包含一個ACC_SYNCHCRONIZED標記符

同步代碼塊圖示

同步代碼塊會在代碼中插入 monitorenter 和 monitorexist 指令

同步代碼塊同步原理

monitor監視器

  1. 每一個對象都有一個監視器,在同步代碼塊中,JVM經過monitorenter和monitorexist指令實現同步鎖的獲取和釋放功能
  2. 當一個線程獲取同步鎖時,便是經過獲取monitor監視器進而等價爲獲取到鎖
  3. monitor的實現相似於操做系統中的管程

monitorenter指令

每一個對象都有一個監視器。當該監視器被佔用時便是鎖定狀態(或者說獲取監視器便是得到同步鎖)。線程執行monitorenter指令時會嘗試獲取監視器的全部權,過程以下:

  1. 若該監視器的進入次數爲0,則該線程進入監視器並將進入次數設置爲1,此時該線程即爲該監視器的全部者
  2. 若線程已經佔有該監視器並重入,則進入次數+1
  3. 若其餘線程已經佔有該監視器,則線程會被阻塞直到監視器的進入次數爲0,以後線程間會競爭獲取該監視器的全部權只有首先得到鎖的線程才能容許繼續獲取多個鎖

monitorexit指令

執行monitorexit指令將遵循如下步驟:

  1. 執行monitorexit指令的線程必須是對象實例所對應的監視器的全部者
  2. 指令執行時,線程會先將進入次數-1,若-1以後進入次數變成0,則線程退出監視器(即釋放鎖)其餘阻塞在該監視器的線程能夠從新競爭該監視器的全部權

實現原理

  1. 在同步代碼塊中,JVM經過monitorenter和monitorexist指令實現同步鎖的獲取和釋放功能
  2. monitorenter指令是在編譯後插入到同步代碼塊的開始位置
  3. monitorexit指令是插入到方法結束處和異常處
  4. JVM要保證每一個monitorenter必須有對應的monitorexit與之配對
  5. 任何對象都有一個monitor與之關聯,當且一個monitor被持有後,它將處於鎖定狀態
  6. 線程執行monitorenter指令時,將會嘗試獲取對象所對應的monitor的全部權,即嘗試得到對象的鎖
  7. 線程執行monitorexit指令時,將會將進入次數-1直到變成0時釋放監視器
  8. 同一時刻只有一個線程可以成功,其它失敗的線程會被阻塞,並放入到同步隊列中,進入BLOCKED狀態

補充

因爲 wait/notify 等方法底層實現是基於監視器,所以只有在同步方法(塊)中才能調用wait/notify等方法,不然會拋出 java.lang.IllegalMonitorStateException 的異常的緣由

同步方法同步原理

區別於同步代碼塊的監視器實現,同步方法經過使用 ACC_SYNCHRONIZED 標記符隱示的實現原理是經過方法調用指令檢查該方法在常量池中是否包含 ACC_SYNCHRONIZED 標記符,若是有,JVM 要求線程在調用以前請求鎖

進階原理

Monitor Obejct模式

Monitor Obejct模式綜述

Monitor實際上是一種同步工具,也能夠說是一種同步機制,它一般被描述爲一個對象,主要特色是互斥和信號機制

  1. 互斥: 一個Monitor鎖在同一時刻只能被一個線程佔用,其餘線程沒法佔用
  2. 信號機制(signal): 佔用Monitor鎖失敗的線程會暫時放棄競爭並等待某個謂詞成真(條件變量),但該條件成立後,當前線程會經過釋放鎖通知正在等待這個條件變量的其餘線程,讓其能夠從新競爭鎖

Mesa派的signal機制

  1. Mesa派的signal機制又稱"Non-Blocking condition variable"
  2. 佔有Monitor鎖的線程發出釋放通知時,不會當即失去鎖,而是讓其餘線程等待在隊列中,從新競爭鎖
  3. 這種機制裏,等待者拿到鎖後不能肯定在這個時間差裏是否有別的等待者進入過Monitor,所以不能保證謂詞必定爲真,因此對條件的判斷必須使用while
  4. Java中採用就是Mesa派的singal機制,即所謂的notify

Monitor Obejct模式結構

在 Monitor Object 模式中,主要有四種類型的參與者:

Monitor Obejct模式協做過程

  1. 同步方法的調用和串行化:

    • 當客戶線程調用監視者對象的同步方法時,必須首先獲取它的監視鎖
    • 只要該監視者對象有其餘同步方法正在被執行,獲取操做便不會成功
    • 當監視者對象已被線程佔用時(即同步方法正被執行),客戶線程將被阻塞直到它獲取監視鎖
    • 當客戶線程成功獲取監視鎖後,進入臨界區,執行方法實現的服務
    • 一旦同步方法完成執行,監視鎖會被自動釋放,目的是使其餘客戶線程有機會調用執行該監視者對象的同步方法
  2. 同步方法線程掛起:

    • 若是調用同步方法的客戶線程必須被阻塞或是有其餘緣由不能馬上進行,它可以在一個監視條件(Monitor Condition)上等待,這將致使該客戶線程暫時釋放監視鎖,並被掛起在監視條件上
  3. 監視條件通知:

    • 一個客戶線程可以通知一個監視條件,目的是通知阻塞在該監視條件(該監視鎖)的線程恢復運行
  4. 同步方法線程恢復:

    • 一旦一個早先被掛起在監視條件上的同步方法線程獲取通知,它將繼續在最初的等待監視條件的點上執行
    • 在被通知線程被容許恢復執行同步方法以前,監視鎖將自動被獲取(線程間自動相互競爭鎖)

對象頭

JVM內存中的對象

在JVM中,對象在內存中的佈局分紅三塊區域:對象頭、示例數據和對齊填充

對象頭: 對象頭主要存儲 Mark Word(對象的hashCode、鎖信息)、類型指針、數組長度(如果數組的話)等信息

示例數據:存放類的屬性數據信息,包括父類的屬性信息,若是是數組的實例部分還包括數組長度,這部份內存按4字節對齊

填充數據:因爲JVM要求對象起始地址必須是8字節的整數倍,當不知足8字節時會自動填充(所以填充數據並非必須的,僅僅是爲了字節對齊)

對象頭綜述

  1. synchcronized的鎖是存放在Java對象頭中的

  2. 若是對象是數組類型,JVM用3個字寬(Word)存儲對象頭,不然是用2個子寬在32位虛擬機中,1字寬等於4個字節,即32bit;64位的話就是8個字節,即64bit

Mark Word的存儲結構

32位JVM的Mark Word的默認存儲結構(無鎖狀態)

在運行期間,Mark Word裏存儲的數據會隨着鎖標誌位的變化而變化(32位)

64位JVM的Mark Word的默認存儲結構(對於32位無鎖狀態,有25bit沒有使用)

Monitor Record

Monitor Record綜述

  1. MonitorRecord(統一簡稱MR)是Java線程私有的數據結構,每個線程都有一個可用MR列表,同時還有一個全局的可用列表
  2. 一個被鎖住的對象都會和一個MR關聯(對象頭的MarkWord中的LockWord指向MR的起始地址)
  3. MR中有一個Owner字段存放擁有該鎖的線程的惟一標識,表示該鎖被這個線程佔用

Monitor Record結構

Monitor Record工做原理

  1. 線程若是得到監視鎖成功,將成爲該監視鎖對象的擁有者
  2. 在任一時刻,監視器對象只屬於一個活動線程(Owner)
  3. 擁有者能夠調用wait方法自動釋放監視鎖,進入等待狀態

鎖優化

自旋鎖

  1. 痛點:因爲線程的阻塞/喚醒須要CPU在用戶態和內核態間切換,頻繁的轉換對CPU負擔很重,進而對併發性能帶來很大的影響
  2. 現象:經過大量分析發現,對象鎖的鎖狀態一般只會持續很短一段時間,不必頻繁地阻塞和喚醒線程
  3. 原理:經過執行一段無心義的空循環讓線程等待一段時間,不會被當即掛起,看持有鎖的線程是否很快釋放鎖,若是鎖很快被釋放,那當前線程就有機會不用阻塞就能拿到鎖了,從而減小切換,提升性能
  4. 隱患:若鎖能很快被釋放,那麼自旋效率就很好(真正執行的自旋次數越少效率越好,等待時間就少);但如果鎖被一直佔用,那自旋其實沒有作任何有意義的事但又白白佔用和浪費了CPU資源,反而形成資源浪費
  5. 注意:自旋次數必須有個限度(或者說自旋時間),若是超過自旋次數(時間)還沒得到鎖,就要被阻塞掛起
  6. 使用: JDK1.6以上默認開啓-XX:+UseSpinning,自旋次數可經過-XX:PreBlockSpin調整,默認10次

自適應自旋鎖

  1. 痛點:因爲自旋鎖只能指定固定的自旋次數,但因爲任務的差別,致使每次的最佳自旋次數有差別
  2. 原理:經過引入"智能學習"的概念,由前一次在同一個鎖上的自旋時間和鎖的持有者的狀態來決定自旋的次數,換句話說就是自旋的次數不是固定的,而是能夠經過分析上次得出下次,更加智能
  3. 實現:若當前線程針對某鎖自旋成功,那下次自旋此時可能增長(由於JVM認爲此次成功是下次成功的基礎),增長的話成功概率可能更大;反正,若自旋不多成功,那麼自旋次數會減小(減小空轉浪費)甚至直接省略自旋過程,直接阻塞(由於自旋徹底沒有意義,還不如直接阻塞)
  4. 補充:有了自適應自旋鎖,隨着程序運行和性能監控信息的不斷完善,JVM對鎖的情況預測會愈來愈準確,JVM會變得愈來愈智能

阻塞鎖

阻塞鎖

  1. 加鎖成功:當出現鎖競爭時,只有得到鎖的線程可以繼續執行
  2. 加鎖失敗:競爭失敗的線程會由running狀態進入blocking狀態,並被放置到與目標鎖相關的一個等待隊列中
  3. 解鎖:當持有鎖的線程退出臨界區,釋放鎖後,會將等待隊列中的一個阻塞線程喚醒,令其從新參與到鎖競爭中

公平鎖

公平鎖就是得到鎖的順序按照先到先得的原則,從實現上說,要求當一個線程競爭某個對象鎖時,只要這個鎖的等待隊列非空,就必須把這個線程阻塞並塞入隊尾(插入隊尾通常經過一個CAS操做保持插入過程當中沒有鎖釋放)

非公平鎖

相對的,非公平鎖場景下,每一個線程都先要競爭鎖,在競爭失敗或當前已被加鎖的前提下才會被塞入等待隊列,在這種實現下,後到的線程有可能無需進入等待隊列直接競爭到鎖(隨機性)

鎖粗化

  1. 痛點:屢次鏈接在一塊兒的加鎖、解鎖操做會形成
  2. 原理:將屢次鏈接在一塊兒的加鎖、解鎖操做合併爲一次,將多個連續的鎖擴展成一個範圍更大的鎖
  3. 使用:將多個彼此靠近的同步塊合同在一個同步塊 或 把多個同步方法合併爲一個方法
  4. 補充:在JDK內置的API中,例如StringBuffer、Vector、HashTable都會存在隱性加鎖操做,可合併
/** * StringBuffer是線程安全的字符串處理類 * 每次調用stringBuffer.append方法都須要加鎖和解鎖,若是虛擬機檢測到有一系列連串的對同一個對象加鎖和解鎖操做,就會將其合併成一次範圍更大的加鎖和解鎖操做,即在第一次append方法時進行加鎖,最後一次append方法結束後進行解鎖 */
StringBuffer stringBuffer = new StringBuffer();
public void append(){
    stringBuffer.append("kira");
    stringBuffer.append("sally");
    stringBuffer.append("mengmeng");
}
複製代碼

鎖消除

  1. 痛點:根據代碼逃逸技術,若是判斷到一段代碼中,堆上的數據不會逃逸出當前線程,那麼能夠認爲這段代碼是線程安全的,沒必要要加鎖
  2. 原理: JVM在編譯時經過對運行上下文的描述,去除不可能存在共享資源競爭的鎖,經過這種方式消除無用鎖,即刪除沒必要要的加鎖操做,從而節省開銷
  3. 使用: 逃逸分析和鎖消除分別可使用參數-XX:+DoEscapeAnalysis和-XX:+EliminateLocks(鎖消除必須在-server模式下)開啓
  4. 補充:在JDK內置的API中,例如StringBuffer、Vector、HashTable都會存在隱性加鎖操做,可消除
/** * 好比執行10000次字符串的拼接 */
public static void main(String[] args) {
    SynchronizedDemo synchronizedDemo = new SynchronizedDemo();
    for (int i = 0 ; i < 10000 ; i++){
        synchronizedDemo.append("kira","sally");
    }
}
public void append(String str1,String str2){
    //因爲StringBuffer對象被封裝在方法內部,不可能存在共享資源競爭的狀況
    //所以JVM會認爲該加鎖是無心義的,會在編譯期就刪除相關的加鎖操做
    //還有一點特別要註明:明知道不會有線程安全問題,代碼階段就應該使用StringBuilder
    //不然在沒有開啓鎖消除的狀況下,StringBuffer不會被優化,性能可能只有StringBuilder的1/3
    StringBuffer stringBuffer = new StringBuffer();
    stringBuffer.append(str1).append(str2);
}/** 複製代碼

鎖的升級

  1. 從JDK1.6開始,鎖一共有四種狀態:無鎖狀態、偏向鎖狀態、輕量鎖狀態、重量鎖狀態

  2. 鎖的狀態會隨着競爭狀況逐漸升級,鎖容許升級但不容許降級

  3. 不容許降級的目的是提升得到鎖和釋放鎖的效率

  4. 後面會經過倒序的方式,即重量級鎖->輕量級鎖->偏向鎖進行講解,由於一般後者是前者的優化

鎖的升級過程

重量級鎖

  1. 重量級鎖經過對象內部的monitor實現(見上文的Monitor Object模式)
  2. monitor的本質是依賴於底層操做系統的MutexLock實現,操做系統實現線程間的切換是經過用戶態與內核態的切換完成的,而切換成本很高
  3. MutexLock最核心的理念就是嘗試獲取鎖.若可獲得就佔有.若不能,就進入睡眠等待

輕量級鎖

輕量級鎖綜述

  1. 痛點:因爲線程的阻塞/喚醒須要CPU在用戶態和內核態間切換,頻繁的轉換對CPU負擔很重,進而對併發性能帶來很大的影響
  2. 主要目的: 在沒有多線程競爭的前提下,減小傳統的重量級鎖使用操做系統互斥量產生的性能消耗
  3. 升級時機: 當關閉偏向鎖功能或多線程競爭偏向鎖會致使偏向鎖升級爲輕量級鎖
  4. 原理: 在只有一個線程執行同步塊時進一步提升性能
  5. 數據結構: 包括指向棧中鎖記錄的指針、鎖標誌位

輕量級鎖流程圖

線程1和線程2同時爭奪鎖,並致使鎖膨脹成重量級鎖

輕量級鎖加鎖

  1. 線程在執行同步塊以前,JVM會先在當前線程的棧幀中建立用於存儲鎖記錄的空間,並將對象頭中的Mark Word複製到鎖記錄中(Displaced Mark Word-即被取代的Mark Word)作一份拷貝
  2. 拷貝成功後,線程嘗試使用CAS將對象頭的Mark Word替換爲指向鎖記錄的指針(將對象頭的Mark Word更新爲指向鎖記錄的指針,並將鎖記錄裏的Owner指針指向Object Mark Word)
  3. 若是更新成功,當前線程得到鎖,繼續執行同步方法
  4. 若是更新失敗,表示其餘線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖,若自旋後沒有得到鎖,此時輕量級鎖會升級爲重量級鎖,當前線程會被阻塞

輕量級鎖解鎖

  1. 解鎖時會使用CAS操做將Displaced Mark Word替換回到對象頭,
  2. 若是解鎖成功,則表示沒有競爭發生
  3. 若是解鎖失敗,表示當前鎖存在競爭,鎖會膨脹成重量級鎖,須要在釋放鎖的同時喚醒被阻塞的線程,以後線程間要根據重量級鎖規則從新競爭重量級鎖

輕量級鎖注意事項

隱患:對於輕量級鎖有個使用前提是"沒有多線程競爭環境",一旦越過這個前提,除了互斥開銷外,還會增長額外的CAS操做的開銷,在多線程競爭環境下,輕量級鎖甚至比重量級鎖還要慢

偏向鎖

偏向鎖綜述

  1. 痛點: Hotspot做者發如今大多數狀況下不存在多線程競爭的狀況,而是同一個線程屢次獲取到同一個鎖,爲了讓線程得到鎖代價更低,所以設計了偏向鎖 (這個跟業務使用有很大關係)
  2. 主要目的: 爲了在無多線程競爭的狀況下儘可能減小沒必要要的輕量級鎖執行路徑
  3. 原理: 在只有一個線程執行同步塊時經過增長標記檢查而減小CAS操做進一步提升性能
  4. 數據結構: 包括佔有鎖的線程id,是不是偏向鎖,epoch(偏向鎖的時間戳),對象分代年齡、鎖標誌位

偏向鎖流程圖

線程1演示了偏向鎖的初始化過程,線程2演示了偏向鎖的撤銷鎖過程

偏向鎖初始化

  1. 當一個線程訪問同步塊並獲取到鎖時,會在對象頭和棧幀中的鎖記錄裏存儲偏向鎖的線程ID,之後該線程在進入和退出同步塊時不須要花費CAS操做來加鎖和解鎖,而是先簡單檢查對象頭的MarkWord中是否存儲了當前線程
  2. 若是已存儲,說明當前線程已經獲取到鎖,繼續執行任務便可
  3. 若是未存儲,則須要再判斷當前鎖否是偏向鎖(即對象頭中偏向鎖的標識是否設置爲1,鎖標識位爲01)
  4. 若是沒有設置,則使用CAS競爭鎖(說明此時並非偏向鎖,必定是等級高於它的鎖)
  5. 若是設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程,也就是結構中的線程ID

偏向鎖撤銷鎖

  1. 偏向鎖使用一種等到競爭出現才釋放鎖的機制,只有當其餘線程競爭鎖時,持有偏向鎖的線程纔會釋放鎖

  2. 偏向鎖的撤銷須要等待全局安全點(該時間點上沒有字節碼正在執行)

  3. 偏向鎖的撤銷須要遵循如下步驟: -

    • 首先會暫停擁有偏向鎖的線程並檢查該線程是否存活:
      • 若是線程非活動狀態,則將對象頭設置爲無鎖狀態(其餘線程會從新獲取該偏向鎖)
      • 若是線程是活動狀態,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,並將對棧中的鎖記錄和對象頭的MarkWord進行重置
  4. 要麼從新偏向於其餘線程(即將偏向鎖交給其餘線程,至關於當前線程"被"釋放了鎖)

  5. 要麼恢復到無鎖或者標記鎖對象不適合做爲偏向鎖(此時鎖會被升級爲輕量級鎖)

  6. 最後喚醒暫停的線程,被阻塞在安全點的線程繼續往下執行同步代碼塊

偏向鎖關閉鎖

  1. 偏向鎖在JDK1.6以上默認開啓,開啓後程序啓動幾秒後纔會被激活
  2. 有必要可使用JVM參數來關閉延遲 -XX:BiasedLockingStartupDelay = 0
  3. 若是肯定鎖一般處於競爭狀態,則可經過JVM參數 -XX:-UseBiasedLocking=false 關閉偏向鎖,那麼默認會進入輕量級鎖

偏向鎖注意事項

  1. 優點:偏向鎖只須要在置換ThreadID的時候依賴一次CAS原子指令,其他時刻不須要CAS指令(相比其餘鎖)
  2. 隱患:因爲一旦出現多線程競爭的狀況就必須撤銷偏向鎖,因此偏向鎖的撤銷操做的性能損耗必須小於節省下來的CAS原子指令的性能消耗(這個一般只能經過大量壓測纔可知)
  3. 對比:輕量級鎖是爲了在線程交替執行同步塊時提升性能,而偏向鎖則是在只有一個線程執行同步塊時進一步提升性能

偏向鎖 vs 輕量級鎖 vs 重量級鎖

相關文章
相關標籤/搜索