天天看點

【漫畫】JAVA并發程式設計 如何解決原子性問題知識回顧鎖模型JAVA中的鎖模型明确鎖和資源的關系如何保證原子性

并發程式設計BUG源頭 文章中,我們初識了并發程式設計的三個bug源頭:可見性、原子性、有序性。在 如何解決可見性和原子性 文章中我們大緻了解了可見性和有序性的解決思路,今天輪到最後一個大bug,那就是原子性。

知識回顧

【漫畫】JAVA并發程式設計 如何解決原子性問題知識回顧鎖模型JAVA中的鎖模型明确鎖和資源的關系如何保證原子性

鎖模型

【漫畫】JAVA并發程式設計 如何解決原子性問題知識回顧鎖模型JAVA中的鎖模型明确鎖和資源的關系如何保證原子性
【漫畫】JAVA并發程式設計 如何解決原子性問題知識回顧鎖模型JAVA中的鎖模型明确鎖和資源的關系如何保證原子性

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);
    }           

運作一下吧!你會發現永遠都可以達到我們想要的效果了~

除了上面代碼中修飾非靜态方法,還可以修飾靜态方法和代碼塊

// 修飾靜态方法 當修飾靜态方法的時候,鎖定的是目前類的 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個人同時進了?

現在給出兩段錯誤代碼,想一想到底為啥錯了吧?

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() 也沒有可見性保證,這就導緻并發問題了。

第二段錯誤原因:

加鎖本質就是在鎖對象的對象頭中寫入目前線程id,但是synchronized (new Object())每次在記憶體中都是新對象,是以加鎖無效。

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

答:如果多個資源彼此之間是沒有關聯的,那可以用一個鎖來保護。如果有關聯的話,那是不行的。比如說銀行轉賬操作,你給我轉賬,我賬戶多100,你賬戶少100,我不能用我的鎖來保護你,就像現實生活中我的鎖是不能保護你的财産的。

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

問:剛剛說到銀行轉賬的案例,那麼假如某天在某銀行同時發生這樣一個事,櫃員小王需要完成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包),那麼你可以很顯眼的發現它倆:

一個是lock包,一個是atomic包,隻要你英語過了四級。。我相信你都可以馬上斷定,它們可以解決原子性問題。

由于這兩個包比較重要,是以會放在後面的子產品單獨說,持續關注【胖滾豬學程式設計】公衆号吧!