從單例的雙重檢查鎖想到的

  常說的單例有懶漢跟餓漢兩種寫法。餓漢因爲類加載的時候就建立了對象,所以不存在併發拿到不一樣對象的問題,但會因爲開始就加載了對象,可能會形成一些啓動緩慢等性能問題;而懶漢雖然避免了這個問題,但普通的寫法會在高併發環境下建立多個對象,單純加synchronize又會明顯下降併發效率,較好的兩種寫法是靜態內部類跟雙重檢查鎖兩種。
  雙重檢查鎖這個,你們都很熟悉了,上代碼:
public class SingleTest {
    private static SingleTest singleTest;
    //獲取單例的方法
    public static SingleTest getInstance() {
        if(singleTest == null){
            synchronized (SingleTest.class){
                if(singleTest == null){
                    singleTest = new SingleTest();
                }
            }
        }
        return singleTest;
    }
}
  這個完美了麼,並無。實際是,在多線程環境下以上寫法有時會報錯。緣由是java的線程內重排序致使的,正確作法應該在singleTest上添加volatile進行修飾。
  咱們知道volatile能夠保證可見性,synchronized能夠保證可見性一致性跟原子性。既然synchronized比volatile還要強大,爲何還要volatile進行修飾呢?
  先來分析錯誤緣由:AB兩個線程同時獲取單例,A先進入同步塊,進行new操做,但這個new實際要分爲3步進行:
    memory = allocate(); // 一、分配對象內存空間
    ctorInstance(memory); // 二、初始化對象
    instance = memory; // 三、設置instance指向剛分配的內存地址
  其中2,3是能夠重排序的。若是發生了重排序,這時候雖然synchronized並無執行結束,但若是已經執行了3,則B線程在執行第5行時,可能會讀到singleTest的值已經不是null,而此時並無執行2,若是B線程直接將數據返回出去使用,是會有問題的。
  注意:這裏是可能會有這種狀況,並不必定每次都會發生。《java併發編程的藝術》書中有對此部分的解釋。記得在第二次看的時候發現了一個疑問,而且跟一羣人討論了很久:synchronized是基於monitor機制的,在monitorexit的時候回強制刷新線程內存到主存,這也是synchronized可見性的保證;但這個地方,A線程尚未執行monitorexit,爲何B線程就讀到了還沒徹底賦值的singleTest呢,這裏是否是有什麼問題,說好的原子性呢,不是說在synchronized結束前其它線程沒法訪問麼?
  先解釋問題:這個synchronized的可見性,上面的理解沒啥問題。但jvm發展至今,實際已經針對如今的硬件作了很大程度優化,基本上很大程度的保障了工做內存跟主內存的及時同步,至關於默認使用了一個不太靠譜的volatile。也就是有monitorexit固然會刷新會主存,但沒有到monitorexit的時候,其實也是會刷會主存的,這就解釋了這裏多線程的時候會出問題的緣由。解決的話,加上volatile,禁止了重排序,等讀到有值的時候已經初始化完了,固然也就不會有問題了。
  最後再來看一下:synchronized的這個原子性,網上的解釋是這麼說的:
   【衆所周知,原子是構成物質的基本單位(固然電子等暫且不論),因此原子的意思表明着——「不可分」;由不可分性可知,原子性是拒絕多線程操做的(只有分解爲多步操做,多個線程才能對其操做:就像一個盒子裏有多個兵乓球,多我的可以從盒子裏拿乒乓球;若是盒子只有一個兵乓球,一我的拿的話,其餘人就拿不到了;這就是原子性,乒乓球就具備原子性,人就至關於線程)
   簡而言之——不被線程調度器中斷的操做,如:賦值或者return。好比"a = 1;"和 "return a;"這樣的操做都具備原子性。
  原子性不管是多核仍是單核,具備原子性的量,同一時刻只能有一個線程來對它進行操做!】
  這裏強調的是同一時間段只有一個線程在執行,
  再回想一下數據庫事務的原子性,進行兩個操做,若是一個失敗了那麼另外一個也會回滾;跟synchronized的原子性說的不太同樣吧,synchronized的原子性只是同一時間只讓一個線程訪問而已,若是裏邊修改了某個公共變量,因爲jvm不定時刷新致使的可見性問題,在synchronized尚未執行完的時候,其它線程也是大機率能夠看到這個改動的。這是jvm決定的,synchronized的原子性「管不到這裏」。(jvm怎麼決定的,有啥依據~~這個,水平有限,沒搞清楚,就只能先賴給jvm了)
  再說一點,數據庫的原子性咱們是很熟悉的,但是它是執行結束了,其它事務纔會看到的麼?顯然不是,有事務的隔離級別麼,若是把隔離級別降到最低(讀未提交),A事務一修改,還沒提交,B事務就能看見啦。跟這裏的原子性還有可見性對比一下,就是java的這個可見性至關於數據庫的最低級別了。說到數據庫事務的原子性,真的必定能保證要麼所有執行,要麼回滾麼?這個固然不能,假設一個事務很是龐大,執行了一半,斷電了~~數據庫在設計上固然會考慮這個,重啓數據庫後會根據日誌來繼續執行或者回滾,但若是日誌跟數據庫的數據對不上怎麼辦,數據庫本身搞不定了,這時候固然就須要專業dba出手了。那java的原子性呢,執行到一半,異常了,,,程序員本身考慮異常處理,執行到一半,斷電了,,,,沒得辦,來電重啓唄,相關業務數據處理,程序員本身想辦法搞定(通常設計上應該會有相關處理)。
相關文章
相關標籤/搜索