天天看點

Java關鍵字——synchronized

文章目錄

      • 一、并發程式設計中的三個問題
        • 1.可見性問題
        • 2.原子性問題
        • 3.有序性問題
      • 二、Java記憶體模型(JMM)
        • 1.CPU 緩存 記憶體
        • 2.Java記憶體模型
        • 3.JMM記憶體模型與CPU硬體記憶體架構的關系
      • 三、synchronized如何保證三大特性
        • 1.synchronized與原子性
        • 2.synchronized與可見性
        • 3.synchronized與有序性
      • 四、synchronized的特性
        • 1.可重入特性
        • 2.不可中斷特性
        • 補充面試題:synchronized與Lock的差別
      • 五、synchronized原理
        • 1.通過反彙編看原理
        • 2.通過JVM源碼分析synchronized的原理
        • 3.為什麼說monitor是重量級鎖(使用者态和核心态)
      • 六、JDK6 synchronized優化
        • 1.CAS
        • 2.鎖更新
        • 3.偏向鎖
        • 4.輕量級鎖
        • 5.自旋鎖與自适應自旋鎖
        • 6.鎖消除
        • 7.鎖粗化
        • 8.synchronized使用建議

一、并發程式設計中的三個問題

1.可見性問題

  可見性(Visibility):是指一個線程對共享變量進行修改,另一個先立即得到修改後的最新值。

案例示範:一個線程根據boolean類型的标記flag, while循環,另一個線程改變這個flag變量的值,另一個線程并不會停止循環。

package com.liuwen.JVM虛拟機.關鍵字synchronized.三個特性;
import java.util.concurrent.TimeUnit;
/**
 * @description:    驗證可見性
 * @author: Liu Wen          一個線程對共享變量的修改,另一個線程不能立即得到最新值
 * @create: 2020-03-26 23:13
 **/
public class Test01Visibility {
    public static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (flag){
                //是以程式會一直運作,不會終止
            }
        },"Thread-1").start();

        TimeUnit.SECONDS.sleep(2);

        new Thread(()->{
            flag = false;
            System.out.println(Thread.currentThread().getName()+"已經将flag改為:"+flag);
        },"Thread-2").start();
    }
}
           
Java關鍵字——synchronized

明明已經将flag改為false,但是程式卻不可停止。分析原因:并發程式設計時,會出現可見性問題,當一個線程對共享變量進行了修改,另外的線程并沒有立即看到修改

後的最新值。

2.原子性問題

  原子性(Atomicity):在一次或多次操作中,要麼所有的操作都執行并且不會受其他因素幹擾而中斷,要麼所有的操作都不執行。

案例示範:5個線程各執行1000次 i++;

package com.liuwen.JVM虛拟機.關鍵字synchronized.三個特性;
import java.util.ArrayList;
/**
 * @description:    驗證原子性
 * @author: Liu Wen
 * @create: 2020-03-26 23:26
 **/
public class Test02Atomicity {
    private static int number = 0;
    public static void main(String[] args) throws InterruptedException{
        Runnable runnable = ()->{
            for (int i = 0; i < 1000; i++) {
                number++;
            }
        };
        ArrayList<Thread> arrayList = new ArrayList<>();
        //開啟五個線程運作runnable任務。
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(runnable);
            t.start();
            arrayList.add(t);
        }
        //為了避免main主線程先執行完畢
        for (Thread t : arrayList) {
            t.join();
        }
        System.out.println("number:"+number);
    }
}
           
Java關鍵字——synchronized

會出現輸出<5000的情況。分析原因:并發程式設計時,會出現原子性問題,當一個線程對共享變量操作到一半時,另外的線程也有可能來操作共享變量,幹擾了前一個線程的操作。

補充:number++不是一個原子性操作,它分為三步:取值,+1,指派三個操作。

3.有序性問題

  有序性(Ordering):是指程式中代碼的執行順序,Java在編譯時和運作時會對代碼進行優化,會導緻程式最終的執行順序不一定就是我們編寫代碼時的順序。

案例示範:列印i和j的值。

package com.liuwen.JVM虛拟機.關鍵字synchronized.三個特性;
/**
 * @description: Good good study,day day up!
 * @author: Liu Wen
 * @create: 2020-03-27 19:05
 **/
public class Test03Order2 {
    private static int i = 0, j = 0;
    private static int a = 0, b = 0;
//    private volatile static int i = 0, j = 0;   //禁止重排序
//    private volatile static int a = 0, b = 0;
    public static void main(String[] args) throws InterruptedException {
        int count = 0; // 計數
        while(true) {
            count++;
            i = 0;
            j = 0;
            a = 0;
            b = 0;
            Thread one = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 1;
                    i = b;
                }
            });
            Thread two = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    j = a;
                }
            });
            two.start();
            one.start();
            one.join();
            two.join();
            String result = "第" + count + "次( i= " + i + ", j= " + j + ")";
            if (i == 0 && j == 0) {
                System.out.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }
}
           
Java關鍵字——synchronized

代碼有四種結果:i=0,j=1;i=1,j=0;i=1,j=1;i=0,j=0。分析原因:程式代碼在執行過程中的先後順序,由于Java在編譯期以及運作期的優化,導緻了代碼的執行順序未必就是開發者編寫代碼時的順序。

二、Java記憶體模型(JMM)

1.CPU 緩存 記憶體

CPU:中央處理器,是計算機的控制和運算的核心,我們的程式最終都會變成指令讓CPU去執行,處理程式中的資料。

記憶體:我們的程式都是在記憶體中運作的,記憶體會儲存程式運作時的資料,供CPU處理。

緩存:CPU的運算速度和記憶體的通路速度相差比較大。這就導緻CPU每次操作記憶體都要耗費很多等待時間。記憶體的讀寫速度成為了計算機運作的瓶頸。于是就有了在CPU和主記憶體之間增加緩存的設計。最靠近CPU的緩存稱為L1,然後依次是 L2,L3和主記憶體,CPU緩存模型如圖下圖所示。

Java關鍵字——synchronized

CPU Cache分成了三個級别: L1, L2, L3。級别越小越接近CPU,速度也更快,同時也代表着容量越小。

  1. L1是最接近CPU的,它容量最小,例如32K,速度最快,每個核上都有一個L1 Cache。
  2. L2 Cache 更大一些,例如256K,速度要慢一些,一般情況下每個核上都有一個獨立的L2 Cache。
  3. L3 Cache是三級緩存中最大的一級,例如12MB,同時也是緩存中最慢的一級,在同一個CPU插槽之間的核共享一個L3 Cache。

  Cache的出現是為了解決CPU直接通路記憶體效率低下問題的,程式在運作的過程中,CPU接收到指令後,它會最先向CPU中的一級緩存(L1 Cache)去尋找相關的資料,如果命中緩存,CPU進行計算時就可以直接對CPU Cache中的資料進行讀取和寫人,當運算結束之後,再将CPUCache中的最新資料重新整理到主記憶體當中,CPU通過直接通路Cache的方式替代直接通路主存的方式極大地提高了CPU 的吞吐能力。但是由于一級緩存(L1 Cache)容量較小,是以不可能每次都命中。這時CPU會繼續向下一級的二級緩存(L2 Cache)尋找,同樣的道理,當所需要的資料在二級緩存中也沒有的話,會繼續轉向L3Cache、記憶體(主存)和硬碟。

2.Java記憶體模型

  Java記憶體模型,是Java虛拟機規範中所定義的一種記憶體模型,Java記憶體模型是标準化的,屏蔽掉了底層不同計算機的差別。

  **Java記憶體模型是一套規範,描述了Java程式中各種變量(線程共享變量)的通路規則,以及在JVM中将變量存儲到記憶體和從記憶體中讀取變量這樣的底層細節,Java記憶體模型是對共享資料的可見性、有序性、和原子性的規則和保障。**具體如下:

主記憶體

主記憶體是所有線程都共享的,都能通路的。所有的共享變量都存儲于主記憶體。

工作記憶體

每一個線程有自己的工作記憶體,工作記憶體隻存儲該線程對共享變量的副本。線程對變量的所有的操作(讀,取)都必須在工作記憶體中完成,而不能直接讀寫主記憶體中的變量,不同線程之間也不能直接通路對方工作記憶體中的變量。

Java關鍵字——synchronized

  Java記憶體模型中定義了以下8種操作來完成,主記憶體與工作記憶體之間具體的互動協定,即一個變量如何從主記憶體拷貝到工作記憶體、如何從工作記憶體同步回主記憶體之類的實作細節,虛拟機實作時必須保證下面提及的每一種操作都是原子的、不可再分的。

Java關鍵字——synchronized

注意:

  1. 如果對一個變量執行lock操作,将會清空工作記憶體中此變量的值。
  2. 對一個變量執行unlock操作之前,必須先把此變量同步到主記憶體中。

主記憶體與工作記憶體之間的資料互動過程:

lock -> read -> load -> use -> assign -> store -> write -> unlock
           

Java記憶體模型的作用:Java記憶體模型是一套在多線程讀寫共享資料時,對共享資料的可見性、有序性、和原子性的規則和保障。(提供synchronized,volatile)

3.JMM記憶體模型與CPU硬體記憶體架構的關系

  Java記憶體模型和硬體記憶體架構并不完全一緻。對于硬體記憶體來說隻有寄存器、緩存記憶體、主記憶體的概念,并沒有工作記憶體和主記憶體之分,也就是說Java記憶體模型對記憶體的劃分對硬體記憶體并沒有任何影響,因為JMM隻是一種抽象的概念,是一組規則,不管是工作記憶體的資料還是主記憶體的資料,對于計算機硬體來說都會存儲在計算機主記憶體中,當然也有可能存儲到CPU緩存或者寄存器中,是以總體上來說,Java記憶體模型和計算機硬體記憶體架構是一個互相交叉的關系,是一種抽象概念劃分與真實實體硬體的交叉。

Java關鍵字——synchronized

三、synchronized如何保證三大特性

基本原理:synchronized能夠保證在同一時刻最多隻有一個線程執行該段代碼,以達到保證并發安全的效果。

1.synchronized與原子性

代碼示例:

package com.liuwen.JVM虛拟機.關鍵字synchronized.三個特性;
import java.util.ArrayList;
/**
 * @description:    驗證原子性
 * @author: Liu Wen
 * @create: 2020-03-26 23:26
 **/
public class Test02Atomicity {
    private static int number = 0;
    private static Object obj = new Object();        //保證原子操作
    public static void main(String[] args) throws InterruptedException{
        Runnable runnable = ()->{
            for (int i = 0; i < 1000; i++) {
                synchronized (obj) {
                    number++;
                }
            }
        };
        ArrayList<Thread> arrayList = new ArrayList<>();
        //開啟五個線程運作runnable任務。
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(runnable);
            t.start();
            arrayList.add(t);
        }
        //為了避免main主線程先執行完畢
        for (Thread t : arrayList) {
            t.join();
        }
        System.out.println("number:"+number);
    }
}
           
Java關鍵字——synchronized

輸出5000,符合預期的結果。分析原因:對number++;增加同步代碼塊後,synchronized保證隻有一個線程拿到鎖,能夠進入同步代碼塊。保證同一時間隻有一個線程操作number++,就不會出現安全問題。

2.synchronized與可見性

代碼示例:

package com.liuwen.JVM虛拟機.關鍵字synchronized.三個特性;

import java.util.concurrent.TimeUnit;

/**
 * @description:    驗證可見性
 * @author: Liu Wen          一個線程對共享變量的修改,另一個線程不能立即得到最新值
 * @create: 2020-03-26 23:13
 **/
public class Test01Visibility {
    public static boolean flag = true;
//    public static volatile boolean flag = true;    //保證可見性
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{

            while (flag){
                // 增加對象共享資料的列印,println是同步方法
                    System.out.println(flag);
            }
        },"Thread-1").start();

        TimeUnit.SECONDS.sleep(2);

        new Thread(()->{
            flag = false;
            System.out.println(Thread.currentThread().getName()+"已經将flag改為:"+flag);
        },"Thread-2").start();
    }
}
           
Java關鍵字——synchronized

分析原因:println是同步方法,synchronized保證可見性的原理,執行synchronized時,會對應lock原子操作會重新整理工作記憶體中共享變量的值。

Java關鍵字——synchronized
  1. 如果對一個變量執行lock操作,将會清空工作記憶體中此變量的值。
  2. 對一個變量執行unlock操作之前,必須先把此變量同步到主記憶體中。

3.synchronized與有序性

  為了提高程式的執行效率,編譯器和CPU會對程式中代碼進行重排序。但必須遵從as-if-serial語義。as-if-serial語義的意思是:不管編譯器和CPU如何重排序,必須保證在單線程情況下程式的結果是正确的。

  編譯器和處理器不會對存在資料依賴關系的操作做重排序,因為這種重排序會改變執行結果。但是,如果操作之間不存在資料依賴關系,這些操作就可能被編譯器和處理器重排序。

  synchronized可以保證有序性的原理,我們加synchronized後,依然會發生重排序,隻不過,我們有同步代碼塊,可以保證隻有一個線程執行同步代碼中的代碼。進而保證有序性。

補充:如果隻是為了保證有序性,不建議用synchronized關鍵字來保證有序性,建議使用volatile關鍵字,它是一個輕量級操作。如下代碼:

package com.liuwen.JVM虛拟機.關鍵字synchronized.三個特性;
/**
 * @description:   volatile 可以禁止指令重排序
 * @author: Liu Wen
 * @create: 2020-03-27 19:05
 **/
public class Test03Order2 {
    private volatile static int i = 0, j = 0;   //禁止重排序
    private volatile static int a = 0, b = 0;
    public static void main(String[] args) throws InterruptedException {
        int count = 0; // 計數
        while(true) {
            count++;
            i = 0;
            j = 0;
            a = 0;
            b = 0;
            Thread one = new Thread(new Runnable() {
                @Override
                public void run() {
                    a = 1;
                    i = b;
                }
            });
            Thread two = new Thread(new Runnable() {
                @Override
                public void run() {
                    b = 1;
                    j = a;
                }
            });
            two.start();
            one.start();
            one.join();
            two.join();
            String result = "第" + count + "次( i= " + i + ", j= " + j + ")";
            if (i == 0 && j == 0) {
                System.out.println(result);
                break;
            } else {
                System.out.println(result);
            }
        }
    }
}
           
Java關鍵字——synchronized

代碼永遠不會找到 i=0,j=0 的情況。因為禁止了重排序,是以一定不會出現重排的情況。

四、synchronized的特性

1.可重入特性

可重入定義:一個線程可以多次執行synchronized,可以重複擷取同一把鎖。

可重入原理:synchronized的鎖對象中有一個計數器(recursions變量)會記錄線程獲得幾次鎖。

可重入的好處:可以避免死鎖;可以讓我們更好的來封裝代碼。

總結:synchronized是可重入鎖,内部鎖對象中會有一個計數器記錄線程擷取幾次鎖啦,在執行完同步代碼塊時,計數器的數量會-1,知道計數器的數量為0,就釋放這個鎖。

2.不可中斷特性

不可中斷定義:一個線程獲得鎖後,另一個線程想要獲得鎖,必須處于阻塞或等待狀态,如果第一個線程不釋放鎖,第二個線程會一直阻塞或等待,不可被中斷。

synchronized屬于不可被中斷。

Lock的lock方法是不可中斷的。

Lock的tryLock方法是可中斷的。

補充面試題:synchronized與Lock的差別

  1. synchronized是關鍵字,而Lock是一個接口。
  2. synchronized會自動釋放鎖,而Lock必須手動釋放鎖。
  3. synchronized是不可中斷的,Lock可以中斷也可以不中斷。
  4. 通過Lock可以知道線程有沒有拿到鎖,而synchronized不能。(如果在指定時間内知道自己拿不到鎖,則可中斷)
  5. synchronized能鎖住方法和代碼塊,而Lock隻能鎖住代碼塊。
  6. Lock可以使用讀鎖提高多線程讀效率。(讀寫分離)
  7. synchronized是非公平鎖,ReentrantLock可以控制是否是公平鎖。

五、synchronized原理

1.通過反彙編看原理

對下列代碼進行反彙編

package com.liuwen.JVM虛拟機.關鍵字synchronized.syn鎖;
/**
 * @description:           通過位元組碼檔案檢視syn原理
 * @author: Liu Wen
 * @create: 2020-03-27 08:05
 **/
public class Demo01 {
    private static Object obj = new Object();
    public static void main(String[] args){
        synchronized (obj){
            System.out.println("1");
        }
    }
    public synchronized void test(){
        System.out.println("a");
    }
}
           

要看synchronized的原理,但是synchronized是一個關鍵字,看不到源碼。我們可以将class檔案進行反彙編。

我是通過在IDEA的添加工具方式實作反彙編的:

Java關鍵字——synchronized
public static void main(java.lang.String[]);
    Code:
       0: getstatic     #2                  // Field obj:Ljava/lang/Object;
       3: dup
       4: astore_1
       5: monitorenter
       6: getstatic     #3                  // Field java/lang/System.out:Ljava/io/PrintStream;
       9: ldc           #4                  // String 1
      11: invokevirtual #5                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      14: aload_1
      15: monitorexit
      16: goto          24
      19: astore_2
      20: aload_1
      21: monitorexit
      22: aload_2
      23: athrow
      24: return
           

5:monitorenter 21:monitorexit 這是由JVM的C++建立的對象。

通過javap反彙編我們看到synchronized使用monitorentor和monitorexit兩個指令。每個鎖對象都會關聯一個monitor(螢幕,它才是真正的鎖對象),它内部有兩個重要的成員變量owner會儲存獲得鎖的線程,recursions會儲存線程獲得鎖的次數,當執行到monitorexit時,recursions會-1,當計數器減到0時這個線程就會釋放鎖。

可以簡單了解為下圖:

Java關鍵字——synchronized

對于monitorenter :

  synchronized的鎖對象會關聯一個monitor,這個monitor不是我們主動建立的,是JVM的線程執行到這個同步代碼塊,發現鎖對象沒有monitor就會建立monitor,monitor内部有兩個重要的成員變量owner:擁有這把鎖的線程,recursions會記錄線程擁有鎖的次數,當一個線程擁有monitor後其他線程隻能等待。

對于monitorexit:

  monitorexit插入在方法結束處和異常處,JVM保證每個monitorenter必須有對應的monitorexit。(是以,synchronized出現異常時會釋放鎖哦)

補充:對于同步方法,同步方法在反彙編後,會增加 ACC_SYNCHRONIZED 修飾。這會隐式調用monitorenter和

monitorexit。在執行同步方法前會調用monitorenter,在執行完同步方法後會調用monitorexit。

2.通過JVM源碼分析synchronized的原理

monitor螢幕鎖:可以看出無論是synchronized代碼塊還是synchronized方法,其線程安全的語義實作最終依賴一個叫monitor的東西,那麼這個神秘的東西是什麼呢?

在HotSpot虛拟機中,monitor是由ObjectMonitor實作的。其源碼是用c++來實作的,位于HotSpot虛拟機源碼ObjectMonitor.hpp檔案中(src/share/vm/runtime/objectMonitor.hpp)。ObjectMonitor主要資料結構如下:

ObjectMonitor() {
        _header = NULL;
        _count = 0;
        _waiters = 0,
        _recursions = 0;   // 線程的重入次數
        _object = NULL;    // 存儲該monitor的對象
        _owner = NULL;     // 辨別擁有該monitor的線程
        _WaitSet = NULL;   // 處于wait狀态的線程,會被加入到_WaitSet
        _WaitSetLock = 0 ;
        _Responsible = NULL;
        _succ = NULL;
        _cxq = NULL;       // 多線程競争鎖時的單向清單
        FreeNext = NULL;
        _EntryList = NULL; // 處于等待鎖block狀态的線程,會被加入到該清單
        _SpinFreq = 0;
        _SpinClock = 0;
        OwnerIsThread = 0;
}
           
  1. _owner:初始時為NULL。當有線程占有該monitor時,owner标記為該線程的唯一辨別。當線程釋放monitor時,owner又恢複為NULL。owner是一個臨界資源,JVM是通過CAS操作來保證其線程安全的。
  2. _cxq:競争隊列,所有請求鎖的線程首先會被放在這個隊列中(單向連結)。_cxq是一個臨界資源,JVM通過CAS原子指令來修改_cxq隊列。修改前_cxq的舊值填入了node的next字段,_cxq指向新值(新線程)。是以_cxq是一個後進先出的stack(棧)。
  3. _EntryList:_cxq隊列中有資格成為候選資源的線程會被移動到該隊列中。
  4. _WaitSet:因為調用wait方法而被阻塞的線程會被放在該隊列中。
    Java關鍵字——synchronized

  每一個Java對象都可以與一個螢幕monitor關聯,我們可以把它了解成為一把鎖,當一個線程想要執行一段被synchronized圈起來的同步方法或者代碼塊時,該線程得先擷取到synchronized修飾的對象對應的monitor。

  Java代碼裡不會顯示地去創造這麼一個monitor對象,我們也無需建立,可以這麼了解:monitor并不是随着對象建立而建立的。我們是通過synchronized修飾符告訴JVM需要為我們的某個對象建立關聯的monitor對象。每個線程都存在兩個ObjectMonitor對象清單,分别為free和used清單。同時JVM中也維護着global locklist。當線程需要ObjectMonitor對象時,首先從線程自身的free表中申請,若存在則使用,若不存在則從global list中申請。

monitor競争、monitor擷取、monitor等待、monitor釋放等操作都可以到JVM源碼中找出原理,不得不佩服寫這些JVM源碼的大佬們。這些源碼太難讀懂了,有時間再攻讀吧。源碼下載下傳閱讀:http://openjdk.java.net/ --> Mercurial --> jdk8 --> hotspot --> zip

3.為什麼說monitor是重量級鎖(使用者态和核心态)

  ObjectMonitor的函數調用中會涉及到核心函數(Atomic::cmpxchg_ptr,Atomic::inc_ptr等),執行同步代碼塊,沒有競争到鎖的對象會park()被挂起,競争到鎖的線程會unpark()喚醒。這個時候就會存在作業系統使用者态和核心态的轉換,這種切換會消耗大量的系統資源。是以synchronized是Java語言中是一個重量級(Heavyweight)的操作。

要了解使用者态和核心态,先了解一下Linux系統的體系架構:

Java關鍵字——synchronized

從上圖可以看出,Linux作業系統的體系架構分為:使用者空間(應用程式的活動空間)和核心。

核心:本質上可以了解為一種軟體,控制計算機的硬體資源,并提供上層應用程式運作的環境。

使用者空間:上層應用程式活動的空間。應用程式的執行必須依托于核心提供的資源,包括CPU資源、存

儲資源、I/O資源等。

系統調用:為了使上層應用能夠通路到這些資源,核心必須為上層應用提供通路的接口:即系統調用。

  所有程序初始都運作于使用者空間,此時即為使用者運作狀态(簡稱:使用者态);但是當它調用系統調用執行某些操作時,例如 I/O調用,此時需要陷入核心中運作,我們就稱程序處于核心運作态(或簡稱為核心态)。 系統調用的過程可以簡單了解為:

  1. 使用者态程式将一些資料值放在寄存器中, 或者使用參數建立一個堆棧, 以此表明需要作業系統提供的服務。
  2. 使用者态程式執行系統調用。
  3. CPU切換到核心态,并跳到位于記憶體指定位置的指令。
  4. 系統調用處理器(system call handler)會讀取程式放入記憶體的資料參數,并執行程式請求的服務。
  5. 系統調用完成後,作業系統會重置CPU為使用者态并傳回系統調用的結果。

由此可見使用者态切換至核心态需要傳遞許多變量,同時核心還需要保護好使用者态在切換時的一些寄存器值、變量等,以備核心态切換回使用者态。這種切換就帶來了大量的系統資源消耗,這就是在synchronized未優化之前,效率低的原因。

是以在JDK6中,對synchronized做了優化處理。

六、JDK6 synchronized優化

1.CAS

CAS:Compare And Swap(比較相同再交換)。CAS可以将比較和交換轉換為原子操作,這個原子操作直接由CPU保證。CAS可以保證共享變量指派時的原子操作。CAS操作依賴3個值:記憶體中的值V,舊的預估值X,要修改的新值B,如果舊的預估值X等于記憶體中的值V,就将新的值B儲存到記憶體中。

JUC中atomic包下的類都是原子類,原理就是使用CAS操作。代碼示例:

package com.liuwen.JVM虛拟機.關鍵字synchronized.syn鎖優化;
import java.util.ArrayList;
import java.util.concurrent.atomic.AtomicInteger;
/**
 * @description: Good good study,day day up!
 * @author: Liu Wen
 * @create: 2020-03-27 09:11
 **/
public class Demo01CAS {
    private static AtomicInteger atomicInteger = new AtomicInteger();
    public static void main(String[] args) throws InterruptedException{
        Runnable runnable = ()->{
            for (int i = 0; i < 1000; i++) {
               atomicInteger.incrementAndGet();
            }
        };
        ArrayList<Thread> arrayList = new ArrayList<>();
        //開啟五個線程運作runnable任務。
        for (int i = 0; i < 5; i++) {
            Thread t = new Thread(runnable);
            t.start();
            arrayList.add(t);
        }
        //為了避免main主線程先執行完畢
        for (Thread t : arrayList) {
            t.join();
        }
        System.out.println("number:"+atomicInteger);
    }
}
/*
5000
*/
           

上段代碼在沒有做任何同步的情況下,代碼輸出正确,滿足原子性操作。為什麼???????

檢視 AtomicInteger 源碼如下:

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;
    private static final Unsafe unsafe = Unsafe.getUnsafe(); //提供原子性
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    private volatile int value;      //提供可見性
    //.............
}
           

分析:AtomicInteger的源碼中Unsafe類提供了原子性,基于CAS原理。以incrementAndGet()為例:

CAS操作流程:假設兩個線程t1和t2來執行incrementAndGet()方法,其中記憶體值是由var1和var2尋址得到的最新的(由volatile保證)記憶體值:value,舊的預期值var5,新值var5+var4。極端現象:t1和t2都執行到doWhile的循環體,都得到舊的預估值var5=0,這時t1繼續執行,t2先執行其它。t1對while循環條件進行判斷,cas操作傳回true,while判斷條件為false,循環結束,新值賦給var5傳回,是以得到最新的記憶體值value為1;這時候t2回來執行while判斷,cas傳回false,是以判斷條件為true,則繼續執行do循環體,通過var1和var2尋址得到舊的預估值var5=1,再進行while判斷,完成指派,得到var5=2,最後将var5傳回給value。

Java關鍵字——synchronized

補充:從通過var1、var2的尋址得到value的操作可以看出,Unsafe類使Java擁有了像C語言的指針一樣操作記憶體空間的能力,同時也帶來了指針的問題。過度的使用Unsafe類會使得出錯的幾率變大,是以Java官方并不建議使用的,官方文檔也幾乎沒有。Unsafe對象不能直接調用,隻能通過反射獲得。

CAS操作是一種樂觀鎖的實作方式:總是假設最好的情況,每次去拿資料的時候都認為别人不會修改,就算改了也沒關系,再重試即可。是以不會上鎖,但是在更新的時候會判斷一下在此期間别人有沒有去修改這個資料,如何沒有人修改則更新,如果有人修改則重試。

CAS總結:CAS擷取共享變量時,為了保證該變量的可見性,需要使用volatile修飾。結合CAS和volatile可以實作無鎖并發,适用于競争不激烈、多核 CPU 的場景下。

  1.因為沒有使用 synchronized,是以線程不會陷入阻塞,這是效率提升的因素之一。

  2.但如果競争激烈,可以想到重試必然頻繁發生,反而效率會受影響。

2.鎖更新

  高效并發是從JDK 5到JDK 6的一個重要改進,HotSpot虛拟機開發團隊在這個版本上花費了大量的精力去實作各種鎖優化技術,包括偏向鎖( Biased Locking )、輕量級鎖( Lightweight Locking )和如适應性自旋(Adaptive Spinning)、鎖消除( Lock Elimination)、鎖粗化( Lock Coarsening )等,這些技術都是為了線上程之間更高效地共享資料,以及解決競争問題,進而提高程式的執行效率。

鎖更新:無鎖–》偏向鎖–》輕量級鎖–》重量級鎖

如何更新?必須了解java對象。Java對象由3部分組成,對象頭,執行個體資料,對齊資料。

Java關鍵字——synchronized

  對象頭由兩部分組成,一部分用于存儲自身的運作時資料,稱之為 Mark Word,它記錄了對象和鎖有關的資訊,另外一部分是類型指針,及對象指向它的類中繼資料的指針,JVM通過這個指針确定對象是哪個類的執行個體。(為了節約記憶體可以使用選項-XX:+UseCompressedOops 開啟指針壓縮)

  Mark Word用于存儲對象自身的運作時資料,如哈希碼(HashCode)、GC分代年齡、鎖狀态标志、線程持有的鎖、偏向線程ID、偏向時間戳等等,占用記憶體大小與虛拟機位長一緻。在64位虛拟機下,Mark Word是64bit大小的,其存儲結構如下:

Java關鍵字——synchronized

3.偏向鎖

  偏向鎖是JDK 6中的重要引進,因為HotSpot作者經過研究實踐發現,在大多數情況下,鎖不僅不存在多線程競争,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低,引進了偏向鎖。

  偏向鎖的“偏”,就是偏心的“偏”、偏袒的“偏”,它的意思是這個鎖會偏向于第一個獲得它的線程,會在對象頭存儲鎖偏向的線程ID,以後該線程進入和退出同步塊時隻需要檢查是否為偏向鎖、鎖标志位以及ThreadID即可。

偏向鎖原理:

當線程第一次通路同步塊并擷取鎖時,偏向鎖處理流程如下:

1.虛拟機将會把對象頭中的标志位設為“01”,即偏向模式。

2.同時使用CAS操作把擷取到這個鎖的線程的ID記錄在對象的Mark Word之中 ,如果CAS操作成功,持有偏向鎖的線程以後每次進入這個鎖相關的同步塊時,虛拟機都可以不再進行任何同步操作,偏向鎖的效率高。

偏向鎖的撤銷:

1.偏向鎖的撤銷動作必須等待全局安全點

2.暫停擁有偏向鎖的線程,判斷鎖對象是否處于被鎖定狀态

3.撤銷偏向鎖,恢複到無鎖(标志位為 01)或輕量級鎖(标志位為 00)的狀态

偏向鎖好處:偏向鎖是在隻有一個線程執行同步塊時進一步提高性能,适用于一個線程反複獲得同一鎖的情況。偏向鎖可以提高帶有同步但無競争的程式性能。它同樣是一個帶有效益權衡性質的優化,也就是說,它并不一定總是對程式運作有利,如果程式中大多數的鎖總是被多個不同的線程通路比如線程池,那偏向模式就是多餘的。

補充:在JDK5中偏向鎖預設是關閉的,而到了JDK6中偏向鎖已經預設開啟。但在應用程式啟動幾秒鐘之後才激活,可以使用-XX:BiasedLockingStartupDelay=0 參數關閉延遲,如果确定應用程式中所有鎖通常情況下處于競争狀态,可以通過XX:-UseBiasedLocking=false 參數關閉偏向鎖。

4.輕量級鎖

  在多線程交替執行同步塊的情況下,盡量避免重量級鎖引起的性能消耗,但是如果多個線程在同一時刻進入臨界區,會導緻輕量級鎖膨脹更新重量級鎖,是以輕量級鎖的出現并非是要替代重量級鎖。

  對于輕量級鎖,其性能提升的依據是“對于絕大部分的鎖,在整個生命周期内都是不會存在競争的”,如果打破這個依據則除了互斥的開銷外,還有額外的CAS操作,是以在有多線程競争的情況下,輕量級鎖比重量級鎖更慢。

輕量級鎖原理:

  當關閉偏向鎖功能或者多個線程競争偏向鎖導緻偏向鎖更新為輕量級鎖,則會嘗試擷取輕量級鎖,其擷取鎖步驟如下:

  1.判斷目前對象是否處于無鎖狀态(hashcode、0、01),如果是,則JVM首先将在目前線程的棧幀中建立一個名為鎖記錄(Lock Record)的空間,用于存儲鎖對象目前的Mark Word的拷貝(官方把這份拷貝加了一個Displaced字首,即Displaced Mark Word),将對象的Mark Word複制到棧幀中的Lock Record中,将Lock Reocrd中的owner指向目前對象。

  2.JVM利用CAS操作嘗試将對象的Mark Word更新為指向Lock Record的指針,如果成功表示競争到鎖,則将鎖标志位變成00,執行同步操作。

  3.如果失敗則判斷目前對象的Mark Word是否指向目前線程的棧幀,如果是則表示目前線程已經持有目前對象的鎖,則直接執行同步代碼塊;否則隻能說明該鎖對象已經被其他線程搶占了,這時輕量級鎖需要膨脹為重量級鎖,鎖标志位變成10,後面等待的線程将會進入阻塞狀态。

輕量級鎖的釋放:

輕量級鎖的釋放也是通過CAS操作來進行的,主要步驟如下:

1.取出在擷取輕量級鎖儲存在Displaced Mark Word中的資料。

2.用CAS操作将取出的資料替換目前對象的Mark Word中,如果成功,則說明釋放鎖成功。

3.如果CAS操作替換失敗,說明有其他線程嘗試擷取該鎖,則需要将輕量級鎖需要膨脹更新為重量級鎖。

輕量級鎖好處:在多線程交替執行同步塊的情況下,可以避免重量級鎖引起的性能消耗。

5.自旋鎖與自适應自旋鎖

  monitor實作鎖的時候,知道monitor會阻塞和喚醒線程,線程的阻塞和喚醒需要CPU從使用者态轉為核心态,頻繁的阻塞和喚醒對CPU來說是一件負擔很重的工作,這些操作給系統的并發性能帶來了很大的壓力。同時,虛拟機的開發團隊也注意到在許多應用上,共享資料的鎖定狀态隻會持續很短的一段時間,為了這段時間阻塞和喚醒線程并不值得。如果實體機器有一個以上的處理器,能讓兩個或以上的線程同時并行執行,我們就可以讓後面請求鎖的那個線程“稍等一下”,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖。為了讓線程等待,我們隻需讓線程執行一個忙循環(自旋) , 這項技術就是所謂的自旋鎖。

  自旋鎖在JDK 1.4.2中就已經引入 ,隻不過預設是關閉的,可以使用-XX:+UseSpinning參數來開啟,在JDK 6中就已經改為預設開啟了。自旋次數的預設值是10次,使用者可以使用參數-XX : PreBlockSpin來更改。

  在JDK 6中引入了自适應的自旋鎖。自适應意味着自旋的時間不再固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀态來決定。如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運作中,那麼虛拟機就會認為這次自旋也很有可能再次成功,進而它将允許自旋等待持續相對更長的時間,比如100次循環。另外,如果對于某個鎖,自旋很少成功獲得過,那在以後要擷取這個鎖時将可能省略掉自旋過程,以避免浪費處理器資源。有了自适應自旋,随着程式運作和性能監控資訊的不斷完善,虛拟機對程式鎖的狀況預測就會越來越準确,虛拟機就會變得越來越“聰明”了。

6.鎖消除

  鎖消除是指虛拟機即時編譯器(JIT)在運作時,對一些代碼上要求同步,但是被檢測到不可能存在共享資料競争的鎖進行消除。鎖消除的主要判定依據來源于逃逸分析的資料支援,如果判斷在一段代碼中,堆上的所有資料都不會逃逸出去進而被其他線程通路到,那就可以把它們當做棧上資料對待,認為它們是線程私有的,同步加鎖自然就無須進行。

package com.liuwen.JVM虛拟機.關鍵字synchronized.syn鎖優化;
/**
 * @description:    鎖消除
 * @author: Liu Wen
 * @create: 2020-03-31 16:45
 **/
public class Demo02LockElimination {
    public static void main(String[] args) {
        contactString("aa", "bb", "cc");
    }
    public static String contactString(String s1, String s2, String s3) {
        return new StringBuffer().append(s1).append(s2).append(s3).toString();
    }
}
           

append()方法是同步操作,源碼如下:

@Override
    public synchronized StringBuffer append(String str) {
        toStringCache = null;
        super.append(str);
        return this;
    }
           

  StringBuffer的append ( ) 是一個同步方法,鎖就是this也就是(new StringBuilder())。虛拟機發現它的動态作用域被限制在concatString( )方法内部。也就是說, new StringBuilder()對象的引用永遠不會“逃逸”到concatString ( )方法之外,其他線程無法通路到它,是以,雖然這裡有鎖,但是可以被安全地消除掉,在即時編譯之後,這段代碼就會忽略掉所有的同步而直接執行了。

7.鎖粗化

鎖粗化:JVM會探測到一連串細小的操作都使用同一個對象加鎖,将同步代碼塊的範圍放大,放到這串操作的外面,這樣隻需要加一次鎖即可。

代碼示例:

package com.liuwen.JVM虛拟機.關鍵字synchronized.syn鎖優化;
/**
 * @description:   鎖粗化
 * @author: Liu Wen
 * @create: 2020-03-31 16:50
 **/
public class Demo03LockCuhua {
    public static void main(String[] args) {
        StringBuffer sb = new StringBuffer();
        for (int i = 0; i < 100; i++) {
            sb.append("aa");
        }
        System.out.println(sb.toString());
    }
}
           

  補充:原則上,我們在編寫代碼的時候,總是推薦将同步塊的作用範圍限制得盡量小,隻在共享資料的實際作用域中才進行同步,這樣是為了使得需要同步的操作數量盡可能變小,如果存在鎖競争,那等待鎖的線程也能盡快拿到鎖。大部分情況下,上面的原則都是正确的,但是如果一系列的連續操作都對同一個對象反複加鎖和解鎖,甚至加鎖操作是出現在循環體中的,那即使沒有線程競争,頻繁地進行互斥同步操作也會導緻不必要的性能損耗。

8.synchronized使用建議

a.減少synchronized的範圍

同步代碼塊中盡量短,減少同步代碼塊中代碼的執行時間,減少鎖的競争。

synchronized (Demo01.class) {
		System.out.println("aaa");
}
           

b.降低synchronized鎖的粒度

Hashtable:鎖定整個hash表,一個操作正在進行時,其它操作阻塞等待,效率低下。

ConcurrentHashMap:局部鎖定(鎖分段機制),各個分段之間的操作互不影響,效率高于Hashtable。

c.讀寫分離

LinkedBlockingQueue在出隊(讀)和入隊(寫)的時候使用兩把鎖,是以take()(讀)和put()(寫)互不影響。

讀取時不加鎖,寫入或删除時加鎖,如:ConcurrentHashMap,CopyOnWriteArrayList和ConyOnWriteSet