volatile

volatile關鍵字保證了多線程環境下變量的可見性與有序性,底層實現基於內存屏障(Memory Barrier)。

爲了優化性能,現代CPU工作時的指令執行順序與應用程序的代碼順序其實是不一致的(有些編譯器也會進行這種優化),也就是所謂的亂序執行技術。亂序執行可以提高CPU流水線的工作效率,只要保證數據符合程序邏輯上的正確性即可(遵循happens-before原則)。不過如今是多核時代,如果隨便亂序而不提供防護措施那是會出問題的。每一個cpu上都會進行亂序優化,單cpu所保證的邏輯次序可能會被其他cpu所破壞。

內存屏障就是針對此情況的防護措施。可以認爲它是一個同步點(但它本身也是一條cpu指令)。例如在IA32指令集架構中引入的SFENCE指令,在該指令之前的所有寫操作必須全部完成,讀操作仍可以亂序執行。LFENCE指令則保證之前的所有讀操作必須全部完成,另外還有粒度更粗的MFENCE指令保證之前的所有讀寫操作都必須全部完成。

內存屏障就像是一個保護指令順序的柵欄,保護後面的指令不被前面的指令跨越。將內存屏障插入到寫操作與讀操作之間,就可以保證之後的讀操作可以訪問到最新的數據,因爲屏障前的寫操作已經把數據寫回到內存(根據緩存一致性協議,不會直接寫回到內存,而是改變該cpu私有緩存中的狀態,然後通知給其他cpu這個緩存行已經被修改過了,之後另一個cpu在讀操作時就可以發現該緩存行已經是無效的了,這時它會從其他cpu中讀取最新的緩存行,然後之前的cpu纔會更改狀態並寫回到內存)。

例如,讀一個被volatile修飾的變量V總是能夠從JMM(Java Memory Model)主內存中獲得最新的數據。因爲內存屏障的原因,每次在使用變量V(通過JVM指令use,後面說的也都是JVM中的指令而不是cpu)之前都必須先執行load指令(把從主內存中得到的數據放入到工作內存),根據JVM的規定,load指令必須發生在read指令(從主內存中讀取數據)之後,所以每次訪問變量V都會先從主內存中讀取。相對的,寫操作也因爲內存屏障保證的指令順序,每次都會直接寫回到主內存。

不過volatile關鍵字並不能保證操作的原子性,對該變量進行併發的連續操作是非線程安全的,所幸ConcurrentHashMap只是用來確保訪問到的變量是最新的,所以也不會發生什麼問題。

出於性能考慮,Doug Lea(java.util.concurrent包的作者)直接通過Unsafe類來對table進行操作。

Java號稱是安全的編程語言,而保證安全的代價就是犧牲程序員自由操控內存的能力。像在C/C++中可以通過操作指針變量達到操作內存的目的(其實操作的是虛擬地址),但這種靈活性在新手手中也經常會帶來一些愚蠢的錯誤,比如內存訪問越界。

Unsafe從字面意思可以看出是不安全的,它包含了許多本地方法(在JVM平臺上運行的其他語言編寫的程序,主要爲C/C++,由JNI實現),這些方法支持了對指針的操作,所以它才被稱爲是不安全的。雖然不安全,但畢竟是由C/C++實現的,像一些與操作系統交互的操作肯定是快過Java的,畢竟Java與操作系統之間還隔了一層抽象(JVM),不過代價就是失去了JVM所帶來的多平臺可移植性(本質上也只是一個c/cpp文件,如果換了平臺那就要重新編譯)。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章