JVM并发机制探讨—内存模型、内存可见性和指令重排序
并發(fā)本來就是個有意思的問題,尤其是現(xiàn)在又流行這么一句話:“高帥富加機器,窮矮搓搞優(yōu)化”。從這句話可以看到,無論是高帥富還是窮矮搓都需要深入理解并發(fā)編程,高帥富加多了機器,需要協(xié)調(diào)多臺機器或者多個CPU對共享資源的訪問,因此需要了解并發(fā),窮矮搓搞優(yōu)化需要編寫各種多線程的代碼來壓榨 CPU的計算資源,讓它在同一時刻做更多的事情,這個更需要了解并發(fā)。
并發(fā)本來就是個有意思的問題,尤其是現(xiàn)在又流行這么一句話:“高帥富加機器,窮矮搓搞優(yōu)化”。從這句話可以看到,無論是高帥富還是窮矮搓都需要深入理解并發(fā)編程,高帥富加多了機器,需要協(xié)調(diào)多臺機器或者多個CPU對共享資源的訪問,因此需要了解并發(fā),窮矮搓搞優(yōu)化需要編寫各種多線程的代碼來壓榨 CPU的計算資源,讓它在同一時刻做更多的事情,這個更需要了解并發(fā)。
在我前一篇關(guān)于并發(fā)的文章http://my.oschina.net/chihz/blog/54731中提到過管程,管程的特色是在編程語言中 對并發(fā)的細(xì)節(jié)進(jìn)行封裝,使程序員可以直接在語言中就得到并發(fā)的支持,而不必自己去處理一些像是控制信號量之類容易出錯且繁瑣的細(xì)節(jié)問題。一些語言是通過在 編譯時解開語法糖的方式去實現(xiàn)管程,但Java在編譯后生成的字節(jié)碼層面上對并發(fā)仍然是一層封裝,比如syncrhonized塊在編譯之后只是對應(yīng)了兩 條指令:monitorenter和monitorexit。更多的并發(fā)細(xì)節(jié)是在JVM運行時去處理的,而不是編譯。這篇文章主要是針對JVM處理并發(fā)的 一些細(xì)節(jié)的探討。
JAVA內(nèi)存模型
對于我們平時開發(fā)的業(yè)務(wù)應(yīng)用來說,內(nèi)存應(yīng)該是訪問速度最快的存儲設(shè)備,對于頻繁訪問的數(shù)據(jù),我們總是習(xí)慣把它們放到內(nèi)存緩存中,有句話不是說么,緩 存就像是清涼油,哪里有問題就抹一抹。但是CPU的運算速度比起內(nèi)存的訪問速度還要快幾個量級,為了平衡這個差距,于是就專門為CPU引入了高速緩存,頻 繁使用的數(shù)據(jù)放到高速緩存當(dāng)中,CPU在使用這些數(shù)據(jù)進(jìn)行運算的時候就不必再去訪問內(nèi)存。但是在多CPU時代卻有一個問題,每個CPU都擁有自己的高速緩 存,內(nèi)存又是所有CPU共享的公共資源,于是內(nèi)存此時就成了一個臨界區(qū),如果控制不好各個CPU對內(nèi)存的并發(fā)訪問,那么就會產(chǎn)生錯誤,出現(xiàn)數(shù)據(jù)不一致的情 況。為了避免這種情況,需要采取緩存一致性協(xié)議來保證,這類協(xié)議有很多,各個硬件平臺和操作系統(tǒng)的實現(xiàn)不盡相同。
JVM需要實現(xiàn)跨平臺的支持,它需要有一套自己的同步協(xié)議來屏蔽掉各種底層硬件和操作系統(tǒng)的不同,因此就引入了Java內(nèi)存模型。對于Java來說 開發(fā)者并不需要關(guān)心任何硬件細(xì)節(jié),因此沒有多核CPU和高速緩存的概念,多核CPU和高速緩存在JVM中對應(yīng)的是Java語言內(nèi)置的線程和每個線程所擁有 的獨立內(nèi)存空間,Java內(nèi)存模型所規(guī)范的也就是數(shù)據(jù)在線程自己的獨立內(nèi)存空間和JVM共享內(nèi)存之間同步的問題。下面這兩張圖說明了硬件平臺和JVM內(nèi)存 模型的相似和差異之處。
Java內(nèi)存模型規(guī)定,對于多個線程共享的變量,存儲在主內(nèi)存當(dāng)中,每個線程都有自己獨立的工作內(nèi)存,線程只能訪問自己的工作內(nèi)存,不可以訪問其它 線程的工作內(nèi)存。工作內(nèi)存中保存了主內(nèi)存共享變量的副本,線程要操作這些共享變量,只能通過操作工作內(nèi)存中的副本來實現(xiàn),操作完畢之后再同步回到主內(nèi)存當(dāng) 中。如何保證多個線程操作主內(nèi)存的數(shù)據(jù)完整性是一個難題,Java內(nèi)存模型也規(guī)定了工作內(nèi)存與主內(nèi)存之間交互的協(xié)議,首先是定義了8種原子操作:
(1) lock:將主內(nèi)存中的變量鎖定,為一個線程所獨占
(2) unclock:將lock加的鎖定解除,此時其它的線程可以有機會訪問此變量
(3) read:將主內(nèi)存中的變量值讀到工作內(nèi)存當(dāng)中
(4) load:將read讀取的值保存到工作內(nèi)存中的變量副本中。
(5) use:將值傳遞給線程的代碼執(zhí)行引擎
(6) assign:將執(zhí)行引擎處理返回的值重新賦值給變量副本
(7) store:將變量副本的值存儲到主內(nèi)存中。
(8) write:將store存儲的值寫入到主內(nèi)存的共享變量當(dāng)中。
我們可以看到,要保證數(shù)據(jù)的同步,lock和unlock定義了一個線程訪問一次共享內(nèi)存的界限,有l(wèi)ock操作也必須有unlock操作,另外一 些操作也必須要成對出現(xiàn)才可以,像是read和load、store和write需要成對出現(xiàn),如果單一指令出現(xiàn),那么就會造成數(shù)據(jù)不一致的問題。 Java內(nèi)存模型也針對這些操作指定了必須滿足的規(guī)則:
(1) read和load、store和write必須要成對出現(xiàn),不允許單一的操作,否則會造成從主內(nèi)存讀取的值,工作內(nèi)存不接受或者工作內(nèi)存發(fā)起的寫入操作而主內(nèi)存無法接受的現(xiàn)象。
(2) 在線程中使用了assign操作改變了變量副本,那么就必須把這個副本通過store-write同步回主內(nèi)存中。如果線程中沒有發(fā)生assign操作,那么也不允許使用store-write同步到主內(nèi)存。
(3) 在對一個變量實行use和store操作之前,必須實行過load和assign操作。
(4) 變量在同一時刻只允許一個線程對其進(jìn)行l(wèi)ock,有多少次lock操作,就必須有多少次unlock操作。在lock操作之后會清空此變量在工作內(nèi)存中原 先的副本,需要再次從主內(nèi)存read-load新的值。在執(zhí)行unlock操作前,需要把改變的副本同步回主存。
轉(zhuǎn)載于:https://blog.51cto.com/ibin1520/1350561
總結(jié)
以上是生活随笔為你收集整理的JVM并发机制探讨—内存模型、内存可见性和指令重排序的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows Management F
- 下一篇: JDK源码笔记-java.util.Ha