讀寫一致性的一些思考

先說明下,本文要討論的多線程讀寫是指一個線程寫,一個或多個線程讀,不包括多線程同時寫的情況。

更多文章見作者個人博客:https://github.com/farmerjohngit/myblog

試想下這樣一個場景:一個線程往hashmap中寫數據,一個線程往hashmap中讀數據。 這樣會有問題嗎?如果有,那是什麼問題?

相信大家都知道是有問題的,但至於到底是什麼問題,可能就不是那麼顯而易見了。

問題有兩點。一是內存可見性的問題,hashmap存儲數據的table並沒有用voliate修飾,也就是說讀線程可能一直讀不到數據的最新值。二是指令重排序的問題,get的時候可能得到的是一箇中間狀態的數據,我們看下put方法的部分代碼。

可以看到,在put操作時,如果table數組的指定位置爲null,會創建一個Node對象,並放到table數組上。

但我們知道jvm中tab[i] = new Node<>(hash, key, value, next);這樣的操作不是原子的,並且可能因爲指令重排序,導致另一個線程調用get取tab[i]的時候,拿到的是一個還沒有調用完構造方法的對象,導致不可預料的問題發生。

上述的兩個問題可以說都是因爲HashMap中的內部屬性沒有被voliate修飾導致的,如果HashMap中的對象全部由voliate修飾,則一個線程寫,一個線程讀的情況是不會有問題。

(這裏是我的猜測,證實這個猜測正確性的一點依據是ConcurrentHashMap的get並沒有加鎖,也就是說在Map結構裏讀寫其實是不衝突)

創建對象的原子性問題

有的同學對於Object obj = new Object();這樣的操作在多線程的情況下會拿到一個未初始化的對象這點可能有疑惑,這裏也做個簡單的說明。以上java語句分爲4個步驟:

  1. 在棧中分配一片空間給obj引用
  2. 在jvm堆中創建一個Object對象,注意這裏僅僅是分配空間,沒有調用構造方法
  3. 初始化第2步創建的對象,也就是調用其構造方法
  4. 棧中的obj指向堆中的對象

以上步驟看起來也是沒有問題的,畢竟創建的對象要調用完構造方法後纔會被引用。

但問題是jvm是會對指令進行重排序的,重排之後可能是第4步先於第3步執行,那這時候另外一個線程讀到的就是沒有還執行構造方法的對象,導致未知問題。jvm重排只保證重排前和重排後在單線程中的結果一致性。

注意java中引用的賦值操作一定是原子的,比如說a和b均是對象的情況下不管是32位還是64位jvm,a=b操作均是原子的。

但如果a和b是long或者double原子型數據,那在32位jvm上a=b不一定是原子的(看jvm具體實現),有可能是分成了兩個32位操作。 但是對於voliate的long,double 變量來說,其賦值是原子的。具體可以看這裏https://docs.oracle.com/javase/specs/jls/se7/html/jls-17.html#jls-17.7

數據庫中讀寫一致性

跳出hashmap,在數據庫中都是要用mvcc機制避免加讀寫鎖。也就是說如果不用mvcc,數據庫是要加讀寫鎖的,那爲什麼數據庫要加讀寫鎖呢?原因是寫操作不是原子的,如果不加讀寫鎖或mvcc,可能會讀到中間狀態的數據,以HBase爲例,Hbase寫流程分爲以下幾個步驟:

1.獲得行鎖

2.開啓mvcc

3.寫到內存buffer

4.寫到append log

5.釋放行鎖

6.flush log

7.mvcc結束(這時纔對讀可見)

試想,如果沒有不走 2,7 也不加讀寫鎖,那在步驟3的時候,其他的線程就能讀到該數據。如果說3之後出現了問題,那該條數據其實是寫失敗的。也就是說其他線程曾經讀到過不存在的數據。

同理,在mysql中,如果不用mvcc也不用讀寫鎖,一個事務還沒commit,其中的數據就能被讀到,如果用讀寫鎖,一個事務會對中更改的數據加寫鎖,這時其他讀操作會阻塞,直到事務提交,對於性能有很大的影響,所以大多數情況下數據庫都採用MVCC機制實現非鎖定讀。

farmerjohngit https://github.com/farmerjohngit/myblog/issues/9

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