java樂觀鎖和悲觀鎖最底層的實現

1. CAS實現的樂觀鎖

CAS(Compare And Swap 比較並且替換)是樂觀鎖的一種實現方式,是一種輕量級鎖,JUC 中很多工具類的實現就是基於 CAS 的,也可以理解爲自旋鎖

JUC是指import java.util.concurrent下面的包,

比如:import java.util.concurrent.atomic.AtomicInteger;

最終實現是彙編指令:lock cmpxchg 不僅僅是CAS的底層實現而且它是volatile 、synchronized 的底層實現

1.1CAS如何實現線程安全?

線程在讀取數據時不進行加鎖,在準備寫回數據時,先去查詢原值,操作的時候比較原值是否修改,若未被其他線程修改則寫回,若已被修改,則重新執行讀取流程

舉個栗子:現在一個線程要修改數據庫的name,修改前我會先去數據庫查name的值,發現name=“A”,拿到值了,我們準備修改成name=“b”,在修改之前我們判斷一下,原來的name是不是等於“A”,如果被其他線程修改就會發現name不等於“A”,我們就不進行操作,如果原來的值還是A,我們就把name修改爲“b”,至此,一個流程就結束了。

1.2 CAS會帶來什麼問題?

  1. 如果一致循環,CPU開銷過大

是因爲CAS操作長時間不成功的話,會導致一直自旋,相當於死循環了,CPU的壓力會很大。

  1. ABA問題

1.2.1 什麼是ABA問題?

  1. 線程1讀取了數據A
  2. 線程2讀取了數據A
  3. 線程2通過CAS比較,發現值是A沒錯,可以把數據A改成數據B
  4. 線程3讀取了數據B
  5. 線程3通過CAS比較,發現數據是B沒錯,可以把數據B改成了數據A
  6. 線程1通過CAS比較,發現數據還是A沒變,就寫成了自己要改的值FF

在這個過程中任何線程都沒做錯什麼,但是值被改變了,線程1卻沒有辦法發現,其實這樣的情況出現對結果本身是沒有什麼影響的,但是我們還是要防範,怎麼防範我下面會提到。

1.3 JUC舉例分析CAS

AtomicInteger的自增函數incrementAndGet()

其實是調用Unsafe類的getAndAddInt

    public final int incrementAndGet() {
        return U.getAndAddInt(this, VALUE, 1) + 1;
    }


public final int getAndAddInt(Object o, long offset, int delta) {
        int v;
        do {
            v = this.getIntVolatile(o, offset);
        } while(!this.weakCompareAndSetInt(o, offset, v, v + delta));

        return v;
    }

大概意思就是循環判斷給定偏移量是否等於內存中的偏移量,直到成功才退出,看到do while的循環沒。

1.4 實際應用保證CAS產生的ABA安全問題?

加標誌位,例如搞個自增的字段version,操作一次就自增加一,或者version字段設置爲時間戳,比較時間戳的值

舉個栗子:現在我們去要求操作數據庫,根據CAS的原則我們本來只需要查詢原本的值就好了,現在我們一同查出他的標誌位版本字段vision。

update table set value = newValue where value = #{oldValue}
//oldValue就是我們執行前查詢出來的值
update table set value = newValue ,vision = vision + 1 where value = #{oldValue} and vision = #{vision} 
// 判斷原來的值和版本號是否匹配,中間有別的線程修改,值可能相等,但是版本號100%不一樣

2. 悲觀鎖

2.1JVM 悲觀鎖synchronized

關於synchronized到底鎖住的是什麼?

https://blog.csdn.net/qq_39455116/article/details/86634362

在上面的博客中,我們可以看到:

  1. 第一種:synchronized可以讓線程A進入,A結束之後,B線程才能進入

  2. 第二種:也可以讓線程ABC在不同的時刻進入,只要不是同時就行

而且即使是第一種,當前方法A也不會對當前類裏面的其他的方法有影響,並不是說會把當前類的所有方法都鎖住,只是鎖一個方法而已

2.2 Synchronized 之Monitor對象

synchronized原理:

monitor對象
在這裏插入圖片描述

2.2.1 JDK8markword實現表

在這裏插入圖片描述

無鎖-偏向鎖-輕量級鎖(自旋鎖、自適應自旋鎖)-重量級鎖

2.2.2如何查看Java字節碼:

  1. 找到【Plugins】選項,可以首先確認一下是否安裝ByteCode Viewer插件,如果沒有安裝,可以按照下圖示意來進行搜索安裝

  2. 點擊菜單欄【View】,彈出下拉選項,在選項中找到【Show Bytecode】按鈕,單擊此按鈕,來查看java類字節碼。

2.2.3 加synchronized之後對象到底哪裏變化了?

請運行下面的代碼,注意添加Maven

<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.9</version>
</dependency>
public class CasOptimistic {
    /**
     * 查看普通數組的構成
     */
    public static void getSimpleArr() {
        int arr[] = new int[]{};
        String arrStr = ClassLayout.parseInstance(arr).toPrintable();
        System.out.println(arrStr);
        System.out.println("----------------------");
    }

    /**
     * 查看普通對象的構成
     */
    public static void getSimple() {
        Object o = new Object();
        String str = ClassLayout.parseInstance(o).toPrintable();
        System.out.println(str);
        System.out.println("----------------------");
    }


    /**
     * 查看普通對象加上鎖之後的構成,發現只有頭文件變了,說明sync只與對象頭文件相關
     */
    public static void getSyncSimple() {
        Object o = new Object();
        synchronized (o){
            String str = ClassLayout.parseInstance(o).toPrintable();
            System.out.println(str);
        }
        System.out.println("----------------------");
    }

    public static void main(String[] args) {
        getSimpleArr();
        getSimple();
        getSyncSimple();
    }
}

只要你打印下面程序的結果就會發現了,只有對象頭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)
      加上鎖之後變成了
0     4        (object header)                           05 78 e5 e8 (00000101 01111000 11100101 11101000) (-387614715)
4     4        (object header)                           47 02 00 00 (01000111 00000010 00000000 00000000) (583)

2.2.4 synchronized鎖升級過程

所以synchronized 鎖優化的過程和markword息息相關

markword中最低三位代表鎖狀態,第一位代表是否是偏向鎖,2、3位代表鎖標誌位

無鎖:對象頭裏面存儲當前對象的hashcode,即原來的Markword組成是:001+hashcode

偏向鎖:其實就是偏向一個用戶,適用場景,只有幾個線程,其中某個線程會經常訪問,他就會往對象頭裏面添加線程id,就像在門上貼個紙條一樣,佔用當前線程,只要紙條存在,就可以一直用

輕量級鎖:比如你貼個紙條,一直使用,但是其他人不樂意了,要和你搶,只要發生搶佔,synchronized就會升級變成輕量級鎖,也就是不同的線程通過CAS方式搶佔當前對象的指針,如果搶佔成功,則把剛纔的線程id改成自己棧中鎖記錄的指針LR(LockRecord),因爲是通過CAS的方式,所以也叫自旋鎖

這個時候你可能回想,無論變成什麼鎖,對象頭都會發生改變,那之前對象頭裏面存儲的hashcode會不會丟失啊?

答案:不會,在發生鎖的第一刻,他就會把原來的header存儲在自己的線程棧中,所以不會丟失

什麼時候重量級鎖?

線程非常多,比如有的線程超過10次自旋,-XX:PreBlockSpin,或者自旋次數超過CPU核數的一半,就會升級成重量級鎖,當然Java1.6之後加入了自適應自旋鎖,JVM自己控制自旋次數

而且重量級鎖是操作系統實現的

2.2.5 synchronized最底層實現

還是CAS

2.2.6 鎖降級

要求比較嚴格,而且只有偏向鎖回到無鎖的過程,其它的沒有,而且是要很長時間線程確認死了的情況下纔會有

2.2.7 鎖粗化

    /**
     * 鎖粗化 lock coarsening
     *
     * @param str
     * @return
     */
    public String test(String str) {
        int i = 0;
        StringBuffer sb = new StringBuffer();
        while (i < 100) {
            sb.append(str);
            i++;
        }
        return sb.toString();
    }

因爲stringbuffer的append()是synchronized的,但循環裏面如果每次都加鎖,就會加鎖、釋放鎖一百次,所以JVM就會將加上鎖的訪問粗化到這一連串的操作,比如while循環,只要加一次鎖即可

2.2.8 鎖消除

/**
* 鎖消除 lock eliminate
*
* @param str1
* @param str2
*/
public void add(String str1, String str2) {
StringBuffer sb = new StringBuffer();
sb.append(str1).append(str2);
}

因爲stringBuffer裏面都是synchronied,所以裏面的append就會消除鎖

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章