解決原子性問題?腦海中有這個模型就能夠了

上一篇文章 可見性有序性,Happens-before來搞定,解決了併發三大問題中的兩個,今天咱們就聊聊如何解決原子性問題java

原子性問題的源頭就是 線程切換,但在多核 CPU 的大背景下,不容許線程切換是不可能的,正所謂「魔高一尺,道高一丈」,新規矩來了:面試

互斥: 同一時刻只有一個線程執行

實際上,上面這句話的意思是: 對共享變量的修改是互斥的,也就是說線程 A 修改共享變量時其餘線程不能修改,這就不存在操做被打斷的問題了,那麼如何實現互斥呢?編程

對併發有所瞭解的小夥伴立刻就能想到 這個概念,而且你的第一反應極可能就是使用 synchronized,這裏列出來你常見的 synchronized 的三種用法:併發

public class ThreeSync {

    private static final Object object = new Object();

    public synchronized void normalSyncMethod(){
        //臨界區
    }

    public static synchronized void staticSyncMethod(){
        //臨界區
    }

    public void syncBlockMethod(){
        synchronized (object){
            //臨界區
        }
    }
}

三種 synchronized 鎖的內容有一些差異:app

  • 對於普通同步方法,鎖的是當前實例對象,一般指 this
  • 對於靜態同步方法,鎖的是當前類的 Class 對象,如 ThreeSync.class
  • 對於同步方法塊,鎖的是 synchronized 括號內的對象

我特地在三種 synchronized 代碼裏面添加了「臨界區」字樣的註釋,那什麼是臨界區呢?工具

臨界區: 咱們把須要互斥執行的代碼當作爲臨界區

說到這裏,和你們串的知識都是表層認知,如何用鎖保護有效的臨界區纔是關鍵,這直接關係到你是否會寫出併發的 bug,瞭解過本章內容後,你會發現不管是隱式鎖/內置鎖 (synchronized) 仍是顯示鎖 (Lock) 的使用都是在找尋這種關係,關係對了,一切就對了,且看學習

上面鎖的三種方式均可以用下圖來表達:this

線程進入臨界區以前,嘗試加鎖 lock(), 加鎖成功,則進入臨界區(對共享變量進行修改),持有鎖的線程執行完臨界區代碼後,執行 unlock(),釋放鎖。針對這個模型,你們常常用搶佔廁所坑位來形容:spa

在學習 Java 早期我就是這樣記憶與理解鎖的,但落實到代碼上,咱們很容易忽略兩點:線程

  1. 咱們鎖的是什麼?
  2. 咱們保護的又是什麼?

將這兩句話聯合起來就是你的鎖可否對臨界區的資源起到保護的做用?因此咱們要將上面的模型進一步細化

現實中,咱們都知道本身的鎖來鎖本身須要保護的東西 ,這句話翻譯成你的行動語言以後你已經明確知道了:

  1. 你鎖的是什麼
  2. 你保護的資源是什麼

CPU 可不像咱們大腦這麼智能,咱們要明確說明咱們鎖的是什麼,咱們要保護的資源是什麼,它纔會用鎖保護咱們想要保護的資源(共享變量)

拿上圖來講,資源 R (共享變量) 就是咱們要保護的資源,因此咱們就要建立資源 R 的鎖來保護資源 R,細心的朋友可能發現上圖幾個問題:

LR 和 R 之間有明確的指向關係
咱們編寫程序時,每每腦子中的模型是對的,可是忽略了這個指向關係,致使本身的鎖不能起到保護資源 R 的做用(用別人家的鎖保護本身家的東西或用本身家的鎖保護別人家的東西),最終引起併發 bug, 因此在你勾畫草圖時,要明確找到這個關係

左圖 LR 虛線指向了非共享變量
咱們寫程序的時候很容易這麼作,不肯定哪一個是要保護的資源,直接大雜燴,用 LR 將要保護的資源 R 和不必保護的非共享變量一塊兒保護起來了,舉兩個例子來講你就明白這麼作的壞處了

  1. 編寫串行程序時,是不建議 try...catch 整個方法的,這樣若是出現問是很難定位的,道理同樣,咱們要用鎖精確的鎖住咱們要保護的資源就夠了,其餘無心義的資源是不要鎖的
  2. 鎖保護的東西越多,臨界區就越大,一個線程從走入臨界區到走出臨界區的時間就越長,這就讓其餘線程等待的時間越久,這樣併發的效率就有所降低,其實這是涉及到鎖粒度的問題,後續也都會作相關說明

做爲程序猿仍是簡單拿代碼說明一下內心比較踏實,且看:

public class ValidLock {
    
    private static final Object object = new Object();
    
    private int count;
    
    public synchronized void badSync(){
        //其餘與共享變量count無關的業務邏輯
        count++;
    }
    
    public void goodSync(){
        //其餘與共享變量count無關的業務邏輯
        synchronized (object){
            count++;
        }
    }
}

這裏並非說 synchronized 放在方法上很差,只是提醒你們用合適的鎖的粒度纔會更高效

在計數器程序例子中,咱們會常常這麼寫:

public class SafeCounter {

    private int count;

    public synchronized void counter(){
        count++;
    }

    public synchronized int getCount(){
        return count;
    }
}

下圖就是上面程序的模型展現:

這裏咱們鎖的是 this,能夠保護 this.count。但有些同窗認爲 getCount 方法不必加 synchronized 關鍵字,由於是讀的操做,不會對共享變量作修改,若是不加上 synchronized 關鍵字,就違背了咱們上一篇文章 happens-before 規則中的監視器鎖規則:

對一個鎖的解鎖 happens-before 於隨後對這個鎖的加鎖
也就是說對 count 的寫極可能對 count 的讀不可見,也就致使髒讀

上面咱們看到一個 this 鎖是能夠保護多個資源的,那用多個不一樣的鎖保護一個資源能夠嗎?來看一段程序:

public class UnsafeCounter {

    private static int count;

    public synchronized void counter(){
        count++;
    }

    public static synchronized int calc(){
        return count++;
    }
}

睜大眼睛仔細看,一個鎖的是 this,一個鎖的是 UnsafeCounter.class, 他們都想保護共享變量 count,你以爲如何?下圖就是行面程序的模型展現:

兩個臨界區是用兩個不一樣的鎖來保護的,因此臨界區沒有互斥關係,也就不能保護 count,因此這樣加鎖是無心義的

總結

  1. 解決原子性問題,就是要互斥,就是要保證中間狀態對外不可見
  2. 鎖是解決原子性問題的關鍵,明確知道咱們鎖的是什麼,要保護的資源是什麼,更重要的要知道你的鎖可否保護這個受保護的資源(圖中的箭頭指向)
  3. 有效的臨界區是一個入口和一個出口,多個臨界區保護一個資源,也就是一個資源有多個並行的入口和多個出口,這就沒有起到互斥的保護做用,臨界區形同虛設
  4. 鎖本身家門能保護資源就不必鎖整個小區,若是鎖了整個小區,這嚴重影響其餘業主的活動(鎖粒度的問題)

本文以 synchronized 鎖舉例來講明如何解決原子性問題,主要是幫助你們創建宏觀的理念,用於解決原子性問題,這樣後續你看到不管什麼鎖,只要腦海中回想起本節說明的模型,你會發現都是換湯不換藥,學習起來就很是輕鬆了.

到這裏併發的三大問題 有序性,可見性,原子性都有了解決方案,這是遠看併發,讓你們有了宏觀的概念;但面試和實戰都是講求細節的,接下來咱們由遠及近,逐步看併發的細節,順帶說明那些面試官常常會問到的問題

更多信息請訪問我的博客:https://dayarch.top/

靈魂追問

  1. 多個鎖鎖一個資源必定會有問題嗎?
  2. 何時須要鎖小區,而不能鎖某一戶呢?
  3. 銀行轉帳,兩人互轉和別人給本身轉,用什麼樣的鎖粒度合適呢?

提升效率工具


推薦閱讀


歡迎持續關注公衆號:「日拱一兵」

  • 前沿 Java 技術乾貨分享
  • 高效工具彙總 | 回覆「工具」
  • 面試問題分析與解答
  • 技術資料領取 | 回覆「資料」

以讀偵探小說思惟輕鬆趣味學習 Java 技術棧相關知識,本着將複雜問題簡單化,抽象問題具體化和圖形化原則逐步分解技術問題,技術持續更新,請持續關注......

相關文章
相關標籤/搜索