volatile關鍵字解析,synchronized,lock

jmm數據操作

在這裏插入圖片描述
在這裏插入圖片描述
在執行store指令的時候,當前線程會加一把lock鎖,一直到修改完畢主內存內的值後纔會釋放鎖。這個時候另一個線程纔會讀到新的值。
在這裏插入圖片描述

volatile

使用volatile修飾的變量會通過lock指令會開啓mesi緩存一致性協議和總線嗅探機制。也會把當前線程修改的值立刻同步到主內存。當其他線程改變共享內存(靜態變量)裏的變量的時候。其他線程會通過這個協議監聽到。然後會把原有的值置爲失效,這時候這個線程會重新從主內存中取值。這樣就保證了多個線程之間使用同一個變量的時候保證了值的同步。

volatile修飾之後會保證不會進行代碼重排。

package com.math;
public class VolatileTest {
    /***
     * 當使用volatile修飾的時候會輸出:
     * waiting..............
     * 11111111111111
     * 22222222222222
     * success................
     */
//    private static volatile boolean flag = false;
    /***
     * 當不使用volatile修飾的時候程序會在while循環的地方一直運行。
     * 會輸出:
     * waiting..............
     * 11111111111111
     * 22222222222222
     * @param args
     */
    private static boolean flag = false;
    public static void main(String[] args) throws Exception{
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("waiting..............");
                while (!flag) {
                }
                System.out.println("success................");
            }
        }).start();
        Thread.sleep(500);
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("11111111111111");
                flag = true;
                System.out.println("22222222222222");
            }
        }).start();
    }
}

在這裏插入圖片描述
在這裏插入圖片描述

如果多個線程同時用一個值做操作。可能會丟失操作。也就是線程a剛剛修改了值,在加鎖修改主內存之前。線程b也修改完成了值,但是猶豫鎖和mesi協議造成了值失效重新讀值,丟失了操作。所以他不能保證原子性。
volatile關鍵字會在多線程的情況下禁止指令重排。(單線程指令是否重排無意義,應爲他本身就保證了單線程下的最終結果一致性的語義。)

public class VolatileTest1 {

    /***
     * 如果多個線程同時用一個值做操作。可能會丟失操作。也就是線程a剛剛修改了值,在加鎖修改主內存之前。
     * 線程b也修改完成了值,但是猶豫鎖和mesi協議造成了值失效重新讀值,丟失了操作。所以他不能保證原子性。
     *
     * 輸出的結果是小於等於10000
     */
    private static volatile int a = 0;
    /***
     * 如果添加了synchronized則保證了原子性,輸出結果爲10000
     */
    //public synchronized static void add() {
    public static void add() {
        a++;
    }
    public static void main(String[] args) throws Exception {
        Thread[] thread = new Thread[10];
        for (int i = 0; i < thread.length; i++) {
            thread[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    for (int j = 0; j < 1000; j++) {
                        add();
                    }
                }
            });
            thread[i].start();
        }
        for (Thread t : thread) {
            t.join();
        }
        System.out.println("//////////" + a);
    }

}

AtomicInteger:

  1. 就如上面得代碼,我爲了多線程下保持一個數據得累加就動用synchronized這種東西,誠然隨着Java版本更新,也對synchronized做了很多優化,但是處理這種簡單的累加操作,仍然顯得“太重了”。人家synchronized是可以解決更加複雜的併發編程場景和問題的,而且,在這個場景下,你要是用synchronized,不就相當於讓各個線程串行化了麼?一個接一個的排隊,加鎖,處理數據,釋放鎖,下一個再進來。你不嫌麻煩,程序都嫌你麻煩。
  2. 那什麼可以替代呢?AtomicInteger就可以,上面說得volatile並不可以,再次提醒你volatile解決的是java併發中可見性的問題,像count++這種多線程調用其實它並沒有辦法解決,還是會發生調用出錯,所以用的時候你一定要明白你到底在幹啥?
  3. 現在介紹的AtomicInteger就解決了這個問題,不得不說前人的高智商,不愧爲大牛,原子操作的出現,鎖的核心也就誕生了。有了AtomicInteger上面的代碼前半部分就可以改爲
public static AtomicInteger count = new AtomicInteger(0);
public static void incNum() {
        count.incrementAndGet();
}

這樣就完美解決了多線程遞增問題,那麼問題來了AtomicInteger是怎麼做到的呢?難道它鎖住了?
實際上,Atomic原子類底層用的不是傳統意義的鎖機制,而是無鎖化的CAS機制,通過CAS機制保證多線程修改一個數值的安全性。
那什麼是CAS呢?他的全稱是:Compare and Set,也就是先比較再設置的意思。原理如圖:

cas:是通過Unsafe類和自旋鎖實現的。Unsafe類是rt.jar包中的一個類。他是依賴於硬件的。cas可以看成是一種系統原語,系統原語就是一種cpu的指令。執行過程中不可中斷,保證了數據最終一致性。
cas缺點:可能會造成死循環,只能保證一個對象的原子性。ABA問題。
cas可能會導致ABA問題:

  1. 比如兩個線程同時操作了一塊內存。這時候,線程a,b都把原始值1同步到了私有內存中。但是線程a執行了10秒,線程b執行了2秒。這時候線程b先把內存改爲了2,同步到了主內存中。然後他又改回了1.然後有同步到主內存中了。這時候線程a執行完去那原始值1比較的時候會發現主內存中的值也是1.他就認爲沒有人修改過主內存。
  2. ABA問題可以用AtomicStampedReference類進行解決。原理就是加上了一個版本號的比較。

在這裏插入圖片描述

  1. 首先,每個線程都會先獲取當前的值,接着走一個原子的CAS操作,原子的意思就是這個CAS操作一定是自己完整執行完的,不會被別人打斷。
    然後CAS操作裏,會比較一下說,唉!大兄弟!現在你的值是不是剛纔我獲取到的那個值啊?
    如果是的話,說明沒人改過這個值,那你給我設置成累加1之後的一個值好了!
  2. 同理,如果有人在執行CAS的時候,發現自己之前獲取的值跟當前的值不一樣,會導致CAS失敗,失敗之後,進入一個無限循環,再次獲取值,接着執行CAS操作!
    這樣就能保證數據會正常顯示而不會因爲什麼多線程而發生錯誤,這個原子操作也成爲了一種鎖機制的核心(cas)。
  3. 很神奇是不是前人的想法,但這種方式也存在缺點,當線程過多的時候,多個線程要改值發現被人改了,那它們都要自旋,原地打轉,這樣就會發生問題,不斷的再次進入下一個循環,獲取值,發起CAS操作又失敗了,再次進入下一個循環,java8提出了新類LongAdder,基本原理就是裏面實現了分段CAS的機制,有興趣可以去認識一下。

AQS全稱AbstractQueuedSynchronizer,抽象隊列同步器

ReentrantLock、ReentrantReadWriteLock底層都是基於AQS來實現的。
ReentrantLock和AQS之間的關係:
在這裏插入圖片描述
cas在AQS的作用:
在這裏插入圖片描述
看到沒cas不是用到了?而且很重要。現在我給大家解讀下這個圖,

  1. 線程1進入鎖,發起cas,取得當前state值,詢問值,是否是0,假如是就設置state爲1,線程1進入到加鎖線程。
  2. 此時線程2,也同時加鎖,取得state,cas嘗試把state從0變成1,發現不行,因爲已經被人動過了,這個時候還會看看自己之前是否加過鎖,不是,則把自己放入等待隊列。
  3. 線程1用完後,unlock會釋放鎖,重置state等於0,退出加鎖線程,去通知線程2,小老弟我好了換你來,線程2再次嘗試加鎖,cas嘗試把state值從0變1成功後把自己加入,加鎖線程。
  4. 步驟就這樣,這裏面也用到了cas,所以AQS大致就這個原理,當然你可能也聽過公平鎖和非公平鎖。
  5. 公平和非公平,給大家個比喻,就不細說了,其實這個就好比上廁所,你要進廁所才能上,有人先進去了,你排隊,後面又有人來排隊,假如是不公平的,後來是個壯漢你幹不過麼,只能讓人家先,但你也很急啊,但沒辦法人家運氣好生的這麼壯。那你能怎麼辦,只能忍一時越想越氣。
    然後就有了公平機制的出現,我先排隊肯定我先的原理,然等待隊列有了秩序。
  6. 怎麼樣簡單明瞭吧,那代碼怎麼實現的呢?很簡單隻要加個true就好,如下
ReentrantLock lock = new ReentrantLock(true);

synchronized:

屬於jvm層面的隱式鎖,通過內部的moitor(監視器鎖)來實現。montor是底層操作系統互斥鎖實現的。
是一種重入鎖(synchronznized映射成字節碼指令就是增加兩個指令:monitorenter、monitorexit,當一條線程執行時遇到monitorenter指令時,它會嘗試去獲得鎖,如果獲得鎖,那麼所計數器+1(爲什麼要加1,因爲它是可重入鎖,可根據這個瑣計數器判斷鎖狀態),如果沒有獲得鎖,那麼阻塞,
當它遇到一個monitoerexit時,瑣計數器會-1,當計數器爲0時,就釋放鎖
)。
不需要手動釋放鎖。不能中斷響應。悲觀鎖。
非公平鎖,不能保證等待鎖線程的順序

lock:

樂觀鎖,底層基於voliatile和cas實現。一般通過Reentrant類來實現。
可中斷響應,需要手動釋放鎖。默認非公平鎖。

lock,synchronized一點區別:

lock擁有比synchronized更好的語義。
lock可以通過參數設置是否爲公平鎖。
lock擁有一些操作方法,比如可以嘗試獲取鎖,中斷鎖的方法。
lock適合加載代碼量較大的邏輯中。synchronized適合鎖定少量代碼。

參考:https://blog.csdn.net/qq_36963177/article/details/86635978

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