【漫畫】JAVA併發編程 如何解決原子性問題

原創聲明:本文轉載自公衆號【胖滾豬學編程】,轉載務必註明出處!

併發編程BUG源頭文章中,咱們初識了併發編程的三個bug源頭:可見性、原子性、有序性。在如何解決可見性和原子性文章中咱們大體瞭解了可見性和有序性的解決思路,今天輪到最後一個大bug,那就是原子性。java

知識回顧

_1

鎖模型

_2
_3

JAVA中的鎖模型

鎖是一種通用的技術方案,Java 語言提供的 synchronized 關鍵字,就是鎖的一種實現。編程

  • synchronized 是獨佔鎖/排他鎖(就是有你沒個人意思),可是注意!synchronized並不能改變CPU時間片切換的特色,只是當其餘線程要訪問這個資源時,發現鎖還未釋放,因此只能在外面等待。
  • synchronized必定能保證原子性,由於被 synchronized 修飾某段代碼後,不管是單核 CPU 仍是多核 CPU,只有一個線程可以執行該代碼,因此必定能保證原子操做
  • synchronized也可以保證可見性和有序性。根據前第二篇文章:Happens-Before 規則之管程中鎖的規則:對一個鎖的解鎖 Happens-Before 於後續對這個鎖的加鎖。即前一個線程的解鎖操做對後一個線程的加鎖操做可見。綜合 Happens-Before 的傳遞性原則,咱們就能得出前一個線程在臨界區修改的共享變量(該操做在解鎖以前),對後續進入臨界區(該操做在加鎖以後)的線程是可見的。- synchronized 關鍵字能夠用來修飾靜態方法,非靜態方法,也能夠用來修飾代碼塊

理論說完了,來點實際的吧!首先咱們用synchronized 修飾非靜態方法來改寫第一章中原子性問題的那段代碼:併發

private long count = 0;
    
    // 修飾非靜態方法 當修飾非靜態方法的時候,鎖定的是當前實例對象 this。
    // 當該類中有多個普通方法被Synchronized修飾(同步),那麼這些方法的鎖都是這個類的一個對象this。多個線程訪問這些方法時,若是這些線程調用方法時使用的是同一個該類的對象,雖然他們訪問不一樣方法,可是他們使用同一個對象來調用,那麼這些方法的鎖就是同樣的,就是這個對象,那麼會形成阻塞。若是多個線程經過不一樣的對象來調用方法,那麼他們的鎖就是不同的,不會形成阻塞。
    private synchronized void add10K(){
        int start = 0;
        while (start ++ < 10000){
            this.count ++;
        }
    }

    public static void main(String[] args) throws InterruptedException {
        TestSynchronized2 test = new TestSynchronized2();
        // 建立兩個線程,執行 add() 操做
        Thread th1 = new Thread(()->{
            test.add10K();
        });
        Thread th2 = new Thread(()->{
            test.add10K();
        });
        // 啓動兩個線程
        th1.start();th2.start();
        // 等待兩個線程執行結束
        th1.join();th2.join();
        System.out.println(test.count);
    }

運行一下吧!你會發現永遠均可以達到咱們想要的效果了~
除了上面代碼中修飾非靜態方法,還能夠修飾靜態方法和代碼塊app

// 修飾靜態方法 當修飾靜態方法的時候,鎖定的是當前類的 Class 對象,即TestSynchronized2.class 。這個範圍就比對象鎖大。這裏就算是不一樣對象,可是隻要是該類的對象,就使用的是同一把鎖。
    synchronized static void bar() {
        // 臨界區
    }
    // 修飾代碼塊 java中經典的雙重鎖檢查機制
    private volatile static TestSynchronized2 instance;
    public static TestSynchronized2 getInstance() {
        if (instance == null) {
            synchronized (TestSynchronized2.class) {
                if (instance == null) {
                    instance = new TestSynchronized2();
                }
            }
        }
        return instance;
    }

明確鎖和資源的關係

深刻分析鎖定的對象和受保護資源的關係,綜合考慮受保護資源的訪問路徑,多方面考量才能用好互斥鎖。受保護資源和鎖之間的關聯關係是 N:1 的關係。若是一個資源用N個鎖,那確定出問題的,就好像一個廁所坑位,你有10把鑰匙,那不是能夠10我的同時進了?性能

如今給出兩段錯誤代碼,想想到底爲啥錯了吧?this

static long value1 = 0L;

    synchronized long get1() {
        return value1;
    }

    synchronized static void addOne1() {
        value1 += 1;
    }
long value = 0L;

    long get() {
        synchronized (new Object()) {
            return value;
        }
    }

第一段錯誤緣由:
由於咱們說過synchronized修飾普通方法 鎖定的是當前實例對象 this 而修飾靜態方法 鎖定的是當前類的 Class 對象
因此這裏有兩把鎖 分別是 this 和 TestSynchronized3.class
因爲臨界區 get() 和 addOne() 是用兩個鎖保護的,所以這兩個臨界區沒有互斥關係,臨界區 addOne() 對 value 的修改對臨界區 get() 也沒有可見性保證,這就致使併發問題了。atom

第二段錯誤緣由:
加鎖本質就是在鎖對象的對象頭中寫入當前線程id,可是synchronized (new Object())每次在內存中都是新對象,因此加鎖無效。spa

問:剛剛的例子都是多個鎖保護一個資源,這樣百分百是不行的。那麼一個鎖保護多個資源,就必定能夠了嗎?線程

答:若是多個資源彼此之間是沒有關聯的,那能夠用一個鎖來保護。若是有關聯的話,那是不行的。好比說銀行轉帳操做,你給我轉帳,我帳戶多100,你帳戶少100,我不能用個人鎖來保護你,就像現實生活中個人鎖是不能保護你的財產的。3d

劃重點!要區分多個資源是否有關聯!可是一個鎖保護多個沒關聯的資源,未免性能太差了哦,好比我聽歌和玩遊戲能夠同時進行,你非得讓我作完一個再作另外一個,豈不是要雙倍時間。因此即便一個鎖能夠保護多個沒關聯的資源,可是通常而已,會各自用不一樣的鎖,可以提高性能。這種鎖還有個名字,叫細粒度鎖。

問:剛剛說到銀行轉帳的案例,那麼假如某天在某銀行同時發生這樣一個事,櫃員小王須要完成A帳戶給B帳戶轉帳100元,櫃員小李須要完成B帳戶給A帳戶轉帳100元,請問如何實現呢?

答:其實用兩把鎖就實現了,轉出一把,轉入另外一把。只有當二者都成功時,才執行轉帳操做。

public static void main(String[] args) throws InterruptedException {
        Account a = new Account(200); //A的初始帳戶餘額200
        Account b = new Account(300); //B的初始帳戶餘額200
        Thread threadA = new Thread(()->{
            try {
                transfer(a,b,100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        Thread threadB = new Thread(()->{
            try {
                transfer(b,a,100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        });
        threadA.start();
        threadB.start();
    }

    static void transfer(Account source,Account target, int amt) throws InterruptedException {
        synchronized (source) {
            log.info("持有鎖{} 等待鎖{}",source,target);
            synchronized (target) {
                if (source.getBalance() > amt) {
                    source.setBalance(source.getBalance() - amt);
                    target.setBalance(target.getBalance() + amt);
                }
            }
        }
    }

至此,恭喜你,一波問題解決了,但是遺憾的告訴你:又致使了另外一個bug。這段代碼是有可能發生死鎖的!併發編程中要注意的東西可真是多喲。我們先把死鎖這個名詞記住!持續關注【胖滾豬學編程】公衆號!在咱們後面的文章中找答案!

如何保證原子性

如今咱們已經知道互斥鎖能夠保證原子性,也知道了如何使用synchronized來保證原子性。但synchronized 並非JAVA中惟一能保證原子性的方案。

若是你粗略的看一下J.U.C(java.util.concurrent包),那麼你能夠很顯眼的發現它倆:
image.png

一個是lock包,一個是atomic包,只要你英語過了四級。。我相信你均可以立刻判定,它們能夠解決原子性問題。

因爲這兩個包比較重要,因此會放在後面的模塊單獨說,持續關注【胖滾豬學編程】公衆號吧!

本文轉載自公衆號【胖滾豬學編程】 用漫畫讓編程so easy and interesting!
相關文章
相關標籤/搜索