並發程序正確地執行,必須要保證原子性、可見性以及有序性。只要有一個沒有被保證,就有可能會導致程序運行不正確。
原子性:一個操作或多個操作要么全部執行完成且執行過程不被中斷,要么就不執行。
可見性:當多個線程同時訪問同一個變量時,一個線程修改了這個變量的值,其他線程能夠立即看得到修改的值。
有序性:程序執行的順序按照代碼的先后順序執行。
對於單線程,在執行代碼時jvm會進行指令重排序,處理器為了提高效率,可以對輸入代碼進行優化,它不保證程序中各個語句的執行先后順序同代碼中的順序一致,但是它會保證保存最終執行結果和代碼順序執行的結果是一致的。
Java語言對原子性、可見性、有序性的保證
1、原子性
在Java中,對基本數據類型的變量的讀取和賦值操作是原子性操作,即這些操作是不可被中斷,要么執行,要么不執行。
X=10; //原子性(簡單的讀取、將數字賦值給變量)
Y = x; //變量之間的相互賦值,不是原子操作
X++; //對變量進行計算操作
X = x+1;
語句2實際包括兩個操作,它先要去讀取x的值,再將y值寫入,兩個操作分開是原子性的。合在一起就不是原子性的。
語句3、4:x++ x=x+1包括3個操作:讀取x的值,x+1,將x寫入
注:可以通過 synchronized和Lock實現原子性。因為synchronized和Lock能夠保證任一時刻只有一個線程訪問該代碼塊。
2、可見性
Java提供了volatile關鍵字保證可見性。
當一個共享變量被volatile修飾時,它會保證修改的值立即被其他的線程看到,即修改的值立即更新到主存中,當其他線程需要讀取時,它會去內存中讀取新值。
Synchronized和Lock也可以保證可見性,因為它們可以保證任一時刻只有一個線程能訪問共享資源,並在其釋放鎖之前將修改的變量刷新到內存中,
3、有序性
在Java里面,可以通過volatile關鍵字來保證一定的“有序性”(具體原理在下一節講述volatile關鍵字)。另外可以通過synchronized和Lock來保證有序性,很顯然,synchronized和Lock保證每個時刻是有一個線程執行同步代碼,相當於是讓線程順序執行同步代碼,自然就保證了有序性。
Java內存模型:每個線程都有自己的工作內存(類似於前面的高速緩存)。線程對變量的所有操作都必須在工作內存中進行,而不能直接對主存進行操作。並且每個線程不能訪問其他線程的工作內存。
Java內存模型具備一些先天的“有序性”,即不需要通過任何手段就能夠得到保證的有序性,這個通常也稱為happens-before 原則。如果兩個操作的執行次序無法從happens-before原則推導出來,那么它們就不能保證它們的有序性,虛擬機可以隨意地對它們進行重排序。
指令重排序:java語言規范規定JVM線程內部維持順序化語義。即只要程序的最終結果與它順序化情況的結果相等,那么指令的執行順序可以與代碼順序不一致,此過程叫指令的重排序。
指令重排序的意義:JVM能根據處理器特性(CPU多級緩存系統、多核處理器等)適當的對機器指令進行重排序,使機器指令能更符合CPU的執行特性,最大限度的發揮機器性能。
下面就來具體介紹下happens-before原則(先行發生原則):
程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在后面的操作
鎖定規則:一個unLock操作先行發生於后面對同一個鎖額lock操作
volatile變量規則:對一個變量的寫操作先行發生於后面對這個變量的讀操作。如果一個線程先去寫一個變量,然后一個線程去進行讀取,那么寫入操作肯定會先行發生於讀操作。
傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C。
線程啟動規則:Thread對象的start()方法先行發生於此線程的每個一個動作
線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生
線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行
對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始