有這樣一些知識,它們在業務代碼中不多派上用場,可是又頻繁出如今面試題中,這些知識被稱爲基礎知識。爲了提升自身水平或者經過面試,咱們就會搜索一些闡明基礎知識的文章來學習這些知識。看完後你可能也能夠說得頭頭世道,但若是不在實踐中把這些知識歸入思考,若是不寫出貨真價實心知肚明的代碼,很難說真的學會了。java
闡明基礎知識的文章因爲篇幅限制和爲了闡述簡單,一般只能舉一些簡單的例子,看完後會讓人以爲似懂非懂。我認爲在闡明基礎知識的文章以外,還須要一種文章。這種文章不只能夠提供一個真實的應用場景讓咱們思考,還可讓咱們看到這些知識如何在思考後變成實實在在的代碼。我想到的是分析開源框架,不是分析框架的總體架構,而是基於框架的某個應用場景,分析代碼細節。android
這篇文章涉及的是 Java 多線程的基礎知識:synchronized 和 volatile 。多線程涉及到對 Java 運行機制的瞭解,還和人類習慣的過程式思惟衝突,因此比較難。若是對 synchronized 和 volatile 尚未一個基礎瞭解,能夠先去搜索相關的介紹文章,再回來看下文。git
下面直接開始本文的應用場景。github
這段代碼位於 Picasso 1.0.0 的 UrlConnectionLoader。Picasso 是 android 的一個圖片加載框架。面試
和 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 只有一個,因此當 cache 爲空的才建立。網絡
if (cache == null) {
cache = ResponseCacheHoneycombMR2.install(context);
}
複製代碼
爲了不多線程同時建立多個 HttpResponseCache 對象,因此利用 synchronized 來鎖住這個代碼塊,讓這個代碼塊一次只能由一個線程進入。多線程
synchronized (lock) {
if (cache == null) {
cache = ResponseCacheHoneycombMR2.install(context);
}
}
複製代碼
爲了避免讓每一個線程遇到 synchronized 都阻塞等待獲取鎖,這樣會讓程序變慢,因此在 synchronized 以前增長一個判斷,若是 cache 不爲空,就不進入同步代碼塊。架構
if(cache == null)
synchronized (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 一個對象的時候能夠簡單分紅三個步驟
因爲步驟 2 可能比較耗時,通過指令重排後可能變成 1,3,2。
回到咱們的場景,若是通過這樣的重排,那麼考慮這樣的一種狀況:一開始 cache 爲空,此時線程 A 發起網絡請求,拿到鎖後,對 cache 進行了步驟 1 和 3 ,此時 cache 已經被賦值,並且假設被更新回內存。又假設這時候線程 B 也發起網絡請求,那麼它在 DCL 的第一個判斷看到 cache 不爲空,就會執行下面的網絡請求,直接使用 cache 。而這時候假設線程 A 還沒執行完步驟 2,那麼線程 B 就會因爲使用一個未初始化完成的 cache 而發生錯誤。
若是加了 volatile ,就能夠保證指令不會被重排,這樣就不會發生上面的狀況。
因爲篇幅和水平有限,這個「惑」可能解得不是那麼完備,甚至不是那麼準確,主要是經過 Picasso 的一個真實場景來拋出「如何在多線程的狀況下安全地建立單例」這個問題,歡迎你們指正或者提供更好的相關資料。