Java—執行緒同步

weixin_34377065發表於2017-12-12

幾個小概念

臨界資源:當多執行緒訪問同一個物件時, 這個物件叫做臨界資源

原子操作:在臨界資源中不可分割的操作叫原子操作

執行緒不安全:多執行緒同時訪問同一個物件, 破壞了不可分割的操作, 就可能發生資料不一致

“弱肉強食”的執行緒世界

大家好,我叫王大錘,我的目標是當上CEO...額 不好意思拿錯劇本了。大家好,我叫0x7575,是一個執行緒,我的線生理想是永遠最快拿到CPU。

先給大家介紹一下執行緒世界,執行緒世界是一個弱肉強食的世界,資源永遠稀缺,什麼東西都要搶,這幾個納秒我有幸拿到CPU,對int a = 20進行一次加1操作,當我從記憶體中取出a,進行加1後就失去了CPU,休息結束之後準備寫入記憶體的時候,我驚奇的發現:記憶體中的a這時候已經變成了22。

一定有執行緒趁我不在修改了資料,我左右為難,很多執行緒也都勸我不要寫入,但是迫於指令,我只能把21寫入記憶體覆蓋掉不符合我的運算邏輯的22。

以上只是一個微小的事故,類似的事情線上程世界層出不窮,所以雖然我們每一個執行緒都盡職盡責,但是在人類看來我們是引起資料不安全的禍首。

這是何等的冤枉啊,執行緒世界一直都是競爭激烈的世界,尤其是對於一些共享變數,共享資源(臨界資源),同時有多個執行緒進行爭奪使用時再正常不過的事情了。除非消除共享的資源,但是這又是不可能的,於是事情就開始僵持了。

執行緒世界出現了一把鎖

幸好還是又聰明人的,有人想到了一個解決問題的好方法。雖然不知道誰想到的注意,但是這個注意確實解決了一部分問題,解決的方案是加鎖

你想要進行對一組加鎖的程式碼進行操作嗎?想的話就先去搶到鎖,拿到鎖之後就可以對被加鎖的程式碼為所欲為了,倘若拿不到鎖的話就只能在程式碼塊門口等著,因為等的執行緒太多了,這還成為了一種社會現象(狀態),該社會現象被命名為執行緒的阻塞。

聽上去很簡單,但是實際上加鎖有很多詳細的規定的,詳情政府釋出了《關於synchronzied使用的若干規定》以及後來釋出的《關於Lock使用的若干規定》。

執行緒和執行緒之間是共享記憶體的,當多執行緒對共享記憶體進行操作的時候有幾個問題是難以避免的,競態條件(race condition)和記憶體可見性。

**競態條件:**當多執行緒訪問和操作同一物件的時候,最終結果和執行時序有關,正確性是不能夠人為控制的,可能正確也可能不正確。(如上文例子)

上文中說到的加鎖就是為了解決這個問題,常見的解決方案有:

  • 使用synchronized關鍵字
  • 使用顯式鎖(Lock)
  • 使用原子變數

**記憶體可見性:**關於記憶體可見性問題要先從記憶體和cpu的配合談起,記憶體是一個硬體,執行速度比CPU慢幾百倍,所以在計算機中,CPU在執行運算的時候,不會每次運算都和記憶體進行資料互動,而是先把一些資料寫入CPU中的快取區(暫存器和各級快取),在結束之後寫入記憶體。這個過程是及其快的,單執行緒下並沒有任何問題。

但是在多執行緒下就出現了問題,一個執行緒對記憶體中的一個資料做出了修改,但是並沒有及時寫入記憶體(暫時存放在快取中);這時候另一個執行緒對同樣的資料進行修改的時候拿到的就是記憶體中還沒有被修改的資料,也就是說一個執行緒對一個共享變數的修改,另一個執行緒不能馬上看到,甚至永遠看不到。

這就是記憶體的可見性問題。

解決這個問題的常見方法是:

  • 使用volatile關鍵字
  • 使用synchronized關鍵字或顯式鎖同步

執行緒同步

傳統的鎖 synchronzied

同步程式碼塊

每個java物件都有一個互斥鎖標記,用來分配給執行緒,synchronized(o){ } 對o加鎖的同步程式碼塊,只有拿到鎖標記的執行緒才能夠進入對o加鎖的同步程式碼塊。

同步方法

synchronized作為方法修飾符修飾的方法被稱為同步方法,表示對this加鎖的同步程式碼塊(整個方法都是一個程式碼塊)。

JDK1.5的鎖 Lock

ReentrantLock

ReentrantLock具有和synchronized相似的作用,但是更加的靈活和強大。

它是一個重入鎖(synchronized也是),所謂重入就是可以重複進入同一個函式,這有什麼用呢?

假設一種場景,一個遞迴函式,如果一個函式的鎖只允許進入一次,那麼執行緒在需要遞迴呼叫函式的時候,應該怎麼辦?退無可退,有不能重複進入加鎖的函式,也就形成了一種新的死鎖。

重入鎖的出現就解決了這個問題,實現重入的方法也很簡單,就是給鎖新增一個計數器,一個執行緒拿到鎖之後,每次拿鎖都會計數器加1,每次釋放減1,如果等於0那麼就是真正的釋放了鎖。

//建立一個鎖物件
Lock lock = new ReentrantLock();

//上鎖(進入同步程式碼塊)
lock.lock();

//解鎖(出同步程式碼塊)
lock.unlock();

//嘗試拿到鎖,如果有鎖就拿到,沒有拿到不會阻塞,返回false
tryLock();
複製程式碼

ReadWriteLock

讀寫鎖,讀寫分離。分為readLock和writeLock兩把鎖。對於readLock來說,是一把共享鎖,可以多次分配;但是當readLock鎖上的時候,呼叫writeLock是會阻塞的,反之亦然,另,寫鎖是一把普通的互斥鎖,只可以分配一次。

synchronized和ReentrantLock的區別
  1. 兩者都是互斥鎖,所謂互斥鎖:同一時間只有一個拿到鎖的執行緒才能夠去訪問加鎖的共享資源,其他的執行緒只能阻塞
  2. 都是重入鎖,用計數器實現
  3. ReentrantLock獨有特點
    1. ReenTrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的執行緒先獲得鎖
    2. ReenTrantLock提供了一個Condition(條件)類,用來實現分組喚醒需要喚醒的執行緒們,而不是像synchronized要麼隨機喚醒一個執行緒要麼喚醒全部執行緒
    3. ReenTrantLock提供了一種能夠中斷等待鎖的執行緒的機制,通過lock.lockInterruptibly()來實現這個機制
volatile關鍵字

volatile 修飾符 用來保證可見性

當一個共享變數被volatile修飾的時候,他會保證變數被修改之後立馬在記憶體中更新,另一執行緒在取值的時候需要去記憶體中讀取新的值。

注意:儘管volatile 可以保證變數的記憶體可見性,但是不能夠儲存原子性,對於b++這個操作來說,並不是一步到位的,而是分為好幾步的,讀取變數,定義常量1,變數b加1,結果同步到記憶體。雖然在每一步中獲取的都是變數的最新值,但是沒有保證b++的原子性,自然無法做到執行緒安全

相關文章