1. 程式人生 > 其它 >小白程式設計師也可以的浪漫-星空特效

小白程式設計師也可以的浪漫-星空特效

Java併發-Synchronized

Java併發

CAS

compare and swap 比較並交換,cas又叫做無鎖,自旋鎖,樂觀鎖,輕量級鎖

例如下面的程式碼,如果想在多執行緒情況下保證結果的正確性,可以使用synchronized

public class A {

    private int i;

    public synchronized  void addI(){
        i++;
    }
    
    public int getI() {
        return i;
    }
}
public class ConcurrencyDemo {


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

        long startTime=System.currentTimeMillis();

        A a = new A();

        Thread thread = new Thread(() -> {
            for (int i = 0; i < 1000000; i++) {
                a.addI();
            }
        });
        thread.start();

        for (int i = 0; i < 1000000; i++) {
            a.addI();
        }
        
        thread.join();
        
        System.out.println(a.getI());

        long endTime=System.currentTimeMillis();

        System.out.println("程式執行時間: "+(endTime-startTime)+"ms");
    }
}
結果 2000000
程式執行時間: 109ms

而使用AtomicInteger類來進行相同的操作

    private AtomicInteger atomicInteger=new AtomicInteger();

    public int getCAS() {
        return atomicInteger.get();
    }

    public  void addCAS(){
        atomicInteger.incrementAndGet();
    }
結果 2000000
程式執行時間: 67ms

縮短了不少時間,將迴圈數量調整到1億次效果更加明顯

使用synchronized 程式執行時間: 7512ms

使用atomicInteger類 程式執行時間: 2521ms

結果時間縮短了將近3倍,為什麼atomicInteger類比synchronized關鍵字縮短這麼長時間呢

當使用synchronized時,如果在非靜態方法上鎖住的是物件,因為上面只有一個物件,也就是一個鎖,當這兩個執行緒去獲取鎖時同一時間內只會有一個執行緒獲取到了,而沒有獲取到的執行緒被新增到一個阻塞佇列,只能等待著上一個執行緒釋放鎖,即執行緒阻塞,當釋放完鎖需要喚醒其他執行緒來獲取鎖,其中還有上下文切換,還要找到該執行緒上次執行位置,作業系統進行執行緒排程等等,消耗資源比較多

那atomic類工作原理是什麼呢?

比較並交換

原理很簡單,當它進行加一操作時,並不是直接進行加一然後賦值,首先獲取到舊值,然後進行+1操作,最後比較記憶體中的值是否和取出來的舊值是否一樣,如果一樣則進行賦值,否則進行重試

總共就3步,獲取資料,進行加1,比較原始資料和記憶體中的資料,如果相同賦值,否則重試

原子性

那麼比較並交換是一個原子性操作嗎,光聽著名就感覺是兩個操作,比較,交換,如果在比較後又有其他執行緒進行修改值呢

這個atomic方法點到最後是一個本地方法,無法看到內部實現了,

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

但是在JVM原始碼中它底層彙編的實現會在比較交換前新增一個lock指令

因為不懂彙編在網上搜了一下lock指令的作用:LOCK指令字首會設定處理器的LOCK#訊號(譯註:這個訊號會使匯流排鎖定,阻止其他處理器接管匯流排訪問記憶體),直到使用LOCK字首的指令執行結束,這會使這條指令的執行變為原子操作。在多處理器環境下,設定LOCK#訊號能保證某個處理器對共享記憶體的獨佔使用。 https://blog.csdn.net/imred/article/details/51994189

所以可以將比較並交換看做為一個原子性的操作,不用擔心在比較後值進行了變化

ABA

還有一種情況,在獲取值,增加1,比較交換三步中,當執行緒A在第一步獲取完資料假設為3,正在進行下面的操作時,執行緒B將資料3改為了4,然後又改回了3,執行緒A繼續執行加1,在進行比較交換時判斷正確,原來是3當前程式碼中舊值也是3,然後進行了賦值

例如你買了瓶可樂,放桌上沒來及喝有事出去了,這時小明剛從外面回來非常渴把你的可樂喝了,然後又去商店給你買了一瓶一樣的放回去,你回來並沒有發現有什麼不同,可樂還是可樂,但是這瓶可樂已經不是你自己買的那一瓶了

這個也不算一個問題,因為結果正確,但又算一個問題,因為比較的資料已經和最開始獲取的資料並不是同一個,如果想要解決這個問題新增一個版本號即可,在每次進行比較交換時同時判斷版本號,上面的例子中如果使用了版本號,執行緒A最後判斷舊值和版本號,例如版本號預設為1,B執行緒進行兩次修改,版本號為3,A執行緒在比較並交換時同時判斷版本號和舊版本號,如果不同則不進行交換

在java中有一個類可以解決這個問題,AtomicStampedReference這裡就不再細講了,有興趣可以去看看

鎖升級

一個物件的鎖有4中狀態:無鎖,偏向鎖,輕量級鎖,重量級鎖

一個物件的鎖資訊都會儲存在物件頭的執行時元資料(Mark Word)中,在MarkWord中不僅僅儲存鎖的資訊,還有雜湊值,GC年齡等等,具體可以看這篇文章

64位虛擬機器物件頭中的資料

偏向鎖

偏向鎖是啥?當執行緒A訪問程式碼並獲取鎖時,會在物件頭的markword中儲存這某個執行緒的id,當下次這個執行緒進行操作時先判斷和儲存在物件頭中的執行緒id是否相同,如果相同則直接進行操作,不需要進行加鎖

如果不一致,例如執行緒B也訪問程式碼塊嘗試獲取鎖時,首先判斷記錄在物件頭中的執行緒A是否存活,如果沒有存活則將鎖狀態設定為無鎖,執行緒B競爭時將物件頭中執行緒id設定為B執行緒的id,如果執行緒A存活,則查詢這個執行緒的棧幀資訊,如果還是需要持有這個鎖物件則暫停執行緒A,撤銷偏向鎖,將鎖升級為輕量級鎖,如果不需要再持有執行緒A的鎖則將鎖設定為無狀態,重新設定偏向鎖

為什麼新增偏向鎖?在應用程式中大部分時間並不存在鎖的競爭,如果還是使用重量級鎖進行一系列操作浪費了許多無用的資源,但是如果不加鎖在一些出現執行緒競爭的時候,就無法保證資料的準確性

偏向鎖是預設是在jvm啟動後4秒開啟的,如果不想有延遲可以在啟動引數中新增:XX:BiasedLockingStartUpDelay=0

如果不需要偏向鎖可以新增-XX:-UseBiasedLocking = false來設定

什麼時候升級:執行緒A只要進行一次訪問後,在物件頭markWord中儲存了執行緒A的id,只要下次訪問的執行緒ID和上次儲存的不一致符合上面的條件則升級為偏向鎖

輕量級鎖

使用CAS進行輕量級的獲取鎖,如果沒有獲取到根據條件升級為重量級鎖

過程:

  1. 在當前虛擬機器棧幀中建立一份鎖記錄(LockRecord)的空間,DisplacedMarkWord
  2. 首先將物件頭中的markWord複製一份到當前棧幀的鎖記錄中
  3. 然後使用CAS將物件頭的內容改為指向執行緒儲存鎖記錄的地址
  4. 如果線上程A複製物件頭後,物件頭中的markWord還沒有更換之前,執行緒B也準備獲取鎖,複製物件頭到執行緒B的鎖記錄中,線上程B使用CAS進行替換物件頭時發現,執行緒A已經將物件頭中資料改變了,則執行緒B的CAS失敗,嘗試10次CAS來獲取鎖,如果沒有獲取到則升級為重量級鎖

https://edu.51cto.com/study/11144 可以看這篇部落格

什麼時候升級:當執行緒A獲取到了物件鎖,執行緒B也進行了訪問嘗試通過CAS獲取,自旋10次後還是沒有等到執行緒A釋放鎖則升級為重量級鎖,如果在10次內獲取到了則還是輕量級鎖

因為在JDK1.6之前synchronized是一個重量級鎖,比較消耗資源,在JDK1.6之後對synchronized進行了優化,當使用synchronized修飾並不會默預設初始一個重量級鎖,而是先使用偏向鎖->輕量級鎖->重量級鎖

鎖升級的條件

匯入包jol-core OpenJDK提供了JOL包,可以在執行時檢視物件,類的細節

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.10</version>
</dependency>

下面的程式碼演示鎖升級

public class LockUpDemo {
    public static void main(String[] args) throws InterruptedException {
        A a1 = new A();
        System.out.println(ClassLayout.parseInstance(a1).toPrintable()); ///====第一個輸出
        //等待JVM開啟偏向鎖
        Thread.sleep(5000);
        A a = new A();
        System.out.println(ClassLayout.parseInstance(a).toPrintable()); ///====第二個輸出

        synchronized (a) {
            System.out.println(ClassLayout.parseInstance(a).toPrintable()); ///====第三個輸出
        }

        new Thread(() -> {
            synchronized (a) {
                System.out.println(ClassLayout.parseInstance(a).toPrintable()); ///====第四個輸出
                try {
                    Thread.sleep(1500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();

		//等待一會防止兩個執行緒同時搶奪鎖導致直接升級為重量級鎖
        Thread.sleep(500);

        new Thread(() -> {
            synchronized (a) {
                System.out.println(ClassLayout.parseInstance(a).toPrintable());  //====第五個輸出
            }
        }).start();
    }
}

每次輸出前三行都是物件頭中的資訊,在第一行中儲存markWord的執行緒資訊

==>0  4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)==<--這一行
4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)

第一個輸出

com.jame.concurrency.cas.A object internals:							====主要看這段====
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE            ↓
      0     4        (object header)                           01 00 00 00 (00000001 00000000 00000000 00000000) (1)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int A.i                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

先看第一個輸出,在第一行資料中,在小箭頭指向位置為00000001,儲存這偏向鎖(1bit)和鎖資訊(2bit),也就是最後的001

對比這上面的圖,0為偏向鎖標識,也就是沒有啟用偏向鎖,前面說過在JVM啟動後偏向鎖會延遲一會再啟動,所以這裡為0 而後面的01對應著鎖的標識,也就是偏向鎖

第二個輸出

com.jame.concurrency.cas.A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           05 00 00 00 (00000101 00000000 00000000 00000000) (5)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int A.i                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

前面程式碼等待了5秒,能看到101已經啟動偏向鎖了

第三個輸出

com.jame.concurrency.cas.A object internals:                                ===注意這些資料和上次輸出發生了改變===
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE                    ↓       ↓        ↓
      0     4        (object header)                           05 e8 8b 02 (00000101 11101000 10001011 00000010) (42723333)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int A.i                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

因為啟動了偏向鎖,而這次輸出是在synchronized中進行的,也就是進行了獲取鎖的操作,對比上次輸出能發現在鎖資訊後面多了一些其他資料,而這些資料中就包含了當前執行緒的id

第四個輸出

com.jame.concurrency.cas.A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           78 f0 f5 20 (01111000 11110000 11110101 00100000) (552988792)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int A.i                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

又建立了一個執行緒進行獲取鎖的操作,能看到這裡已經升級為輕量級鎖了000

第五個輸出

com.jame.concurrency.cas.A object internals:
 OFFSET  SIZE   TYPE DESCRIPTION                               VALUE
      0     4        (object header)                           aa f3 2b 1d (10101010 11110011 00101011 00011101) (489419690)
      4     4        (object header)                           00 00 00 00 (00000000 00000000 00000000 00000000) (0)
      8     4        (object header)                           43 c1 00 f8 (01000011 11000001 00000000 11111000) (-134168253)
     12     4    int A.i                                       0
Instance size: 16 bytes
Space losses: 0 bytes internal + 0 bytes external = 0 bytes total

建立一個新執行緒獲取鎖,後三位010現在已經升級為重量級鎖了,也就是原來理解的synchronized,原因就是上一個獲取鎖睡眠了1500ms,而sleep不會釋放掉鎖,所以獲取不到,產生爭搶鎖,升級為了重量級鎖

注意:鎖的升級是不可逆的,即一旦從偏向鎖升級為輕量級鎖,輕量級升級為重量級鎖,則不能再降級,但是偏向鎖可以再設定回無鎖的狀態