并發程式設計是Java程式員最重要的技能之一,也是最難掌握的一種技能。它要求程式設計者對計算機最底層的運作原理有深刻的了解,同時要求程式設計者邏輯清晰、思維缜密,這樣才能寫出高效、安全、可靠的多線程并發程式。本系列會從線程間協調的方式(wait、notify、notifyAll)、Synchronized及Volatile的本質入手,詳細解釋JDK為我們提供的每種并發工具和底層實作機制。
Java并發程式設計系列:
Java 并發程式設計:核心理論
Java并發程式設計:Synchronized及其實作原理
Java并發程式設計:Synchronized底層優化(輕量級鎖、偏向鎖)
Java 并發程式設計:線程間的協作(wait/notify/sleep/yield/join)
Java 并發程式設計:volatile的使用及其原理
并發程式設計是Java程式員最重要的技能之一,也是最難掌握的一種技能。它要求程式設計者對計算機最底層的運作原理有深刻的了解,同時要求程式設計者邏輯清晰、思維缜密,這樣才能寫出高效、安全、可靠的多線程并發程式。本系列會從線程間協調的方式(wait、notify、notifyAll)、Synchronized及Volatile的本質入手,詳細解釋JDK為我們提供的每種并發工具和底層實作機制。在此基礎上,我們會進一步分析java.util.concurrent包的工具類,包括其使用方式、實作源碼及其背後的原理。本文是該系列的第一篇文章,是這系列中最核心的理論部分,之後的文章都會以此為基礎來分析和解釋。
一、共享性
資料共享性是線程安全的主要原因之一。如果所有的資料隻是線上程内有效,那就不存線上程安全性問題,這也是我們在程式設計的時候經常不需要考慮線程安全的主要原因之一。但是,在多線程程式設計中,資料共享是不可避免的。最典型的場景是資料庫中的資料,為了保證資料的一緻性,我們通常需要共享同一個資料庫中資料,即使是在主從的情況下,通路的也同一份資料,主從隻是為了通路的效率和資料安全,而對同一份資料做的副本。我們現在,通過一個簡單的示例來示範多線程下共享資料導緻的問題:
代碼段一:
上述代碼的目的是對count進行加一操作,執行1000次,不過這裡是通過10個線程來實作的,每個線程執行100次,正常情況下,應該輸出1000。不過,如果你運作上面的程式,你會發現結果卻不是這樣。下面是某次的執行結果(每次運作的結果不一定相同,有時候也可能擷取到正确的結果):

可以看出,對共享變量操作,在多線程環境下很容易出現各種意想不到的的結果。
二、互斥性
資源互斥是指同時隻允許一個通路者對其進行通路,具有唯一性和排它性。我們通常允許多個線程同時對資料進行讀操作,但同一時間内隻允許一個線程對資料進行寫操作。是以我們通常将鎖分為共享鎖和排它鎖,也叫做讀鎖和寫鎖。如果資源不具有互斥性,即使是共享資源,我們也不需要擔心線程安全。例如,對于不可變的資料共享,所有線程都隻能對其進行讀操作,是以不用考慮線程安全問題。但是對共享資料的寫操作,一般就需要保證互斥性,上述例子中就是因為沒有保證互斥性才導緻資料的修改産生問題。Java 中提供多種機制來保證互斥性,最簡單的方式是使用Synchronized。現在我們在上面程式中加上Synchronized再執行:
代碼段二:
現在再執行上述代碼,會發現無論執行多少次,傳回的最終結果都是1000。
三、原子性
原子性就是指對資料的操作是一個獨立的、不可分割的整體。換句話說,就是一次操作,是一個連續不可中斷的過程,資料不會執行的一半的時候被其他線程所修改。保證原子性的最簡單方式是作業系統指令,就是說如果一次操作對應一條作業系統指令,這樣肯定可以能保證原子性。但是很多操作不能通過一條指令就完成。例如,對long類型的運算,很多系統就需要分成多條指令分别對高位和低位進行操作才能完成。還比如,我們經常使用的整數 i++ 的操作,其實需要分成三個步驟:(1)讀取整數 i 的值;(2)對 i 進行加一操作;(3)将結果寫回記憶體。這個過程在多線程下就可能出現如下現象:
這也是代碼段一執行的結果為什麼不正确的原因。對于這種組合操作,要保證原子性,最常見的方式是加鎖,如Java中的Synchronized或Lock都可以實作,代碼段二就是通過Synchronized實作的。除了鎖以外,還有一種方式就是CAS(Compare And Swap),即修改資料之前先比較與之前讀取到的值是否一緻,如果一緻,則進行修改,如果不一緻則重新執行,這也是樂觀鎖的實作原理。不過CAS在某些場景下不一定有效,比如另一線程先修改了某個值,然後再改回原來值,這種情況下,CAS是無法判斷的。
四、可見性
要了解可見性,需要先對JVM的記憶體模型有一定的了解,JVM的記憶體模型與作業系統類似,如圖所示:
從這個圖中我們可以看出,每個線程都有一個自己的工作記憶體(相當于CPU進階緩沖區,這麼做的目的還是在于進一步縮小存儲系統與CPU之間速度的差異,提高性能),對于共享變量,線程每次讀取的是工作記憶體中共享變量的副本,寫入的時候也直接修改工作記憶體中副本的值,然後在某個時間點上再将工作記憶體與主記憶體中的值進行同步。這樣導緻的問題是,如果線程1對某個變量進行了修改,線程2卻有可能看不到線程1對共享變量所做的修改。通過下面這段程式我們可以示範一下不可見的問題:
從直覺上了解,這段程式應該隻會輸出100,ready的值是不會列印出來的。實際上,如果多次執行上面代碼的話,可能會出現多種不同的結果,下面是我運作出來的某兩次的結果:
當然,這個結果也隻能說是有可能是可見性造成的,當寫線程(WriterThread)設定ready=true後,讀線程(ReaderThread)看不到修改後的結果,是以會列印false,對于第二個結果,也就是執行if (!ready)時還沒有讀取到寫線程的結果,但執行System.out.println(ready)時讀取到了寫線程執行的結果。不過,這個結果也有可能是線程的交替執行所造成的。Java 中可通過Synchronized或Volatile來保證可見性,具體細節會在後續的文章中分析。
五、有序性
為了提高性能,編譯器和處理器可能會對指令做重排序。重排序可以分為三種:
(1)編譯器優化的重排序。編譯器在不改變單線程程式語義的前提下,可以重新安排語句的執行順序。
(2)指令級并行的重排序。現代處理器采用了指令級并行技術(Instruction-Level Parallelism, ILP)來将多條指令重疊執行。如果不存在資料依賴性,處理器可以改變語句對應機器指令的執行順序。
(3)記憶體系統的重排序。由于處理器使用緩存和讀/寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行。
我們可以直接參考一下JSR 133 中對重排序問題的描述:
(1) (2)
先看上圖中的(1)源碼部分,從源碼來看,要麼指令 1 先執行要麼指令 3先執行。如果指令 1 先執行,r2不應該能看到指令 4 中寫入的值。如果指令 3 先執行,r1不應該能看到指令 2 寫的值。但是運作結果卻可能出現r2==2,r1==1的情況,這就是“重排序”導緻的結果。上圖(2)即是一種可能出現的合法的編譯結果,編譯後,指令1和指令2的順序可能就互換了。是以,才會出現r2==2,r1==1的結果。Java 中也可通過Synchronized或Volatile來保證順序性。
六 總結
本文對Java 并發程式設計中的理論基礎進行了講解,有些東西在後續的分析中還會做更詳細的讨論,如可見性、順序性等。後續的文章都會以本章内容作為理論基礎來讨論。如果大家能夠很好的了解上述内容,相信無論是去了解其他并發程式設計的文章還是在平時的并發程式設計的工作中,都能夠對大家有很好的幫助。
作者:liuxiaopeng
部落格位址:http://www.cnblogs.com/paddix/
聲明:轉載請在文章頁面明顯位置給出原文連接配接。