源碼解惑-Picasso:在 synchronized 保證可見性的狀況下爲何要加 volatile ?

這是一篇什麼樣的文章?

有這樣一些知識,它們在業務代碼中不多派上用場,可是又頻繁出如今面試題中,這些知識被稱爲基礎知識。爲了提升自身水平或者經過面試,咱們就會搜索一些闡明基礎知識的文章來學習這些知識。看完後你可能也能夠說得頭頭世道,但若是不在實踐中把這些知識歸入思考,若是不寫出貨真價實心知肚明的代碼,很難說真的學會了。java

闡明基礎知識的文章因爲篇幅限制和爲了闡述簡單,一般只能舉一些簡單的例子,看完後會讓人以爲似懂非懂。我認爲在闡明基礎知識的文章以外,還須要一種文章。這種文章不只能夠提供一個真實的應用場景讓咱們思考,還可讓咱們看到這些知識如何在思考後變成實實在在的代碼。我想到的是分析開源框架,不是分析框架的總體架構,而是基於框架的某個應用場景,分析代碼細節。android

基礎知識

這篇文章涉及的是 Java 多線程的基礎知識:synchronized 和 volatile 。多線程涉及到對 Java 運行機制的瞭解,還和人類習慣的過程式思惟衝突,因此比較難。若是對 synchronized 和 volatile 尚未一個基礎瞭解,能夠先去搜索相關的介紹文章,再回來看下文。git

下面直接開始本文的應用場景。github

應用場景

這段代碼位於 Picasso 1.0.0 的 UrlConnectionLoader。Picasso 是 android 的一個圖片加載框架。面試

UrlConnectionLoader 的功能主要是根據圖片的網絡連接下載圖片資源,所使用的網絡庫是 android 自帶的 HttpURLConnection。

和 Glide 同樣,一開始 Picasso 也是利用 DiskLruCache 來作圖片的硬盤緩存的,後來它可能以爲利用網絡庫的 http 響應緩存作硬盤緩存更加簡單方便,就把 DiskLruCache 去掉了。要給 HttpURLConnection 設置響應緩存須要調用 HttpResponseCache.intall 。緩存

static Object install(Context context) throws IOException {
    File cacheDir = new File(context.getCacheDir(), PICASSO_CACHE);
    HttpResponseCache cache = HttpResponseCache.getInstalled();
    if (cache == null) {
        cache = HttpResponseCache.install(cacheDir, MAX_SIZE);
    }
    return cache;
}
複製代碼

這步操做涉及到磁盤 I/O ,因此最好把這步操做放到後臺線程進行。Picasso 把這一步操做放到每一個網絡請求以前。 安全

這樣一來能夠實現懶加載,在第一次請求的時候,才安裝 HttpResponseCache ;二來能夠利用網絡請求的後臺線程,不須要單首創建一個後臺線程。每一個網絡請求都是不一樣的線程,而 HttpResponseCache 是一個單例對象,這樣就涉及到如何在多線程下建立單例對象了。

如何在多線程下建立單例對象

爲了保證 HttpResponseCache 只有一個,因此當 cache 爲空的才建立。網絡

if (cache == null) {
    cache = ResponseCacheHoneycombMR2.install(context);
}
複製代碼

爲了不多線程同時建立多個 HttpResponseCache 對象,因此利用 synchronized 來鎖住這個代碼塊,讓這個代碼塊一次只能由一個線程進入。多線程

synchronized (lock) {
    if (cache == null) {
        cache = ResponseCacheHoneycombMR2.install(context);
    }
}
複製代碼

爲了避免讓每一個線程遇到 synchronized 都阻塞等待獲取鎖,這樣會讓程序變慢,因此在 synchronized 以前增長一個判斷,若是 cache 不爲空,就不進入同步代碼塊。架構

if(cache == nullsynchronized (lock) {
        if (cache == null) {
            cache = ResponseCacheHoneycombMR2.install(context);
        }
    }
}
複製代碼

這樣的寫法就叫 DCL(double checked locking) 。

惑從何來

上面所說的仍是比較容易理解的,可是在上圖的代碼中,我還注意到一句註釋

// DCL + volatile should be safe after Java 5.
複製代碼

這句註釋代表單單 DCL 是不能保證 cache 的建立在多線程下是安全的,還須要給 cache 加 volatile。

static volatile Object cache;
複製代碼

若是不是有這句註釋,若是不是對 volatile 很熟悉的話(例如我)是很容易把 cache 變量聲明前的 volatile 關鍵字給忽略掉的。

在我以前的學習中,我知道 volatile 是一種輕量多線程數據同步機制:可讓某個變量的值在操做前從系統內存讀取到線程緩存中,在操做後立刻從線程緩存寫回系統內存,這樣就保證了數據在多線程的可見性。一開始我覺得加 volatile 就是保證 cache 的可見性,保證 cache 在一個線程建立賦值後,寫回內存,這樣其餘線程就能夠看到 cache 已經被建立了。原本覺得這部分代碼就這樣過去了,可是後來我又想起了 synchronized 除了保證線程互斥訪問,也保證了數據的可見性(這個知識點本文不涉及)。

那麼 synchronized 不就是和 volatile 的做用重複了嗎?爲何還要加 volatile ?可見 volatile 在這裏不是爲了數據的可見性,volatile 還有什麼做用呢?答案是避免指令重排。

指令重排

什麼是指令重排?簡單來講是爲了優化代碼運行效率,在實際運行中的代碼順序和咱們寫的代碼順序不同。

在這裏可能會發生什麼指令重排?在 new 一個對象的時候能夠簡單分紅三個步驟

  1. 申請內存空間
  2. 對象初始化
  3. 引用變量指向內存空間地址

因爲步驟 2 可能比較耗時,通過指令重排後可能變成 1,3,2。

回到咱們的場景,若是通過這樣的重排,那麼考慮這樣的一種狀況:一開始 cache 爲空,此時線程 A 發起網絡請求,拿到鎖後,對 cache 進行了步驟 1 和 3 ,此時 cache 已經被賦值,並且假設被更新回內存。又假設這時候線程 B 也發起網絡請求,那麼它在 DCL 的第一個判斷看到 cache 不爲空,就會執行下面的網絡請求,直接使用 cache 。而這時候假設線程 A 還沒執行完步驟 2,那麼線程 B 就會因爲使用一個未初始化完成的 cache 而發生錯誤。

若是加了 volatile ,就能夠保證指令不會被重排,這樣就不會發生上面的狀況。

結語

因爲篇幅和水平有限,這個「惑」可能解得不是那麼完備,甚至不是那麼準確,主要是經過 Picasso 的一個真實場景來拋出「如何在多線程的狀況下安全地建立單例」這個問題,歡迎你們指正或者提供更好的相關資料。

參考資料

相關文章
相關標籤/搜索