java並發程序都是基於多線程,作業系統為了充分利用CPU的資源,將CPU分成若干個時間片,線程會被作業系統調度進行任務切換,達到最大限度地利用CPU空閒時間。
多線程編程雖然可以最大限度地利用CPU,但也突顯了三個問題:原子性問題,可見性問題,有序性問題。
原子性atomic
原子性指的是一個或者多個操作在CPU執行過程中不被中斷的特性。即要不全部執行完成,要不不執行。
請看上面的代碼塊,我們來分析下操作2:
指令1:把變量a從內存加載到CPU寄存器中;指令2:在寄存器中執行+1操作;指令3:將結果寫入內存。通過上面的三步指令操作,完成了「a++」的操作。
假設線程A在完成「指令1」後切換到線程B,線程B完成操作2後切換到線程A,A繼續完成指令2和指令3的步驟,我們發現兩個線程都執行了「a++」的操作,但是結果卻不是我們期望的2,而是1。
這就是原子性的問題,為了解決這個問題,我們需要用到互斥來使操作變為原子性。方法為:使用鎖或者使用原子函數。
可見性volatile
可見性指的是當一個線程修改了共享變量後,其他線程能夠立即得到修改的值。
同樣是上圖中的代碼塊,我們做下規定:操作1和操作2是線程A執行的代碼;操作3是線程B執行的代碼。
假設執行線程A的是CPU1,執行線程B的是CPU2。當線程A執行「a++」這句的時候,會先把a的初始值加載到CPU1的寄存器中,然後自增1,那麼CPU1中的寄存器中a的值變為1,卻沒有立即寫入內存中。
此時線程B執行操作3「b=a」代碼,它會去內存中讀取a的值,此時內存中i的值還是0,那麼完成操作3後,b的值為0,而不是1。
這就是可見性的問題,線程A對變量a修改後,線程B沒有立即看到線程A修改的值。為了解決這個問題,我們設置變量為volatile可見性。volatile的特殊規則保證了新值能立即同步到內存中,已經沒使用前立即從內存刷新數據。
有序性
有序性指的是程序執行的順序按照代碼的先後順序執行。
一般來說,處理器為了提高程序的的運行效率,可能會對輸入代碼進行優化,它不保證程序中各個操作語句的執行先後數據同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行結果的一致性。
比如上圖所示,兩種順序都有可能發生,不過不會影響代碼執行結果的一致性,因為重排序會考慮指令之間的依賴,所以不會出現操作3,在操作4後面的情況。
雖然重排序不會影響單個線程內程序的執行結果,但是多線程呢?
上圖代碼塊中,由於操作1和操作2沒有數據依賴,因此可能會被重排序。假如發生重排序,線程A執行過程中操作2發生在操作1之前,而此時線程B以為初始化工作完成,那麼就會跳出while循環,去執行doSomething(context)方法,而此時context並沒有被初始化,就會導致程序出錯。
為了解決這個問題,我們可以使用synchronized和volatile來保證多線程之間操作的有序性。volatile關鍵字會禁止指令重排;synchronized關鍵字保證同一時刻只允許一條線程操作。