本篇文章為大家展示了如何解讀Java多線程與并發(fā)模型中的共享對象,內(nèi)容簡明扼要并且容易理解,絕對能使你眼前一亮,通過這篇文章的詳細(xì)介紹希望你能有所收獲。
成都創(chuàng)新互聯(lián)于2013年創(chuàng)立,是專業(yè)互聯(lián)網(wǎng)技術(shù)服務(wù)公司,擁有項(xiàng)目成都做網(wǎng)站、成都網(wǎng)站設(shè)計(jì)網(wǎng)站策劃,項(xiàng)目實(shí)施與項(xiàng)目整合能力。我們以讓每一個(gè)夢想脫穎而出為使命,1280元科爾沁左翼做網(wǎng)站,已為上家服務(wù),為科爾沁左翼各地企業(yè)和個(gè)人服務(wù),聯(lián)系電話:18982081108以下內(nèi)容如無特殊說明均指代Java環(huán)境。
共享對象
使用Java編寫線程安全的程序關(guān)鍵在于正確的使用共享對象,以及安全的對其進(jìn)行訪問管理。在第一章我們談到Java的內(nèi)置鎖可以保障線程安全,對于其他的應(yīng)用來說并發(fā)的安全性是在內(nèi)置鎖這個(gè)“黑盒子”內(nèi)保障了線程變量使用的邊界。談到線程的邊界問題,隨之而來的是Java內(nèi)存模型另外的一個(gè)重要的含義,可見性。Java對可見性提供的原生支持是volatile關(guān)鍵字。
volatile關(guān)鍵字
volatile 變量具備兩種特性,其一是保證該變量對所有線程可見,這里的可見性指的是當(dāng)一個(gè)線程修改了變量的值,那么新的值對于其他線程是可以立即獲取的。其二 volatile 禁止了指令重排。
雖然 volatile 變量具有可見性和禁止指令重排序,但是并不能說 volatile 變量能確保并發(fā)安全。
public class VolatileTest {
public static volatile int a = 0;
public static final int THREAD_COUNT = 20;
public static void increase() {a++;
}
public static void main(String[] args)
throws InterruptedException
{
Thread[] threads = new Thread[THREAD_COUNT];
for (int i = 0;
i < THREAD_COUNT; i++)
{
threads[i] = new Thread(new Runnable()
{
public void run() {for (int i = 0;
i < 1000;
i++) {increase();
}
}
}
);
threads[i].start();
}
while (Thread.activeCount() > 2)
{
Thread.yield();
}
System.out.println(a);
}
}
按照我們的預(yù)期,它應(yīng)該返回 20000 ,但是很可惜,該程序的返回結(jié)果幾乎每次都不一樣。
問題主要出在 a++ 上,復(fù)合操作并不具備原子性, 雖然這里利用 volatile 定義了 a ,但是在做 a++ 時(shí), 先獲取到最新的 a 值,比如這時(shí)候最新的可能是 50,然后再讓 a 增加,但是在增加的過程中,其他線程很可能已經(jīng)將 a 的值改變了,或許已經(jīng)成為 52、53 ,但是該線程作自增時(shí),還是使用的舊值,所以會(huì)出現(xiàn)結(jié)果往往小于預(yù)期的 2000。如果要解決這個(gè)問題,可以對 increase() 方法加鎖。
volatile 適用場景
volatile 適用于程序運(yùn)算結(jié)果不依賴于變量的當(dāng)前值,也相當(dāng)于說,上述程序的 a 不要自增,或者說僅僅是賦值運(yùn)算,例如 boolean flag = true 這樣的操作。
volatile boolean shutDown =false;
public voidshutDown()
{
shutDown =true;
}
public voiddoWork()
{while(!shutDown)
{
System.out.println("Do work "+ Thread.currentThread().getId());
}
}
代碼2.1:變量的可見性問題
在代碼2.1中,可以看到按照正常的邏輯應(yīng)該打印10之后線程停止,但是實(shí)際的情況可能是打印出0或者程序永遠(yuǎn)不會(huì)被終止掉。其原因是沒有使用恰當(dāng)?shù)耐綑C(jī)制以保障線程的寫入操作對所有線程都是可見的。
我們一般將volatile理解為synchronized的輕量級實(shí)現(xiàn),在多核處理器中可以保障共享變量的“可見性”,但是不能保障原子性。關(guān)于原子性問題在該章節(jié)的程序變量規(guī)則會(huì)加以說明,下面我們先看下Java的內(nèi)存模型實(shí)現(xiàn)以了解JVM和計(jì)算機(jī)硬件是如何協(xié)調(diào)共享變量的以及volatile變量的可見性。
Java內(nèi)存模型
我們都知道現(xiàn)代計(jì)算機(jī)都是馮諾依曼結(jié)構(gòu)的,所有的代碼都是順序執(zhí)行的。如果計(jì)算機(jī)需要在CPU中運(yùn)算某個(gè)指令,勢必就會(huì)涉及對數(shù)據(jù)的讀取和寫入操作。由于程序數(shù)據(jù)的大部分內(nèi)容都是存儲(chǔ)在主內(nèi)存(RAM)中的,在這當(dāng)中就存在著一個(gè)讀取速度的問題,CPU很快而主內(nèi)存相對來說(相對CPU)就會(huì)慢上很多,為了解決這個(gè)速度階梯問題,各個(gè)CPU廠商都在CPU里面引入了高速緩存來優(yōu)化主內(nèi)存和CPU的數(shù)據(jù)交互。針對上面的技術(shù)我特意整理了一下,有很多技術(shù)不是靠幾句話能講清楚,所以干脆找朋友錄制了一些視頻,很多問題其實(shí)答案很簡單,但是背后的思考和邏輯不簡單,要做到知其然還要知其所以然。如果想學(xué)習(xí)Java工程化、高性能及分布式、深入淺出。微服務(wù)、Spring,MyBatis,Netty源碼分析的朋友可以加我的Java進(jìn)階群:591240817,群里有大牛直播講解技術(shù),以及Java大型互聯(lián)網(wǎng)技術(shù)的視頻免費(fèi)分享
此時(shí)當(dāng)CPU需要從主內(nèi)存獲取數(shù)據(jù)時(shí),會(huì)拷貝一份到高速緩存中,CPU計(jì)算時(shí)就可以直接在高速緩存中進(jìn)行數(shù)據(jù)的讀取和寫入,提高吞吐量。當(dāng)數(shù)據(jù)運(yùn)行完成后,再將高速緩存的內(nèi)容刷新到主內(nèi)存中,此時(shí)其他CPU看到的才是執(zhí)行之后的結(jié)果,但在這之間存在著時(shí)間差。
看這個(gè)例子:
int counter = 0; counter = counter + 1;復(fù)制代碼
代碼2.2:自增不一致問題
代碼2.2在運(yùn)行時(shí),CPU會(huì)從主內(nèi)存中讀取counter的值,復(fù)制一份到當(dāng)前CPU核心的高速緩存中,在CPU執(zhí)行完成加1的指令之后,將結(jié)果1寫入高速緩存中,最后將高速緩存刷新到主內(nèi)存中。這個(gè)例子代碼在單線程的程序中將正確的運(yùn)行下去。
但我們試想這樣一種情況,現(xiàn)在有兩個(gè)線程共同運(yùn)行該段代碼,初始化時(shí)兩個(gè)線程分別從主內(nèi)存中讀取了counter的值0到各自的高速緩存中,線程1在CPU1中運(yùn)算完成后寫入高速緩存Cache1,線程2在CPU2中運(yùn)算完成后寫入高速緩存Cache2,此時(shí)counter的值在兩個(gè)CPU的高速緩存中的值都是1。
此時(shí)CPU1將值刷新到主內(nèi)存中,counter的值為1,之后CPU2將counter的值也刷新到主內(nèi)存,counter的值覆蓋為1,最終的結(jié)果計(jì)算counter為1(正確的兩次計(jì)算結(jié)果相加應(yīng)為2)。這就是緩存不一致性問題。這會(huì)在多線程訪問共享變量時(shí)出現(xiàn)。
解決緩存不一致問題的方案:
通過總線鎖LOCK#方式。
通過緩存一致性協(xié)議。
圖2.1 :緩存不一致問題
圖2.1中提到的兩種內(nèi)存一致性協(xié)議都是從計(jì)算機(jī)硬件層面上提供的保障。CPU一般是通過在總線上增加LOCK#鎖的方式,鎖住對內(nèi)存的訪問來達(dá)到目的,也就是阻塞其他CPU對內(nèi)存的訪問,從而使只有一個(gè)CPU能訪問該主內(nèi)存。因此需要用總線進(jìn)行內(nèi)存鎖定,可以分析得到此種做法對CPU的吞吐率造成的損害很嚴(yán)重,效率低下。
隨著技術(shù)升級帶來了緩存一致性協(xié)議,市場占有率較大的Intel的CPU使用的是MESI協(xié)議,該協(xié)議可以保障各個(gè)高速緩存使用的共享變量的副本是一致的。其實(shí)現(xiàn)的核心思想是:當(dāng)在多核心CPU中訪問的變量是共享變量時(shí),某個(gè)線程在CPU中修改共享變量數(shù)據(jù)時(shí),會(huì)通知其他也存儲(chǔ)了該變量副本的CPU將緩存置為無效狀態(tài),因此其他CPU讀取該高速緩存中的變量時(shí),發(fā)現(xiàn)該共享變量副本為無效狀態(tài),會(huì)從主內(nèi)存中重新加載。但當(dāng)緩存一致性協(xié)議無法發(fā)揮作用時(shí),CPU還是會(huì)降級使用總線鎖的方式進(jìn)行鎖定處理。
一個(gè)小插曲:為什么volatile無法保障的原子性
我們看下圖2.2,CPU在主內(nèi)存中讀取一個(gè)變量之后,拷貝副本到高速緩存,CPU在執(zhí)行期間雖然識(shí)別了變量的“易變性”,但是只能保障最后一步store操作的原子性,在load,use期間并未實(shí)現(xiàn)其原子性操作。
圖2.2:數(shù)據(jù)加載和內(nèi)存屏障
JVM為了使我們的代碼得到最優(yōu)的執(zhí)行體驗(yàn),在進(jìn)行自我優(yōu)化時(shí),并不保障代碼的先后執(zhí)行順序(滿足Happen-Before規(guī)則的除外),這就是“指令重排”,而上面提到的store操作保障了原子性,JVM是如何實(shí)現(xiàn)的呢?其原因是這里存在一個(gè)“內(nèi)存屏障”的指令(以后我們會(huì)談到整個(gè)內(nèi)容),這個(gè)是CPU支持的一個(gè)指令,該指令只能保障store時(shí)的原子性,但是不能保障整個(gè)操作的原子性。
從整個(gè)小插曲中,我們看到了volatile雖然有可見性的語義,但是并不能真正的保證線程安全。如果要保證并發(fā)線程的安全訪問,需要符合并發(fā)程序變量的訪問規(guī)則。
并發(fā)程序變量的訪問規(guī)則
1. 原子性
程序的原子性和數(shù)據(jù)庫事務(wù)的原子性有著同樣的意義,可以保障一次操作要么全部執(zhí)行成功,要不全部都不執(zhí)行。
2. 可見性
可見性是微妙的,因?yàn)樽罱K的結(jié)果總是和我們的直覺大相徑庭,當(dāng)多個(gè)線程共同修改一個(gè)共享變量的值時(shí),由于存在高速緩存中的變量副本操作,不能及時(shí)將數(shù)據(jù)刷新到主內(nèi)存,導(dǎo)致當(dāng)前線程在CP中的操作結(jié)果對其他CPU是不可見狀態(tài)。
3. 有序性
有序性通俗的理解就是程序在JVM中是按照順序執(zhí)行的,但是前面已經(jīng)提到了JVM為了優(yōu)化代碼的執(zhí)行速度,會(huì)進(jìn)行“指令重排”。在單線程中“指令重排”并不會(huì)帶來安全問題,但在并發(fā)程序中,由于程序的順序不能保障,運(yùn)行過程中可能會(huì)出現(xiàn)不安全的線程訪問問題。
綜上,要想在并發(fā)編程環(huán)境中安全的運(yùn)行程序,就必須滿足原子性、可見性和有序性。只要以上任何一點(diǎn)沒有保障,那程序運(yùn)行就可能出現(xiàn)不可預(yù)知的錯(cuò)誤。最后我們介紹一下Java并發(fā)的“殺手锏”,Happens-Before法則,符合該法則的情況下可以保障并發(fā)環(huán)境下變量的訪問規(guī)則。
happens-before語義
Java內(nèi)存模型使用了各種操作來定義的,包括對變量的讀寫,監(jiān)視器的獲取釋放等,JMM中使用了
happens-before
語義闡述了操作之間的內(nèi)存可見性。如果想要保證執(zhí)行操作B的線程看到操作A的結(jié)構(gòu)(無論AB是否在同一線程),那么A,B必須滿足
happens-before
關(guān)系。如果兩個(gè)操作之間缺乏
happens-before
Happens-Before法則:
程序次序法則:線程中的每個(gè)動(dòng)作A都Happens-Before于該線程中的每一個(gè)動(dòng)作B,在程序中,所有的動(dòng)作B都出現(xiàn)在動(dòng)作A之后。
Lock法則:對于一個(gè)Lock的解鎖操作總是Happens-Before于每一個(gè)后續(xù)對該Lock的加鎖操作。
volatile變量法則:對于volatile變量的寫入操作Happens-Before于后續(xù)對同一個(gè)變量的讀操作。
線程啟動(dòng)法則:在一個(gè)線程里,對Thread.start()函數(shù)的調(diào)用會(huì)Happens-Before于每一個(gè)啟動(dòng)線程中的動(dòng)作。
線程終結(jié)法則:線程中的任何動(dòng)作都Happens-Before于其他線程檢測到這個(gè)線程已經(jīng)終結(jié)或者從Thread.join()函數(shù)調(diào)用中成功返回或者Thread.isAlive()函數(shù)返回false。
中斷法則:一個(gè)線程調(diào)用另一個(gè)線程的interrupt總是Happens-Before于被中斷的線程發(fā)現(xiàn)中斷。
終結(jié)法則:一個(gè)對象的構(gòu)造函數(shù)的結(jié)束總是Happens-Before于這個(gè)對象的finalizer(Java沒有直接的類似C的析構(gòu)函數(shù))的開始。
傳遞性法則:如果A事件Happens-Before于B事件,并且B事件Happens-Before于C事件,那么A事件Happens-Before于C事件。
當(dāng)一個(gè)變量在多線程競爭中被讀取和存儲(chǔ),如果并未按照Happens-Before的法則,那么他就會(huì)存在數(shù)據(jù)競爭關(guān)系。
總結(jié)
給大家關(guān)于Java的共享變量的內(nèi)容就介紹到這里,現(xiàn)在你已經(jīng)明白Java的volatile關(guān)鍵字的含義了,了解了為什么volatile不能保障原子性的原因了,了解了Happens-Before規(guī)則能讓我們的Java程序運(yùn)行的更加安全。
在這里給大家提供一個(gè)學(xué)習(xí)交流的平臺(tái),java架構(gòu)師群1017599436
具有1-5工作經(jīng)驗(yàn)的,面對目前流行的技術(shù)不知從何下手,需要突破技術(shù)瓶頸的可以加群。
在公司待久了,過得很安逸,但跳槽時(shí)面試碰壁。需要在短時(shí)間內(nèi)進(jìn)修、跳槽拿高薪的可以加群。
如果沒有工作經(jīng)驗(yàn),但基礎(chǔ)非常扎實(shí),對java工作機(jī)制,常用設(shè)計(jì)思想,常用java開發(fā)框架掌握熟練的可以加群。
通過這節(jié)內(nèi)容希望可以幫助你更深入的了解Java的并發(fā)概念中的內(nèi)置鎖和共享變量。Java的并發(fā)內(nèi)容還有很多,例如在某些場景下比synchronized效率要更高的Lock,阻塞隊(duì)列,同步器等。
上述內(nèi)容就是如何解讀Java多線程與并發(fā)模型中的共享對象,你們學(xué)到知識(shí)或技能了嗎?如果還想學(xué)到更多技能或者豐富自己的知識(shí)儲(chǔ)備,歡迎關(guān)注創(chuàng)新互聯(lián)-成都網(wǎng)站建設(shè)公司行業(yè)資訊頻道。