## 12 什么?還有這種操作!—有序性
> 理想必須要人們去實現它,它不但需要決心和勇敢而且需要知識。
> ——吳玉章
前面我們學習了原子性和可見性。相比較而言,可見性更難理解一點,但是由于緩存已經在日常編程中大量被使用,我們并不陌生,所以理解起來也沒什么難度。不過本節要講的有序性,我們之前并沒有接觸過相關的知識,理解起來會比較抽象。
## 1\. 什么是有序性
有序性指的是代碼在運行期間保證按照編寫的順序。這句話看起來和可見性的定義一樣,好像又是一句廢話。你一定在想,代碼當然是按照編寫順序執行的,否則那還不亂套了?其實并不是這樣,代碼執行的順序還真不一定和你編寫的順序一致。多線程開發復雜就復雜在和我們的認知相違背,我們如果在做多線程開發前不一一搞清楚,那么所編寫出的并發代碼一定是漏洞百出。
## 2\. 指令重排序
說到有序性,我們一定會提到指令重排序。CPU 為了提高運行效率,可能會對編譯后代碼的指令做一些優化,這些優化不能保證 100% 符合你編寫代碼在正常編譯后的順序執行。但是一定能保證代碼執行的結果和按照編寫順序執行的結果是一致的。
指令重排序并不是毫無約束的隨意改變代碼執行順序,而是需要符合指令間的依賴關系,否則會造成程序執行結果錯誤。
我們接下來通過一個例子來理解指令重排序的必要性。
星期六早上,你要去超市進行采購,你自己想買兩斤小龍蝦,你兒子和你說要一袋巧克力,然后你老婆說家里沒有醬油了買一瓶,你媽又說買兩根胡蘿卜。那么你到了超市會死板的按照小龍蝦、巧克力、醬油、胡蘿卜的順序去采購嗎?當然不會,你肯定會大致規劃好路線,從離超市入口最近的貨架開始采購,避免走回頭路。不管你采購的順序如何,最終你肯定會保證所有人給你的需求全部實現。CPU 也是如此,雖然是機器,但它也會規劃更為合理的執行方式,確保程序運行正確的情況下,提高效率。

我們再來看一個不能重排序的例子。還是去超市采購,你媽和你說,如果買不到西葫蘆,才買胡蘿卜。那么買西葫蘆和胡蘿卜這兩個步驟就不能改變。否則假如我們先去了胡蘿卜貨架,發現自己沒買到西葫蘆,就會買胡蘿卜,然后又執行了買西葫蘆。最后的結果就是錯誤的 ---- 我們既買了西葫蘆也買了胡蘿卜。
指令重排序的優化,僅僅對單線程程序確保安全。如果在并發的情況下,程序沒能保證有序性,程序的執行結果往往會出乎我們的意料。另外注意,指令重排序,并不是代碼重排序。我們的代碼被編譯后,一行代碼可能會對應多條指令,所以指令重排序更為細粒度。
## 3\. 單例實現遇到的有序性問題
我們在實現單例的時候,有一種方式叫做雙重判斷。首先判斷 instance 是不是為空,如果為空進入同步代碼塊初始化 instance,否而直接返回 instance。初始化 instance 時再次判斷 instance 是否為空,避免了在進入同步代碼塊這段時間有線程搶先一步完成了 instance 初始化。代碼如下:
~~~java
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
~~~
這種單例的實現方式,看似在提高效率的同時,做到了天衣無縫。其實不然,因為 instance = new Singleton (); 這一行代碼會被編譯為三條指令,正常指令順序如下:
1. 為 instance 分配一塊內存 A
2. 在分配的內存 A 上初始化 instance 實例
3. 把內存 A 的地址賦值給 instance 變量
而編譯器優化后可能會變成:
1. 為 instance 分配一塊內存 A
2. 把內存 A 的地址賦值給 instance 變量
3. 在分配的內存 A 上初始化 instance 實例

可以看出在優化后第 2 和第 3 步調換了位置。調換后單線程運行是沒有問題的。但是換做多線程,假如線程 A 正在初始化 instance,此時執行完第 2 步,正在執行第三步。而線程 B 執行到 if (instance == null) 的判斷,那么線程 B 就會直接得到未初始化好的 instance,而此時線程 B 使用此 instance 顯然是有問題的。
要解決本例的有序性問題很簡單,我們只需要為 instance 聲明時增加 volatile 關鍵字,volatile 修飾的變量是會保證讀操作一定能讀到寫完的值。
## 總結
有序性是在多線程的情況下,確保 CPU 不對我們需要保證順序性的代碼進行重排序的。我們可以通過 sychronized 或者 volatile 來確保有序性。至此,關于多線程的三大特性已經學習完成。我們在多線程開發過程中要牢記原子性、可見性、有序性。千萬不要以寫單線程程序的思路來開發多線程,處理好這三大特性,多線程開發的大部分問題都會得以解決。下一節我們會來學習 Java 內存模型,其實所有的線程安全性都來自于它。
- 前言
- 第1章 Java并發簡介
- 01 開篇詞:多線程為什么是你必需要掌握的知識
- 02 絕對不僅僅是為了面試—我們為什么需要學習多線程
- 03 多線程開發如此簡單—Java中如何編寫多線程程序
- 04 人多力量未必大—并發可能會遇到的問題
- 第2章 Java中如何編寫多線程
- 05 看若兄弟,實如父子—Thread和Runnable詳解
- 06 線程什么時候開始真正執行?—線程的狀態詳解
- 07 深入Thread類—線程API精講
- 08 集體協作,什么最重要?溝通!—線程的等待和通知
- 09 使用多線程實現分工、解耦、緩沖—生產者、消費者實戰
- 第3章 并發的問題和原因詳解
- 10 有福同享,有難同當—原子性
- 11 眼見不實—可見性
- 12 什么?還有這種操作!—有序性
- 13 問題的根源—Java內存模型簡介
- 14 僵持不下—死鎖詳解
- 第4章 如何解決并發問題
- 15 原子性輕量級實現—深入理解Atomic與CAS
- 16 讓你眼見為實—volatile詳解
- 17 資源有限,請排隊等候—Synchronized使用、原理及缺陷
- 18 線程作用域內共享變量—深入解析ThreadLocal
- 第5章 線程池
- 19 自己動手豐衣足食—簡單線程池實現
- 20 其實不用造輪子—Executor框架詳解
- 第6章 主要并發工具類
- 21 更高級的鎖—深入解析Lock
- 22 到底哪把鎖更適合你?—synchronized與ReentrantLock對比
- 23 按需上鎖—ReadWriteLock詳解
- 24 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap上
- 25 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap下
- 26不讓我進門,我就在門口一直等!—BlockingQueue和ArrayBlockingQueue
- 27 倒數計時開始,三、二、一—CountDownLatch詳解
- 28 人齊了,一起行動—CyclicBarrier詳解
- 29 一手交錢,一手交貨—Exchanger詳解
- 30 限量供應,不好意思您來晚了—Semaphore詳解
- 第7章 高級并發工具類及并發設計模式
- 31 憑票取餐—Future模式詳解
- 32 請按到場順序發言—Completion Service詳解
- 33 分階段執行你的任務-學習使用Phaser運行多階段任務
- 34 誰都不能偷懶-通過 CompletableFuture 組裝你的異步計算單元
- 35 拆分你的任務—學習使用Fork/Join框架
- 36 為多線程們安排一位經理—Master/Slave模式詳解
- 第8章 總結
- 37 結束語