這篇文章主要介紹“如何理解JAVA的多線程”,在日常操作中,相信很多人在如何理解JAVA的多線程問(wèn)題上存在疑惑,小編查閱了各式資料,整理出簡(jiǎn)單好用的操作方法,希望對(duì)大家解答”如何理解JAVA的多線程”的疑惑有所幫助!接下來(lái),請(qǐng)跟著小編一起來(lái)學(xué)習(xí)吧!
成都創(chuàng)新互聯(lián)公司服務(wù)項(xiàng)目包括三原網(wǎng)站建設(shè)、三原網(wǎng)站制作、三原網(wǎng)頁(yè)制作以及三原網(wǎng)絡(luò)營(yíng)銷策劃等。多年來(lái),我們專注于互聯(lián)網(wǎng)行業(yè),利用自身積累的技術(shù)優(yōu)勢(shì)、行業(yè)經(jīng)驗(yàn)、深度合作伙伴關(guān)系等,向廣大中小型企業(yè)、政府機(jī)構(gòu)等提供互聯(lián)網(wǎng)行業(yè)的解決方案,三原網(wǎng)站推廣取得了明顯的社會(huì)效益與經(jīng)濟(jì)效益。目前,我們服務(wù)的客戶以成都為中心已經(jīng)輻射到三原省份的部分城市,未來(lái)相信會(huì)繼續(xù)擴(kuò)大服務(wù)區(qū)域并繼續(xù)獲得客戶的支持與信任!
要想明白數(shù)據(jù)一致性問(wèn)題,要先縷下計(jì)算機(jī)存儲(chǔ)結(jié)構(gòu),從本地磁盤(pán)到主存到CPU緩存,也就是從硬盤(pán)到內(nèi)存,到CPU。一般對(duì)應(yīng)的程序的操作就是從數(shù)據(jù)庫(kù)查數(shù)據(jù)到內(nèi)存然后到CPU進(jìn)行計(jì)算。這個(gè)描述有點(diǎn)粗,下邊畫(huà)個(gè)圖。
業(yè)內(nèi)畫(huà)這個(gè)圖一般都是畫(huà)的金字塔型狀,為了證明是我自己畫(huà)的我畫(huà)個(gè)長(zhǎng)方型的(其實(shí)我不會(huì)畫(huà)金字塔)。
CPU多個(gè)核心和內(nèi)存之間為了保證內(nèi)部數(shù)據(jù)一致性還有一個(gè)緩存一致性協(xié)議(MESI),MESI其實(shí)就是指令狀態(tài)中的首字母。M(Modified)修改,E(Exclusive)獨(dú)享、互斥,S(Shared)共享,I(Invalid)無(wú)效。然后再看下邊這個(gè)圖。
太細(xì)的狀態(tài)流轉(zhuǎn)就不作描述了,扯這么多主要是為了說(shuō)明白為什么會(huì)有數(shù)據(jù)一致性問(wèn)題,就是因?yàn)橛羞@么多級(jí)的緩存,CPU的運(yùn)行并不是直接操作內(nèi)存而是先把內(nèi)存里邊的數(shù)據(jù)讀到緩存,而內(nèi)存的讀和寫(xiě)操作的時(shí)候就會(huì)造成不一致的問(wèn)題。解決一致性問(wèn)題怎么辦呢,兩個(gè)思路。
鎖住總線,操作時(shí)鎖住總線,這樣效率非常低,所以考慮第二個(gè)思路。
緩存一致性,每操作一次通知(一致性協(xié)議MESI),(但多線程的時(shí)候還是會(huì)有問(wèn)題,后文講)
上邊稍微扯了一下存儲(chǔ)體系是為了在這里寫(xiě)一下JAVA內(nèi)存模型。
Java虛擬機(jī)規(guī)范中試圖定義一種Java內(nèi)存模型(java Memory Model) 來(lái)屏蔽掉各種硬件和操作系統(tǒng)的內(nèi)存訪問(wèn)差異,以實(shí)現(xiàn)讓Java程序在各種平臺(tái)下都能達(dá)到一致的內(nèi)存訪問(wèn)效果。
內(nèi)存模型是內(nèi)存和線程之間的交互、規(guī)則。與編譯器有關(guān),有并發(fā)有關(guān),與處理器有關(guān)。
Java內(nèi)存模型的主要目標(biāo)是定義程序中各個(gè)變量的訪問(wèn)規(guī)則,即在虛擬機(jī)中將變量存儲(chǔ)到內(nèi)存和從內(nèi)存中取出變量這樣的底層細(xì)節(jié)。此處的變量與Java編程中所說(shuō)的變量有所區(qū)別,它包括 了實(shí)例字段、靜態(tài)字段和構(gòu)成數(shù)組對(duì)象的元素,但不包括局部變量與方法參數(shù),因?yàn)楹笳呤蔷€程私有的,不會(huì)被共享,自然就不會(huì)存在競(jìng)爭(zhēng)問(wèn)題。為了獲得較好的執(zhí)行效能,Java內(nèi)存模型并沒(méi)有限制執(zhí)行引擎使用處理器特定寄存器或緩存來(lái)和主內(nèi)存進(jìn)行交互,也沒(méi)有限制即時(shí)編譯器進(jìn)行調(diào)整代碼執(zhí)行順序這類優(yōu)化措施。
Java內(nèi)存模型規(guī)定了所有的變量都存儲(chǔ)在主內(nèi)存中。每條線程還有自己的工作內(nèi)存,線程的工作內(nèi)存中保存了該線程使用到的變量的主內(nèi)存副本拷貝,線程對(duì)變量的所有操作(讀取,賦值等 )都必需在工作內(nèi)存中進(jìn)行,而不能直接讀寫(xiě)主內(nèi)存中的變量。不同的線程之間也無(wú)法直接訪問(wèn)對(duì)方工作內(nèi)存中的變量,線程間變量值的傳遞均需要通過(guò)主內(nèi)存來(lái)完成。
這里所說(shuō)的主內(nèi)存、工作內(nèi)存和Java內(nèi)存區(qū)域中的Java堆、棧、方法區(qū)等并不是同一個(gè)層次的內(nèi)存劃分,這兩者基本上是沒(méi)有關(guān)系的。 如果兩者一定要勉強(qiáng)對(duì)應(yīng)起來(lái),那從變量、主內(nèi)存、工作內(nèi)存的定義來(lái)看,主內(nèi)存對(duì)應(yīng)Java堆中的對(duì)象實(shí)例數(shù)據(jù)部分 ,而工作內(nèi)存則對(duì)應(yīng)于虛擬機(jī)棧中的部分區(qū)域。從更底層次上說(shuō),主內(nèi)存就是直接對(duì)應(yīng)于物理硬件的內(nèi)存,而為了獲取更好的運(yùn)行速度,虛擬機(jī)可能會(huì)讓工作內(nèi)存優(yōu)先存儲(chǔ)于寄存器和高速緩存中,因?yàn)槌绦蜻\(yùn)行時(shí)主要訪問(wèn)讀寫(xiě)的是工作內(nèi)存。
前邊說(shuō)的都是和內(nèi)存有關(guān)的內(nèi)容,其實(shí)多線程有關(guān)系的還有指令重排序,指令重排序也會(huì)造成在多線程訪問(wèn)下結(jié)束和想的不一樣的情況。大段的介紹就不寫(xiě)了要不篇幅太長(zhǎng)了(JVM那里書(shū)里邊有)。主要就是在CPU執(zhí)行指令的時(shí)候會(huì)進(jìn)行執(zhí)行順序的優(yōu)化。畫(huà)個(gè)圖看一下吧。
具體理論后文再寫(xiě)先來(lái)點(diǎn)干貨,直接上代碼,一看就明白。
public class HappendBeforeTest { int a = 0; int b = 0; public static void main(String[] args) { HappendBeforeTest test = new HappendBeforeTest(); Thread threada = new Thread() { @Override public void run() { test.a = 1; System.out.println("b=" + test.b); } }; Thread threadb = new Thread() { @Override public void run() { test.b = 1; System.out.println("a=" + test.a); } }; threada.start(); threadb.start(); } }
猜猜有可能輸出什么?多選
A:a=0,b=1 B:a=1,b=0 C:a=0,b=0 D:a=1,b=1
上邊這段代碼不太好調(diào),然后我稍微改造了一下。
public class HappendBeforeTest { static int a = 0; static int b = 0; static int x = 0; static int y = 0; public static void shortWait(long interval) { long start = System.nanoTime(); long end; do { end = System.nanoTime(); } while (start + interval >= end); } public static void main(String[] args) throws InterruptedException { for (; ; ) { Thread threada = new Thread() { @Override public void run() { a = 1; x = b; } }; Thread threadb = new Thread() { @Override public void run() { b = 1; y = a; } }; Thread starta = new Thread() { @Override public void run() { // 由于線程threada先啟動(dòng) //下面這句話讓它等一等線程startb shortWait(100); threada.start(); } }; Thread startb = new Thread() { @Override public void run() { threadb.start(); } }; starta.start(); startb.start(); starta.join(); startb.join(); threada.join(); threadb.join(); a = 0; b = 0; System.out.print("x=" + x); System.out.print("y=" + y); if (x == 0 && y == 0) { break; } x = 0; y = 0; System.out.println(); } } }
這段代碼,a和b初始值為0,然后兩個(gè)線程同時(shí)啟動(dòng)分別設(shè)置a=1,x=b和b=1,y=a。這個(gè)代碼里邊的starta和startb線程完全是為了讓threada 和threadb 兩個(gè)線程盡量同時(shí)啟動(dòng)而加的,里邊只是分別調(diào)用了threada 和threadb 兩個(gè)線程。然后無(wú)限循環(huán)只要x和y 不同時(shí)等于0就初始化所有值繼續(xù)循環(huán),直到x和y都是0的時(shí)候break。你猜猜會(huì)不會(huì)break。
結(jié)果看截圖
因?yàn)槲覜](méi)有記錄循環(huán)次數(shù),不知道循環(huán)了幾次,然后觸發(fā)了條件break了。從代碼上看,在輸出A之前必然會(huì)把B設(shè)置成1,在輸出B之前必然會(huì)把A設(shè)置為1。那為什么會(huì)出現(xiàn)同時(shí)是零的情況呢。這就很有可能是指令被重排序了。
指令重排序簡(jiǎn)單了說(shuō)是就兩行以上不相干的代碼在執(zhí)行的時(shí)候有可能先執(zhí)行的不是第一條。也就是執(zhí)行順序會(huì)被優(yōu)化。
如何判斷你寫(xiě)的代碼執(zhí)行順序會(huì)不會(huì)被優(yōu)化,要看代碼之間有沒(méi)有Happens-before
關(guān)系。Happens-before
就是不無(wú)需任何干涉就可以保證有有序執(zhí)行,由于篇幅限制Happens-before
就不在這里多做介紹。
下面簡(jiǎn)單介紹一下java里邊的一個(gè)關(guān)鍵字volatile
。volatile
簡(jiǎn)單來(lái)說(shuō)就是來(lái)解決重排序問(wèn)題的。對(duì)一個(gè)volatile
變量的寫(xiě),一定happen-before
后續(xù)對(duì)它的讀。也就是你在寫(xiě)代碼的時(shí)候不希望你的代碼被重排序就使用volatile
關(guān)鍵字。volatile
還解決了內(nèi)存可見(jiàn)性問(wèn)題,在執(zhí)行執(zhí)行的時(shí)候一共有8條指令lock(鎖定)、read(讀取)、load(載入)、use(使用)、assign(賦值)、store(存儲(chǔ))、write(寫(xiě)入)、unlock(解鎖)(篇幅限制具體指令內(nèi)容自行查詢,看下圖大概有個(gè)了解)。
volatile
主要是對(duì)其中4條指令做了處理。如下圖
也就是把 load和use關(guān)聯(lián)執(zhí)行,把a(bǔ)ssign和store關(guān)聯(lián)執(zhí)行。眾所周知有l(wèi)oad必需有read現(xiàn)在load又和use關(guān)聯(lián)也就是要在緩存中要use的時(shí)候就必須要load要load就必需要read。通俗講就是要use(使用)一個(gè)變量的時(shí)候必需load(載入),要載入的時(shí)候必需從主內(nèi)存read(讀?。┻@樣就解決了讀的可見(jiàn)性。下面看寫(xiě)操作它是把a(bǔ)ssign和store做了關(guān)聯(lián),也就是在assign(賦值)后必需store(存儲(chǔ))。store(存儲(chǔ))后write(寫(xiě)入)。也就是做到了給一個(gè)變量賦值的時(shí)候一串關(guān)聯(lián)指令直接把變量值寫(xiě)到主內(nèi)存。就這樣通過(guò)用的時(shí)候直接從主內(nèi)存取,在賦值到直接寫(xiě)回主內(nèi)存做到了內(nèi)存可見(jiàn)性。
我在網(wǎng)上看到大部分寫(xiě)多線程的時(shí)候都會(huì)寫(xiě)到鎖,AQS和線程池。由于網(wǎng)文太多本文就不多做介紹。下面簡(jiǎn)單寫(xiě)一寫(xiě)CAS。
CAS是一個(gè)比較魔性的操作,用的好可以讓你的代碼更優(yōu)雅更高效。它就是無(wú)鎖編程的核心。
CAS書(shū)上是這么介紹的:“CAS即Compare and Swap,是JDK提供的非阻塞原子性操作,它通過(guò)硬件保證了比較-更新的原子性”。他是非阻塞的還是原子性,也就是說(shuō)這玩意效率更高。還是通過(guò)硬件保證的說(shuō)明這玩意更可靠。
從上圖可以看出,在cas指令修改變量值的時(shí)候,先要進(jìn)行值的判斷,如果值和原來(lái)的值相等說(shuō)明還沒(méi)有被其它線程改過(guò),則執(zhí)行修改,如果被改過(guò)了,則不修改。在java里邊java.util.concurrent.atomic
包下邊的類都使用了CAS操作。最常用的方法就是compareAndSet
。其底層是調(diào)用的Unsafe
類的compareAndSwap
方法。
到此,關(guān)于“如何理解JAVA的多線程”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識(shí),請(qǐng)繼續(xù)關(guān)注創(chuàng)新互聯(lián)網(wǎng)站,小編會(huì)繼續(xù)努力為大家?guī)?lái)更多實(shí)用的文章!
分享題目:如何理解JAVA的多線程
轉(zhuǎn)載源于:http://m.rwnh.cn/article0/ipjcio.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站設(shè)計(jì)、靜態(tài)網(wǎng)站、關(guān)鍵詞優(yōu)化、App開(kāi)發(fā)、響應(yīng)式網(wǎng)站、建站公司
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來(lái)源: 創(chuàng)新互聯(lián)