摘要:下面具體分析的用法及原理,涉及到內(nèi)存模型可見性重排序以及偽共享等方面。緩存的使用提高了的運(yùn)行效率,但是對于多核處理器會有一些問題。需要注意的是,用于保證一個變量的可見性,但是對于這種復(fù)合操作是無法保證原子性的。
簡介
在 Java 并發(fā)編程中,volatile 是經(jīng)常用到的一個關(guān)鍵字,它可以用于保證不同的線程共享一個變量時每次都能獲取最新的值。volatile 具有鎖的部分功能并且性能比鎖更好,所以也被稱為輕量級鎖。下面具體分析 volatile 的用法及原理,涉及到內(nèi)存模型、可見性、重排序以及偽共享等方面。
內(nèi)存模型在深入理解 volatile 之前,先了解一些計算機(jī)的內(nèi)存模型。當(dāng) CPU 執(zhí)行運(yùn)算的時候,需要從內(nèi)存中取數(shù)據(jù),由于 CPU 的運(yùn)算速度遠(yuǎn)遠(yuǎn)快于內(nèi)存的讀取速度,所以 CPU 需要等數(shù)據(jù),這個過程就浪費(fèi)了 CPU 的時間。為了提高效率, 在 CPU 和內(nèi)存之間會有緩存(一般有三級緩存),緩存的讀寫速度高于內(nèi)存,容量也會比內(nèi)存小得多。當(dāng) CPU 讀數(shù)據(jù)的時候會先從緩存中讀,如果緩存未命中則會去內(nèi)存讀,并把數(shù)據(jù)放到緩存中,寫數(shù)據(jù)的時候也會先寫緩存,在適當(dāng)?shù)臅r候再將緩存中的數(shù)據(jù)刷新到內(nèi)存中。
緩存的使用提高了 CPU 的運(yùn)行效率,但是對于多核處理器會有一些問題。如果某個內(nèi)存地址的數(shù)據(jù)同時被兩個 CPU 緩存,其中一個 CPU 修改了這個地址的值,無論這個值是寫入到了緩存中還是被刷新到了內(nèi)存中,只要另一個 CPU 依然使用其緩存中的值,那還是舊值。因此對于多線程來說,需要一些手段來保證數(shù)據(jù)的一致性。
對于 Java 來說,程序運(yùn)行在 JVM 上,JVM 提供了類似的內(nèi)存抽象模型,如下圖所示。
每個線程有自己的工作內(nèi)存,相當(dāng)于緩存,所有的線程共享主內(nèi)存,相當(dāng)于系統(tǒng)中的內(nèi)存。線程之間往往會有共享變量,為了保證共享變量的可見性,需要采用 java 提供的并發(fā)技術(shù)。對于單個變量的可見性來說,volatile 是一種有效的機(jī)制。
內(nèi)存可見性先看下面的一段代碼:
int a = 1; boolean flag = false; int b = 3; // 線程1 a = 2; flag = true; // 線程2 if (flag) { b = a; }
上面的代碼如果線程 1 執(zhí)行后,線程 2 中的 flag 能立刻看到 flag 的新值嗎?根據(jù)上面介紹的 Java 內(nèi)存模型可以知道,答案是不一定。那么如何保證當(dāng)線程 1 更新 flag 之后,線程 2 能夠讀取到最新的值呢?其實很簡單,只需要給 flag 添加 volatile 修飾符。
那么 volatile 是如何做到的呢? 我們想一想,根據(jù) Java 內(nèi)存模型,要實現(xiàn)這種功能該怎么做?應(yīng)該是兩步:1. 當(dāng)線程 1 寫 volatile 變量的時候,將這個值從緩存刷新到主內(nèi)存中 2. 當(dāng)線程 2 讀取 volatile 變量的時候,將本地的工作內(nèi)存置為無效,從主內(nèi)存讀取新值。
其實 volatile 的實現(xiàn)正是以上的原理,對于一個 volatile 變量的寫操作會有一行以 lock 作為前綴的匯編代碼。這個指令在多核處理器下會引發(fā)兩件事:
將當(dāng)前處理器緩存行的數(shù)據(jù)寫回到主內(nèi)存
這個寫回內(nèi)存的操作會使在其它 CPU 里緩存了該內(nèi)存地址的數(shù)據(jù)無效
lock 前綴的指令會鎖住系統(tǒng)總線或者是緩存,目的是保證在同一時間只有一個 CPU 會修改數(shù)據(jù),使得修改具有原子性。根據(jù) 緩存一致性 協(xié)議, CPU 通過嗅探技術(shù)保證它的內(nèi)部緩存、內(nèi)存和其它處理器的緩存的數(shù)據(jù)的一致性。例如,一個處理器檢測其它處理器打算寫內(nèi)存地址,而這個地址當(dāng)前處于共享狀態(tài),那么正在嗅探的處理器將使它的緩存行無效,在下次訪問相同的內(nèi)存地址時,強(qiáng)制執(zhí)行緩存行填充。
禁止重排序volatile 除了保證內(nèi)存可見性,還可以禁止重排序。在了解重排序之前,先看一段代碼:
class Singleton { private static Singleton instance; public static Singleton getInstance() { if (instance == null) { synchronized (Singleton.class) { if (instance == null) { instance = new Singleton(); } } } return instance; } }
上面的代碼一看就是單例模式,并且使用了雙重加鎖提高效率。稍微有經(jīng)驗的程序員還會發(fā)現(xiàn),上面的寫法是不正確的,應(yīng)該給 instance 添加 volatile 修飾。那么為什么需要 volatile 呢?
其實問題出在 instance = new Singleton(); 這一行,這里是創(chuàng)建 Singleton 對象的地方,其實這里可以看成三個步驟:
memory = allocate(); //1: 分配對象的內(nèi)存空間
ctorInstance(memory); //2: 初始化對象
instance = memory; //3: 設(shè)置 instance 指向剛分配的內(nèi)存地址
上面的偽代碼可能會被重排序。什么是重排序?編譯器以及處理器有時候會為了執(zhí)行的效率改變代碼的執(zhí)行順序,這個被稱為重排序。上面的三個步驟可能會被重排序為下面的步驟:
memory = allocate(); //1: 分配對象的內(nèi)存空間
instance = memory; //2: 設(shè)置 instance 指向剛分配的內(nèi)存地址
// 注意:此時對象還沒有被初始化
ctorInstance(memory); //3: 初始化對象
在這種情況下,當(dāng)一個線程執(zhí)行到 instance = memory; 的時候,對象還沒有被初始化,另一個線程也調(diào)用了 getInstance 方法,發(fā)現(xiàn) instance 引用不為 null,就會認(rèn)為這個對象已經(jīng)創(chuàng)建好了,從而使用了未初始化的對象。
為什么 volatile 可以避免上面的問題?其實是因為 volatile 會禁止重排序,方法是插入了內(nèi)存屏障,具體原理較復(fù)雜,這里就不深入分析了。
偽共享CPU 緩存是以緩存行為單位進(jìn)行存取的,一般一個緩存行是 64 字節(jié),如果兩個 volatile 變量被緩存在同一個緩存行,并且有多個 CPU 緩存了同一行數(shù)據(jù),那么會出現(xiàn) 偽共享 的問題,造成性能問題。
例如,CPU A 以及 CPU B 都在同一個緩存行緩存了共享變量 X 和 Y,如果 CPU A 修改了 X,那么 CPU B 中的緩存行也就失效了,如果 CPU 只是需要讀取 Y ,卻因為 X 使得整個緩存行都要重新讀取,這就不劃算了,這叫做偽共享。
解決偽共享主要是讓不同的 volatile 變量不要緩存到同一個緩存行,可以利用填充技術(shù)來解決,具體可以參考這篇文章:Java中的偽共享以及應(yīng)對方案
總結(jié)volatile 作為一個輕量級的鎖可以實現(xiàn)內(nèi)存可見性以及禁止重排序,常用于修飾標(biāo)記變量以及雙重加鎖的場景等。需要注意的是,volatile 用于保證一個變量的可見性,但是對于 i++ 這種復(fù)合操作是無法保證原子性的。另外,注意偽共享問題可以進(jìn)一步提升性能。
參考
《Java 并發(fā)編程的藝術(shù)》
如果我的文章對您有幫助,不妨點個贊支持一下(^_^)
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/69440.html
摘要:結(jié)構(gòu)型模式適配器模式橋接模式裝飾模式組合模式外觀模式享元模式代理模式。行為型模式模版方法模式命令模式迭代器模式觀察者模式中介者模式備忘錄模式解釋器模式模式狀態(tài)模式策略模式職責(zé)鏈模式責(zé)任鏈模式訪問者模式。 主要版本 更新時間 備注 v1.0 2015-08-01 首次發(fā)布 v1.1 2018-03-12 增加新技術(shù)知識、完善知識體系 v2.0 2019-02-19 結(jié)構(gòu)...
摘要:原文地址游客前言金三銀四,很多同學(xué)心里大概都準(zhǔn)備著年后找工作或者跳槽。最近有很多同學(xué)都在交流群里求大廠面試題。 最近整理了一波面試題,包括安卓JAVA方面的,目前大廠還是以安卓源碼,算法,以及數(shù)據(jù)結(jié)構(gòu)為主,有一些中小型公司也會問到混合開發(fā)的知識,至于我為什么傾向于混合開發(fā),我的一句話就是走上編程之路,將來你要學(xué)不僅僅是這些,豐富自己方能與世接軌,做好全棧的裝備。 原文地址:游客kutd...
摘要:下面我們就用一個具體的例子來學(xué)習(xí)的用法。主內(nèi)存中的變量如果被線程使用到,則線程的工作內(nèi)存會維護(hù)一份主內(nèi)存變量的副本拷貝。在變量前加上關(guān)鍵字進(jìn)行修飾,這樣在計數(shù)器線程里每次讀取的值時,會強(qiáng)制該線程從主內(nèi)存讀取,而不是從當(dāng)前線程的工作內(nèi)存讀取。 相信大多數(shù)Java程序員都學(xué)習(xí)過volatile這個關(guān)鍵字的用法。百度百科上對volatile的定義: volatile是一個類型修飾符(type...
摘要:的缺點頻繁刷新主內(nèi)存中變量,可能會造成性能瓶頸不具備操作的原子性,不適合在對該變量的寫操作依賴于變量本身自己。 作者:畢來生微信:878799579 1. 什么是JUC? JUC全稱 java.util.concurrent 是在并發(fā)編程中很常用的實用工具類 2.Volatile關(guān)鍵字 1、如果一個變量被volatile關(guān)鍵字修飾,那么這個變量對所有線程都是可見的。2、如果某條線程修...
閱讀 3274·2023-04-25 18:03
閱讀 1143·2021-11-15 11:38
閱讀 5522·2021-10-25 09:45
閱讀 840·2021-09-24 09:48
閱讀 2272·2021-09-22 15:34
閱讀 1734·2019-08-30 15:44
閱讀 2675·2019-08-30 13:12
閱讀 604·2019-08-29 16:05