多線程同步

目錄

1. 爲什麼引入同步機制

2. 競態條件和內存可見性

2.1 競態條件

2.2 內存可見性

3. 線程同步方法

3.1 synchronzied

3.2 JDK1.5的鎖 Lock

3.3 volatile關鍵字


1. 爲什麼引入同步機制

多線程爲什麼要採用同步機制,因爲不同的線程有自己的棧,棧中可能引用了多個對象,而多個線程可能引用到了堆中的同一個或多個對象,而線程的棧內存當中的數據只是臨時數據,最終都是要刷新到堆中的對象內存,這裏的刷新並不是最終的狀態一次性刷新,而是在程序執行的過程中隨時刷新(肯定有固定的機制,暫不考慮),也許在一個線程中被應用對象中的某一個方法執行到一半的時候就將該對象的變量狀態刷新到了堆的對象內存中,那麼再從多線程角度來看,當多個線程對同一個對象中的同一個變量進行讀寫的時候,就會出現類似數據庫中的併發問題。

假設銀行裏某一用戶賬戶有1000元,線程A讀取到1000,並想取出這1000元,並且在棧中修改成了0但還沒有刷新到堆中,線程B也讀取到1000,此時賬戶刷新到銀行系統中,則賬戶的錢變成了0,這個時候也想去除1000,再次刷新到行系統中,賬號的錢變成0,這個時候A,B都取出1000元,但是賬戶只有1000,顯然出現了問題。針對上述問題,假設我們添加了同步機制,那麼就可以很容易的解決。

怎樣解決這種問題呢,在線程使用一個資源時爲其加鎖即可。訪問資源的第一個線程爲其加上鎖以後,其他線程便不能再使用那個資源,除非被解鎖。

2. 競態條件和內存可見性

線程和線程之間是共享內存的,當多線程對共享內存進行操作的時候有幾個問題是難以避免的,競態條件和內存可見性。

2.1 競態條件

當多線程訪問和操作同一對象的時候計算的正確性取決於多個線程的交替執行時序時,就會發生競態條件

最常見的競態條件爲:

  1. 先檢測後執行。執行依賴於檢測的結果,而檢測結果依賴於多個線程的執行時序,而多個線程的執行時序通常情況下是不固定不可判斷的,從而導致執行結果出現各種問題。
  2. 延遲初始化(最典型即爲單例)

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

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

2.2 內存可見性

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

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

這就是內存的可見性問題。

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

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

3. 線程同步方法

3.1 synchronzied

同步代碼塊

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

同步方法

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

 

3.2 JDK1.5的鎖 Lock

在JavaSE5.0中新增了一個java.util.concurrent包來支持同步。ReentrantLock類是可重入、互斥、實現了Lock接口的鎖, 它與使用synchronized方法和快具有相同的基本行爲和語義,並且擴展了其能力。
ReenreantLock類的常用方法有:

//創建一個鎖對象
Lock lock = new ReentrantLock();

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

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

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

注:ReentrantLock()還有一個可以創建公平鎖的構造方法,但由於能大幅度降低程序運行效率,不推薦使用 

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

synchronized和ReentrantLock的區別

  1. 兩者都是互斥鎖,所謂互斥鎖:同一時間只有一個拿到鎖的線程才能夠去訪問加鎖的共享資源,其他的線程只能阻塞
  2. 都是重入鎖,用計數器實現;

    所謂重入就是可以重複進入同一個函數

    假設一種場景,一個遞歸函數,如果一個函數的鎖只允許進入一次,那麼線程在需要遞歸調用函數的時候,應該怎麼辦?退無可退,有不能重複進入加鎖的函數,也就形成了一種新的死鎖。

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

  3. ReentrantLock獨有特點
    1. ReenTrantLock可以指定是公平鎖還是非公平鎖。而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲得鎖
    2. ReenTrantLock提供了一個Condition(條件)類,用來實現分組喚醒需要喚醒的線程們,而不是像synchronized要麼隨機喚醒一個線程要麼喚醒全部線程
    3. ReenTrantLock提供了一種能夠中斷等待鎖的線程的機制,通過lock.lockInterruptibly()來實現這個機制

3.3 volatile關鍵字

volatile 修飾符 用來保證可見性

當一個共享變量被volatile修飾的時候,他會保證變量被修改之後立馬在內存中更新,另一線程在取值的時候需要去內存中讀取新的值。

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

 

參考:java 多線程 --同步

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章