工作中許多地方需要涉及到多線程的設計與開發,java多線程開發當中我們為了線程安全所做的任何操作其實都是圍繞多線程的三個特性:原子性、可見性、有序性展開的。針對這三個特性的資料網上已經很多了,在這里我希望在站在便於理解的角度,用相對直觀的方式闡述這三大特性,以及為什么要實現和滿足三大特性。
一、原子性
原子性是指一個操作或者一系列操作要么全部執行並且執行的過程不會被任何因素打斷,要么就都不執行。其實這句話就是在告訴你,如果有多個線程執行相同一段代碼時,而你又能夠預見到這多個線程相互之間會影響對方的執行結果,那么這段代碼是不滿足原子性的。結合到實際開發當中,如果代碼中出現這種情況,大概率是你操作了共享變量。
針對這個情況網上有個很經典的例子,銀行轉賬問題:
比如A和B同時向C轉賬10萬元。如果轉賬操作不具有原子性,A在向C轉賬時,讀取了C的余額為20萬,然后加上轉賬的10萬,計算出此時應該有30萬,但還未來及將30萬寫回C的賬戶,此時B的轉賬請求過來了,B發現C的余額為20萬,然后將其加10萬並寫回。然后A的轉賬操作繼續——將30萬寫回C的余額。這種情況下C的最終余額為30萬,而非預期的40萬。 如果A和B兩個轉賬操作是在不同的線程中執行,而C的賬戶就是你要操作的共享變量,那么不保證執行操作原子性的后果是十分嚴重的。
OK,上面的狀況我們理清楚了,由此可以引申出下列三個問題
1、哪些是共享變量
從JVM內存模型的角度上講,存儲在堆內存上數據都是線程共享的,如實例化的對象、全局變量、數組等。存儲在線程棧上的數據是線程獨享的,如局部變量、操作棧、動態鏈接、方法出口等信息。
舉個通俗的例子,如果你的執行方法相當於做菜,你可以認為每個線程都是一名廚師,方法執行時會在虛擬機棧中創建棧幀,相當於給每個廚師分配一個單獨的廚房,做菜也就是執行方法的過程中需要很多資源,里面的鍋碗瓢盆各種工具,就諸如你在方法內的局部變量是每個廚師獨享的;但如果需要使用水電煤氣等公共資源,就諸如全局變量一般是共享的,使用時需要保證線程安全。
2、哪些是原子操作
既然是要保證操作的原子性,如何判斷我的操作是否符合原子性呢,一段代碼肯定是不符合原子性的,因為它包含很多步操作。但如果只是一行代碼呢,比如上面的銀行轉賬的例子如果沒有這么復雜,共享變量“C的賬戶”只是一個簡單的count++操作呢?針對這個問題,首先我們要明確,看起來十分簡單的一句代碼,在JMM(java線程內存模型)中可能是需要多步操作的。
先來看一個經典的例子:使用程序實現一個計數器,期望得到的結果是1000,代碼如下:
public class threadCount { public volatile static int count = 0; public static void main( String[] args ) throws InterruptedException { ExecutorService threadpool = Executors.newFixedThreadPool(1000); for (int i = 0; i < 1000; i++) { threadpool.execute(new Runnable() { @Override public void run() { count++; } }); } threadpool.shutdown(); //保證提交的任務全部執行完畢 threadpool.awaitTermination(10000, TimeUnit.SECONDS); System.out.println(count); } }
運行程序你可以看到,輸出的結果並不每次都是期望的1000,這正是因為count++不是原子操作,線程不安全導致的錯誤結果。
實際上count++包含2個操作,首先它先要去讀取count的值,再將count的值寫入工作內存,雖然讀取count的值以及將count的值寫入工作內存 這2個操作都是原子性操作,但合起來就不是原子性操作了。
在JMM中定義了8中原子操作,如下圖所示,原子性變量操作包括read、load、assign、use、store、write,其實你可以理解為只有JMM定義的一些最基本的操作是符合原子性的,如果需要對代碼塊實行原子性操作,則需要JMM提供的lock、unlock、synchronized等來保證。

3、如何保證操作的原子性
使用較多的三種方式:
內置鎖(同步關鍵字):synchronized;
顯示鎖:Lock;
自旋鎖:CAS;
當然這三種實現方式和保證同步的機制上都有所不同,在這里我們不做深入的說明。
二、可見性
可見性是一種復雜的屬性,因為可見性的錯誤通常比較隱蔽並且違反我們的直覺。
我們看下面這段代碼
public class VolatileApp { //volatile private static boolean isOver = false; private static int number = 0; public static void main(String[] args) throws InterruptedException { Thread thread = new Thread(new Runnable() { @Override public void run() { while (!isOver) { //Thread.yield(); } System.out.println(number); } }); thread.start(); Thread.sleep(1000); number = 50; isOver = true; } }
如果你直接運行上面的代碼,那么你永遠也看不到number的輸出的,線程將會無限的循環下去。你可能會有疑問代碼當中明明已經把isOver設置為了false,為什么循環還不會停止呢?這正是因為多線程之間可見性的問題。在單線程環境中,如果向某個變量寫入某個值,在沒有其他寫入操作的影響下,那么你總能取到你寫入的那個值。然而在多線程環境中,當你的讀操作和寫操作在不同的線程中執行時,情況就並非你想象的理所當然,也就是說不滿足多線程之間的可見性,所以為了確保多個線程之間對內存寫入操作的可見性,必須使用同步機制。
我們來看下JMM(java線程內存模型):

- JMM中的變量指的是線程共享變量(實例變量,static字段和數組元素),不包括線程私有變量(局部變量和方法參數);
- JMM規定線程對變量的寫操作都在自己的本地內存對副本進行,不能直接寫主存中的對應變量;
- 多線程間變量傳遞通過主存完成(Java線程通信通過共享內存),線程修改變量后通過本地內存寫回主存,從主存讀取變量,彼此不允許直接通信(本地內存私有原因);
volatile
保證線程之間可見性的手段有多種,在上面的代碼中,我們就可以通過volatile修飾靜態變量來保證線程的可見性。
你可以把volatile變量看作一種削弱的同步機制,它可以確保將變量的更新操作通知到其他線程;使用volatile保證可見性相比一般的同步機制更加輕量級,開銷也相對更低。
其實這里還有另外一種情況,如果上面的代碼中你撤銷對Thread.yield()的注釋,你會發現即便沒有volatile的修飾兩個靜態變量 ,number也會正常打印輸出了,乍一看你會以為可見性是沒有問題的,其實不然,這是因為Thread.yield()的加入,使JVM幫助你完成了線程的可見性。
下面這段段話闡述的比較明確:
三、有序性
理解多線程的有序性其實是比較困難的,因為你很難直觀的去觀察到它。
有序性的本義是指程序在執行的時候,程序的代碼執行順序和語句的順序是一致的。但是在Java內存模型中,是允許編譯器和處理器對指令進行重排序的,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程並發執行的正確性。也就是說在多線程中代碼的執行順序,不一定會與你直觀上看到的代碼編寫的邏輯順序一致。
下面我們舉個簡單的例子:
線程A:
context = loadContext(); //語句1 inited = true; //語句2
線程B:
while(!inited ){ sleep } doSomethingwithconfig(context);
線程A中的代碼中語句1與語句2之間沒有必然的聯系,所以線程A是會發生重排序問題的,也就是說語句2會在語句1之前執行,這必然會影響到線程B的執行(context沒有實例化)。
其實指令的重排序之所以抽象難懂,因為它是一種較為底層的行為,是基於編譯器對你代碼進行深層優化的一種結果,結合上面的例子如果loadContext()中存在阻塞的話,優先執行語句2可以說是一種合理的行為。
四、happen-before規則
上面我們也提到了,多線程的可見性與有序性之間其實是有聯系的,如果程序沒有按你希望的順序執行,那么可見性也就無從談起。JMM(Java 線程內存模型) 中的 happen-before規則,該規則定義了 Java 多線程操作的有序性和可見性,防止了編譯器重排序對程序結果的影響。
按照官方的說法:
當一個變量被多個線程讀取並且至少被一個線程寫入時,如果讀操作和寫操作沒有happen-before關系,則會產生數據競爭問題。 要想保證操作 B 的線程看到操作 A 的結果(無論 A 和 B 是否在一個線程),那么在 A 和 B 之間必須滿足 HB 原則,如果沒有,將有可能導致重排序。 當缺少 happen-before關系時,就可能出現重排序問題。
1.程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在后面的操作; 2.鎖定規則:一個unLock操作先行發生於后面對同一個鎖額lock操作; 3.volatile變量規則:對一個變量的寫操作先行發生於后面對這個變量的讀操作; 4.傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C; 5.線程啟動規則:Thread對象的start()方法先行發生於此線程的每個一個動作; 6.線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生; 7.線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行; 8.對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始;
從上面的規則中我們可以看到,使用synchronized、volatile,加鎖lock等方式一般及可以保證線程的可見性與有序性。
通過以上對多線程三大特性的總結,可以看出多線程開發中線程安全問題主要是基於原子性、可見性、有序性實現的,在這里我根據自己的理解進行了一下簡單整理和闡述,自我感覺還是比較淺顯的,如有不足之處還望指出與海涵。
關注微信公眾號,查看更多技術文章。

