Java并發(fā)——volatile關鍵字
什么是內存可見性?
這里就要提一下JMM(Java內存模型)。當線程在運行的時候,并不是直接直接修改電腦主內存中的變量的值。線程間通訊也不是直接把一個線程的變量的值傳給另一個線程,讓其刷新變量。下面是一副抽象的結構圖。

線程A要想和線程B通信,其實是通過改變主內存中的共享變量的值。具體的工作原理就是,線程A不能直接修改主內存中的值,而是在主內存和線程A中需要一個緩存區(qū)(每個線程都有自己的一個緩沖區(qū)),再將共享變量的副本拷入緩沖區(qū),在緩沖區(qū)里修改完之后再通過一些指令將副本改變的值刷新進主內存。這里刷新就會出現(xiàn)很多問題,有可能線程A修改了共享變量的值沒有刷新進去,當B需要使用共享變量的時候就會用舊值。所以這就是多線程存在一個比較大的問題。前面介紹的synchronized關鍵字,以及現(xiàn)在介紹的volatile,后面會介紹的CAS,其實都會解決這個內存可見性的問題。不過實現(xiàn)原理不同。要保證內存可見性也就是每一個線程修改一次共享變量的值,都需要讓主內存中的變量刷新,并且讓其他線程也刷新共享變量副本。
volatile如何實現(xiàn)內存可見性?
其實volatile的可見性底層原理主要是內存屏障和禁止重排序。內存屏障是在volatile變量讀操作之前加入load指令,從主內存中讀取最新的共享變量,而不是使用之前的副本值,同樣寫的時候也是加入store指令,將本地內存中的共享變量值強制刷新到主內存中。這樣就保證了每個線程拿到的volatile變量是最新的值。下面是兩個示意圖

StoreStore屏障:對于這樣的語句Store1; StoreStore; Store2,在Store2及后續(xù)寫入操作執(zhí)行前,保證Store1的寫入操作對其它處理器可見。
StoreLoad屏障:對于這樣的語句Store1; StoreLoad; Load2,在Load2及后續(xù)所有讀取操作執(zhí)行前,保證Store1的寫入對所有處理器可見。它的開銷是四種屏障中最大的。在大多數(shù)處理器的實現(xiàn)中,這個屏障是個萬能屏障,兼具其它三種內存屏障的功能
LoadLoad屏障:對于這樣的語句Load1; LoadLoad; Load2,在Load2及后續(xù)讀取操作要讀取的數(shù)據(jù)被訪問前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。也就是
LoadStore屏障:對于這樣的語句Load1; LoadStore; Store2,在Store2及后續(xù)寫入操作被刷出前,保證Load1要讀取的數(shù)據(jù)被讀取完畢。
上面四個屏障不但會強制變量刷新,而且會防止指令之間的重排序(JVM對于沒有數(shù)據(jù)依賴關系的指令會進行重排序,指令重排序也會導致變量的值讀取是錯誤的)。volatile的底層就是這樣實現(xiàn),較synchronized更為輕量級,畢竟synchronized是用來修飾方法和代碼塊的,而volatile保證的只是一個變量,所以更為輕量級。
如何優(yōu)化volatile關鍵字?
我們先來弄清楚對于英特爾酷睿i7、酷睿、Atom和 NetBurst,以及Core Solo和Pentium M處理器的L1、L2或L3緩存的高速緩存行是64個字節(jié)寬,不支持部分填充緩存行。要想優(yōu)化volatile變量運行速度,只需要將變量追加到64個字節(jié)
也就是如果一個volatile變量存入高速緩存且不足64個字節(jié)長度,這時候可能在同一個緩存行中就會再存入另一個volatile變量,而由于緩存一致性機制,當處理第一個volatile變量的時候,整個緩存行是鎖定的,這時候第二個volatile變量或者緩存行其他變量需要被其他處理器操作就必須等到第一個volatile變量操作完才能進行。如果這時volatile變量是64個字節(jié)獨占一個緩存行的時候,那么就不會有上面的影響發(fā)生。
那么什么時候我們都需要追加64個字節(jié)嗎?有下面兩種情況不需要追加
1、緩存行非64字節(jié)寬的處理器。如P6系列和奔騰處理器,它們的L1和L2高速緩存行是32個字節(jié)寬。
2、共享變量不被頻繁地寫??梢钥闯鲎芳幼止?jié)的方式是需要性能消耗的,如果共享變量不被頻繁地寫的話,鎖定的概率小,不需要追加字節(jié)。

