先說明下,本文要討論的多線程讀寫是指一個線程寫,一個或多個線程讀,不包括多線程同時寫的狀況。html
試想下這樣一個場景:一個線程往hashmap中寫數據,一個線程往hashmap中讀數據。 這樣會有問題嗎?若是有,那是什麼問題?java
相信你們都知道是有問題的,但至於究竟是什麼問題,可能就不是那麼顯而易見了。mysql
問題有兩點。
一是內存可見性的問題,hashmap存儲數據的table並無用voliate修飾,也就是說讀線程可能一直讀不到數據的最新值。
二是指令重排序的問題,get的時候可能獲得的是一箇中間狀態的數據,咱們看下put方法的部分代碼。sql
final V putVal(int hash, K key, V value, boolean onlyIfAbsent,
boolean evict) {
...
if ((p = tab[i = (n - 1) & hash]) == null)
tab[i] = new Node<>(hash, key, value, next);
...
}
複製代碼
能夠看到,在put操做時,若是table數組的指定位置爲null,會建立一個Node對象,並放到table數組上。但咱們知道jvm中 tab[i] = new Node<>(hash, key, value, next);
這樣的操做不是原子的,而且可能由於指令重排序,致使另外一個線程調用get取tab[i]的時候,拿到的是一個尚未調用完構造方法的對象,致使不可預料的問題發生。數據庫
上述的兩個問題能夠說都是由於HashMap中的內部屬性沒有被voliate修飾致使的,若是HashMap中的對象所有由voliate修飾,則一個線程寫,一個線程讀的狀況是不會有問題(這裏是個人猜想,證明這個猜想正確性的一點依據是ConcurrentHashMap的get並無加鎖,也就是說在Map結構裏讀寫實際上是不衝突)。見下方區sora-zero
同窗的評論數組
有的同窗對於 Object obj = new Object();
這樣的操做在多線程的狀況下會拿到一個未初始化的對象這點可能有疑惑,這裏也作個簡單的說明。以上java語句分爲4個步驟:bash
以上步驟看起來也是沒有問題的,畢竟建立的對象要調用完構造方法後纔會被引用。多線程
但問題是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 變量來講,其賦值是原子的。mvc
具體能夠看這裏docs.oracle.com/javase/spec…
跳出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機制實現非鎖定讀。
原文:Java架構筆記