您好,登錄后才能下訂單哦!
這篇文章主要介紹“如何理解JAVA的多線程”,在日常操作中,相信很多人在如何理解JAVA的多線程問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”如何理解JAVA的多線程”的疑惑有所幫助!接下來,請跟著小編一起來學習吧!
要想明白數據一致性問題,要先縷下計算機存儲結構,從本地磁盤到主存到CPU緩存,也就是從硬盤到內存,到CPU。一般對應的程序的操作就是從數據庫查數據到內存然后到CPU進行計算。這個描述有點粗,下邊畫個圖。
業內畫這個圖一般都是畫的金字塔型狀,為了證明是我自己畫的我畫個長方型的(其實我不會畫金字塔)。
CPU多個核心和內存之間為了保證內部數據一致性還有一個緩存一致性協議(MESI),MESI其實就是指令狀態中的首字母。M(Modified)修改,E(Exclusive)獨享、互斥,S(Shared)共享,I(Invalid)無效。然后再看下邊這個圖。
太細的狀態流轉就不作描述了,扯這么多主要是為了說明白為什么會有數據一致性問題,就是因為有這么多級的緩存,CPU的運行并不是直接操作內存而是先把內存里邊的數據讀到緩存,而內存的讀和寫操作的時候就會造成不一致的問題。解決一致性問題怎么辦呢,兩個思路。
鎖住總線,操作時鎖住總線,這樣效率非常低,所以考慮第二個思路。
緩存一致性,每操作一次通知(一致性協議MESI),(但多線程的時候還是會有問題,后文講)
上邊稍微扯了一下存儲體系是為了在這里寫一下JAVA內存模型。
Java虛擬機規范中試圖定義一種Java內存模型(java Memory Model) 來屏蔽掉各種硬件和操作系統的內存訪問差異,以實現讓Java程序在各種平臺下都能達到一致的內存訪問效果。
內存模型是內存和線程之間的交互、規則。與編譯器有關,有并發有關,與處理器有關。
Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在虛擬機中將變量存儲到內存和從內存中取出變量這樣的底層細節。此處的變量與Java編程中所說的變量有所區別,它包括 了實例字段、靜態字段和構成數組對象的元素,但不包括局部變量與方法參數,因為后者是線程私有的,不會被共享,自然就不會存在競爭問題。為了獲得較好的執行效能,Java內存模型并沒有限制執行引擎使用處理器特定寄存器或緩存來和主內存進行交互,也沒有限制即時編譯器進行調整代碼執行順序這類優化措施。
Java內存模型規定了所有的變量都存儲在主內存中。每條線程還有自己的工作內存,線程的工作內存中保存了該線程使用到的變量的主內存副本拷貝,線程對變量的所有操作(讀取,賦值等 )都必需在工作內存中進行,而不能直接讀寫主內存中的變量。不同的線程之間也無法直接訪問對方工作內存中的變量,線程間變量值的傳遞均需要通過主內存來完成。
這里所說的主內存、工作內存和Java內存區域中的Java堆、棧、方法區等并不是同一個層次的內存劃分,這兩者基本上是沒有關系的。 如果兩者一定要勉強對應起來,那從變量、主內存、工作內存的定義來看,主內存對應Java堆中的對象實例數據部分 ,而工作內存則對應于虛擬機棧中的部分區域。從更底層次上說,主內存就是直接對應于物理硬件的內存,而為了獲取更好的運行速度,虛擬機可能會讓工作內存優先存儲于寄存器和高速緩存中,因為程序運行時主要訪問讀寫的是工作內存。
前邊說的都是和內存有關的內容,其實多線程有關系的還有指令重排序,指令重排序也會造成在多線程訪問下結束和想的不一樣的情況。大段的介紹就不寫了要不篇幅太長了(JVM那里書里邊有)。主要就是在CPU執行指令的時候會進行執行順序的優化。畫個圖看一下吧。
具體理論后文再寫先來點干貨,直接上代碼,一看就明白。
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
上邊這段代碼不太好調,然后我稍微改造了一下。
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先啟動 //下面這句話讓它等一等線程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,然后兩個線程同時啟動分別設置a=1,x=b和b=1,y=a。這個代碼里邊的starta和startb線程完全是為了讓threada 和threadb 兩個線程盡量同時啟動而加的,里邊只是分別調用了threada 和threadb 兩個線程。然后無限循環只要x和y 不同時等于0就初始化所有值繼續循環,直到x和y都是0的時候break。你猜猜會不會break。
結果看截圖
因為我沒有記錄循環次數,不知道循環了幾次,然后觸發了條件break了。從代碼上看,在輸出A之前必然會把B設置成1,在輸出B之前必然會把A設置為1。那為什么會出現同時是零的情況呢。這就很有可能是指令被重排序了。
指令重排序簡單了說是就兩行以上不相干的代碼在執行的時候有可能先執行的不是第一條。也就是執行順序會被優化。
如何判斷你寫的代碼執行順序會不會被優化,要看代碼之間有沒有Happens-before
關系。Happens-before
就是不無需任何干涉就可以保證有有序執行,由于篇幅限制Happens-before
就不在這里多做介紹。
下面簡單介紹一下java里邊的一個關鍵字volatile
。volatile
簡單來說就是來解決重排序問題的。對一個volatile
變量的寫,一定happen-before
后續對它的讀。也就是你在寫代碼的時候不希望你的代碼被重排序就使用volatile
關鍵字。volatile
還解決了內存可見性問題,在執行執行的時候一共有8條指令lock(鎖定)、read(讀取)、load(載入)、use(使用)、assign(賦值)、store(存儲)、write(寫入)、unlock(解鎖)(篇幅限制具體指令內容自行查詢,看下圖大概有個了解)。
volatile
主要是對其中4條指令做了處理。如下圖
也就是把 load和use關聯執行,把assign和store關聯執行。眾所周知有load必需有read現在load又和use關聯也就是要在緩存中要use的時候就必須要load要load就必需要read。通俗講就是要use(使用)一個變量的時候必需load(載入),要載入的時候必需從主內存read(讀取)這樣就解決了讀的可見性。下面看寫操作它是把assign和store做了關聯,也就是在assign(賦值)后必需store(存儲)。store(存儲)后write(寫入)。也就是做到了給一個變量賦值的時候一串關聯指令直接把變量值寫到主內存。就這樣通過用的時候直接從主內存取,在賦值到直接寫回主內存做到了內存可見性。
我在網上看到大部分寫多線程的時候都會寫到鎖,AQS和線程池。由于網文太多本文就不多做介紹。下面簡單寫一寫CAS。
CAS是一個比較魔性的操作,用的好可以讓你的代碼更優雅更高效。它就是無鎖編程的核心。
CAS書上是這么介紹的:“CAS即Compare and Swap,是JDK提供的非阻塞原子性操作,它通過硬件保證了比較-更新的原子性”。他是非阻塞的還是原子性,也就是說這玩意效率更高。還是通過硬件保證的說明這玩意更可靠。
從上圖可以看出,在cas指令修改變量值的時候,先要進行值的判斷,如果值和原來的值相等說明還沒有被其它線程改過,則執行修改,如果被改過了,則不修改。在java里邊java.util.concurrent.atomic
包下邊的類都使用了CAS操作。最常用的方法就是compareAndSet
。其底層是調用的Unsafe
類的compareAndSwap
方法。
到此,關于“如何理解JAVA的多線程”的學習就結束了,希望能夠解決大家的疑惑。理論與實踐的搭配能更好的幫助大家學習,快去試試吧!若想繼續學習更多相關知識,請繼續關注億速云網站,小編會繼續努力為大家帶來更多實用的文章!
免責聲明:本站發布的內容(圖片、視頻和文字)以原創、轉載和分享為主,文章觀點不代表本網站立場,如果涉及侵權請聯系站長郵箱:is@yisu.com進行舉報,并提供相關證據,一經查實,將立刻刪除涉嫌侵權內容。