併發編程-原子性

併發編程-原子性

咱們都清楚當多個線程去同時作一件事情的時候,咱們須要考慮原子性、可見性、和有序性這幾個問題,本章主要說原子性,如下是闡述內容java

  1. 原子性:主要用原子性問題進行展開討論
  2. 同步鎖(synchronize):使用同步鎖解決問題
  3. MarkWord對象頭:鎖的狀態存在哪裏
  4. synchronize的鎖升級機制:多個線程搶佔資源的時候,鎖的底層狀態是如何改變的
  5. CAS機制:當無鎖化時候(例如自旋鎖的時候,cas起到的做用)

線程的原子性

【原子性】:指的是一個操做一旦進行就不能被別的操做進行干擾。咱們建立兩個線程對一個數字進行增長,兩個線程都增長數字10000,按照常理來說,結果應該是20000,實則否則數據庫

public class AtomicDemo {
    private int i =0;
    private void  mock(){
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicDemo atomicDemo=new AtomicDemo();
        Thread[] thread =new Thread[2];
        for (int i = 0; i < thread.length; i++) {
            thread[i]=new Thread(()->{
                for (int j = 0; j <10000 ; j++) {
                    atomicDemo.mock();
                }
            });
            thread[i].start();
        }
        thread[0].join();
        thread[1].join();
        System.out.println(atomicDemo.i);
    }
}

運行結果爲(證實確定有一個線程被打擾到,不然結果確定是20000,這就是一個典型的原子性問題

 分析爲何致使

以上述demo爲例,實際上當線程對 i 進行 ++ 的時候,在底層分三步編程

  • 加載 內存中 i
  • i++
  • 把++後的數據寫入內存

那咱們能夠想象一下,當線程a正在對i++併發

  • ->此時線程cpu切換到線程B
  • ->當線程b把i=0變成i=1
  • ->這個時候cpu又切換到線程a
  • ->那麼線程a按照以前執行的位置再次進行相加,可是以前的位置是i=0,因此兩個線程都循環了一次結果卻只是相加了1
  • 這就是爲何最終結果不等於20000 的緣由了

進行驗證:打開terminal 輸入 [javap -v AtomicDemo.class] 查看字節指令工具

 

 因此頗有可能在某個過程當中被別的線程打斷,從而獲得咱們預期外的結果佈局

如何解決這一問題?

實質上有不少方法解決,咱們今天只圍繞synchronized進行展開,咱們只用給這個方法加上synchronized就能夠了。性能

 

synchronize是什麼、如何使用?

synchronize就是一種排他鎖,換句話說是一種互斥鎖,他能夠同一時間只讓一個線程對你的邏輯進行訪問,那麼我咱們的邏輯受到怎麼樣的保護,或者說這個關鍵字的範圍是什麼呢?如何使用呢?優化

能夠修飾:實例方法(對象範圍)、代碼塊(取決於括號中你所放置的內容this/對象.class)、靜態方法(對象範圍)this

實例方法:atom

若是兩個線程同時訪問不一樣對象的同一個方法,那麼他們不是互斥關係,就是這個關鍵字不會保護你的邏輯

 

 

 若是訪問的是同一個對象,則會對你的邏輯進行保護,好比你的method1中寫了一個邏輯,必須等線程1執行完成線程而才能執行

 

 代碼塊:若是你的括號中寫的是this那和實例方法的做用域是同樣的

 

 可是若是括號中寫的是**.class那做用域就是無論多少線程想執行必須一個一個進行排隊

 

 靜態方法(由於靜態方法是一個類產生就產生的,因此他的做用也是全局的)

 tips: 細細想來,實際上是對象決定synchronized 的範圍,咱們想一想,若是有多個線程搶佔同一個資源,那其中一個搶佔到怎麼通知別的線程這個坑位已經被搶佔了呢?是否有一個全局的位置去存儲鎖的標記呢,那既然對象決定關鍵字的範圍,那麼一個資源是否被搶佔到,【是否搶佔標記】是否會存儲在每一個對象中呢?對的,如今就來講一下MarkWord對象頭

 MarkWord(就是對象在內存中佈局的三大部分中的2/1部分,這一部分存儲着鎖的標記)

對象在內存中的佈局分爲:

  • 對象頭(Header):這一部分由兩部分組成
    • Mark Word:這一部分就是存儲鎖的標記  
    • class 對象指針(類元信息):對象對應的原數據對象的內存地址
  • 實例數據(Instance Data):這裏存儲的就是對象的成員變量等
  • 對齊填充(Padding):這一部分屬於一個優化部分,因爲HotSpot VM的自動內存管理要求對象起始地址必須是8字節的整數倍,因此若是不是8的倍數,那就就會自動填充爲8的倍數,不然進行讀取就會消耗多餘的性能,僅此而已

tips: 由下圖能夠看出,當線程對資源進行獲取的時候,看一下這個鎖的狀態,在決定是否進行搶佔,這個是否能夠搶佔在你規定的對象中存儲,這也就是爲何線程對兩個不一樣的對象的資源進行搶佔時,其資源不受保護的緣由了,(由於他們有不一樣的鎖的狀態)

 

 

 咱們來看看是否鎖的狀態被存儲了起來

  增長這個jar,這是用來查詢對象的大小和佈局的工具,由 openjdk 提供

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

咱們用這張圖片做爲參考去查詢鎖的狀態

 

 對對象的佈局進行打印

 結果以下:

tip:咱們已經知道了鎖的狀態被存儲起來,那麼那些輕量級、重量級、等等,鎖的狀態都表明了什麼呢鎖的狀態是怎麼流轉的呢,來讓咱們繼續剖析,以及來模擬狀態而且看一下狀態markword中的狀態是否變了...

 synchronize的鎖升級機制

在jdk1.6以前只有無鎖->重量級鎖,使用‘synchronize’的流程是,若是一個線程沒有搶佔到資源,那就直接是一個重量級鎖的狀態,爲了提升性能,纔有了後面的鎖的狀態】

     synchronize鎖的類型有這幾種:

  • 無鎖:
  • 偏向鎖:就是當沒有線程對資源競爭的時候,
    • 此時線程a獲得了資源,
    • 那這個鎖就會存儲一個線程a的指向地址,
    • 下次線程a再次進入資源,就不須要搶佔鎖,直接能夠進行執行
  • 輕量級鎖:避免線程阻塞(由於如何線程阻塞,咱們再次對線程進行喚醒,那就增長了性能開銷,也就是從用戶態到內核態) -》從偏向鎖升級而來
    • 線程b也來搶佔資源,可是發現鎖已經偏向了線程a,那麼他就要升級爲輕量級鎖,輕量級鎖,使用自旋鎖進行實現
      • 自旋鎖:實際上就是用一個for循環不斷詢問時候別的線程已經執行完成,當前正在搶佔線程的線程就能夠執行,然而在循環的時候確定會牽扯到線程問題,那麼咱們用CAS進行實現
  • 重量級鎖:比較消耗性能,爲了優化,因此在1.6後引入了偏向鎖和輕量級鎖
    • 牽扯用戶態到內核態的交換(用戶態就是在java中的操做,而內核態就牽扯到cpu層的操做,因此更消耗性能),
    • 沒有得到鎖的線程會阻塞,當這個線程得到鎖的時候在進行喚醒  

 咱們來驗證是否鎖的流程是像咱們說的同樣

public class LockDemo {
    Object o=new Object();
    public static void main(String[] args) {
        LockDemo lockDemo=new LockDemo();
        System.out.println(ClassLayout.parseInstance(lockDemo).toPrintable());
        // 加鎖後的狀態
        System.out.println("加鎖以後--------");
        synchronized (lockDemo){
            System.out.println(ClassLayout.parseInstance(lockDemo).toPrintable());
        }
    }
}

咱們觀察markword中的狀態,參考查詢鎖的狀態那張圖

 

 咱們發現,按照上面的畫的流程來說,應該首先是無鎖,以後是偏向鎖啊,爲何這裏是自旋鎖的?由於偏向鎖是默認關閉的,由於在程序啓動的時候已經有一些咱們不知道的線程在底層運行了,那麼下個線程來的話直接就會執行重量級鎖了。

public class HeightLockDemo {
    public static void main(String[] args) {
        HeightLockDemo heightLockDemo = new HeightLockDemo();
        Thread thread = new Thread(() -> {
            synchronized (heightLockDemo) {
                System.out.println("線程1");
                System.out.println(ClassLayout.parseInstance(heightLockDemo).toPrintable());
            }
        });
        thread.start();
        synchronized (heightLockDemo) {
            System.out.println("主線程");
            System.out.println(ClassLayout.parseInstance(heightLockDemo).toPrintable());
        }
    }
}

 CAS機制:

【CAS(Compare And Set)】:其實就是在操做邏輯以前,拿以前的數值,和你的預期 值進行對比,若是相同那就修改你傳入的新的數值。實際上有點像數據庫的樂觀鎖

  那在輕量級鎖中大概的流程就是這樣的:首先不斷循環->判斷cas,若是cas返回ture則對鎖的狀態進行修改,固然除了cas的判斷外能夠進行breadk,確定還有自旋次數的限制,不然循環就無終止了 

在java中有使用cas的例子【sun.misc.Unsafe#compareAndSwapObject】

 

 

那麼CAS的底層又是如何實現的,由於在底層也是多個線程來搶佔這個CAS的,其實CAS的底層也是用鎖來實現的,只不過是CPU層面的鎖。

相關文章
相關標籤/搜索