深入学习Java多线程——并发机制底层实现原理
2019獨角獸企業重金招聘Python工程師標準>>>
????Java代碼在編譯后會變成Java字節碼,字節碼被類加載器加載到JVM里,JVM執行字節碼,最終需要轉化為匯編指令在CPU上執行,Java中所使用的并發機制依賴于JVM的實現和CPU的指令。建議先對Java并發的內存模型進行了解。
? ? 對于并發編程的底層實現,必須要保證實現三大特性:
1.volatile
????在多線程并發編程中synchronized和volatile都扮演著重要的角色,volatile是輕量級的synchronized,它在多處理器開發中保證了共享變量的“可見性”。可見性的意思是當一個線程修改一個共享變量時,另外一個線程能讀到這個修改的值。如果volatile變量修飾符使用恰當的話,它比synchronized的使用和執行成本更低,因為它不會引起線程上下文的切換和調度。
推薦博客:
http://www.importnew.com/24082.html
?http://www.cnblogs.com/dolphin0520/p/3920373.html
1.1實現原理
????實現可見性的底層原理,可通過觀察Java代碼與匯編代碼查看。
Java代碼:
instance = new Singleton(); // instance是volatile變量匯編代碼:
0x01a3de1d: movb $0×0,0×1104800(%esi); 0x01a3de24: lock addl $0×0,(%esp);????有volatile變量修飾的共享變量進行寫操作的時候會多出第二行匯編代碼,Lock前綴的指令在多核處理器下會引發了兩件事情:
? ? (1)將當前處理器緩存行的數據寫回到系統內存。
????(2)這個寫回內存的操作會使在其他CPU里緩存了該內存地址的數據無效。
? ? 原本為了提高處理速度,處理器不直接和內存進行通信,而是先將系統內存的數據讀到內部緩存后再進行操作,但操作完不知道何時會寫到內存。
? ? 但是,如果對聲明了volatile的變量進行寫操作,JVM就會向處理器發送一條Lock前綴的指令,將這個變量所在緩存行的數據寫回到系統內存。同時還有一個問題,就算寫回到內存,如果其他處理器緩存的值還是舊的,再執行計算操作就會有問題。所以,在多處理器下,為了保證各個處理器的緩存是一致的,就會實現緩存一致性協議,每個處理器通過嗅探在總線上傳播的數據來檢查自己緩存的值是不是過期了,當處理器發現自己緩存行對應的內存地址被修改,就會將當前處理器的緩存行設置成無效狀態,當處理器對這個數據進行修改操作的時候,會重新從系統內存中把數據讀到處理器緩存里。
? ? 我個人理解就是:在多核處理器中,每個處理器處理計算一個線程的(任務)代碼,比如說一個四核處理器,有一個核正在處理一個包含對共享變量進行更改賦值的操作的線程,另外三個處理器處理一個包含讀取同一個共享變量操作的線程。
????如果該共享變量不是volatile,首先,CPU會從系統內存中獲取數據到CPU緩存中進行相應的處理(關于內存、高速緩存和CPU寄存器,可以參考計算機中內存、cache和寄存器之間的關系及區別),當處理對共享變量進行更改賦值的操作完成后,并不一定會立即將處理后的數據寫回系統內存,這就可能會導致當某個賦值操作完成(即更改操作的那行代碼執行)后,另一個讀取共享變量的線程會讀到錯誤數據,或者說未改變的數據。(如下列代碼測試中兩個線程的i值應該至少一個為2,但是兩個都為1就說明發生了這種情況)
????如果該共享變量是volatile的,那么CPU會從系統內存中獲取數據到CPU緩存中進行相應的處理,當處理對共享變量進行更改賦值的操作(即更改操作的那行代碼執行)完成后,會立即將處理后的數據寫回系統內存,并且其他三個處理器通過緩存一致性協議檢查自己緩存的數據是否過期,是則會重新從系統內存讀取。
? ? 簡單來說,volatile的兩條實現原則是:
? ? (1)Lock前綴的匯編指令會引起處理器緩存回寫到內存
????(2)一個處理器的緩存回寫到內存會導致其他處理器的緩存無效。
//volatile 關鍵字修飾的變量與無該關鍵字修飾的變量在多線程讀改寫時的區別 public class KeyWord_volatile{int i=0;volatile int x=0;class Runner implements Runnable{public void run() {i++;System.out.println(Thread.currentThread().getName()+"計算的i為:"+i);x++;System.out.println(Thread.currentThread().getName()+"計算的x為:"+x);}}Runnable getRun(){return new Runner();}public static void main(String[] args) {KeyWord_volatile v=new KeyWord_volatile();Runner r1=(Runner) v.getRun();Runner r2=(Runner) v.getRun();Thread t1=new Thread(r1);Thread t2=new Thread(r1);t1.start();t2.start();} } //測試結果(隨機,可能會發生) Thread-1計算的i為:1 Thread-0計算的i為:1 Thread-1計算的x為:1 Thread-0計算的x為:2?
2.synchronized
2.1 實現原理
(1)synchronized實現同步的基礎:Java中的每一個對象都可以作為鎖。具體表現為以下3種形式。
????當一個線程試圖訪問synchronized同步代碼塊時,它首先必須得到鎖,退出或拋出異常時必須釋放鎖。那么這個鎖是什么??存儲在那里?
(2)?Synchonized在JVM里的實現原理:JVM基于進入和退出Monitor對象來實現方法同步和代碼塊同步,但兩者的實現細節不一樣。代碼塊同步是使用monitorenter 和monitorexit指令實現的,而方法同步是使用另外一種方式實現的,細節在JVM規范里并沒有詳細說明。但是,方法的同步同樣可以使用這兩個指令來實現。 monitorenter指令是在編譯后插入到同步代碼塊的開始位置,而monitorexit是插入到方法結束處和異常處,JVM要保證每個monitorenter必須有對應的monitorexit與之配對。任何對象都有一個monitor與之關聯,當且一個monitor被持有后,它將處于鎖定狀態。線程執行到monitorenter指令時,將會嘗試獲取對象所對應的monitor的所有權,即嘗試獲得對象的鎖。synchronized用的鎖是存在Java對象頭里的。
(3)對象頭
???https://blog.csdn.net/yinbucheng/article/details/70037521
2.2 鎖的升級與對比
????為了減少獲得鎖和釋放鎖帶來的性能消耗,引入了“偏向鎖”和“輕量級鎖”。鎖一共有4種狀態,級別從低到高依次是:無鎖狀態、偏向鎖狀態、輕量級鎖狀態和重量級鎖狀態,這幾個狀態會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖后不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。
1.偏向鎖
????大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。當一個線程訪問同步塊并獲取鎖時,會在對象頭和棧幀中的鎖記錄里存儲鎖偏向的線程ID,以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需簡單地測試一下對象頭的Mark Word里是否存儲著指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。如果測試失敗,則需要再測試一下Mark Word中偏向鎖的標識是否設置成1(表示當前是偏向鎖):如果沒有設置,則 使用CAS競爭鎖;如果設置了,則嘗試使用CAS將對象頭的偏向鎖指向當前線程。
(1)偏向鎖的撤銷
????偏向鎖使用了一種等到競爭出現才釋放鎖的機制,所以當其他線程嘗試競爭偏向鎖時, 持有偏向鎖的線程才會釋放鎖。偏向鎖的撤銷,需要等待全局安全點(在這個時間點上沒有正在執行的字節碼)。它會首先暫停擁有偏向鎖的線程,然后檢查持有偏向鎖的線程是否活著, 如果線程不處于活動狀態,則將對象頭設置成無鎖狀態;如果線程仍然活著,擁有偏向鎖的棧會被執行,遍歷偏向對象的鎖記錄,棧中的鎖記錄和對象頭的Mark Word要么重新偏向于其他線程,要么恢復到無鎖或者標記對象不適合作為偏向鎖,最后喚醒暫停的線程。
(2)關閉偏向鎖:偏向鎖在Java 6和Java 7里是默認啟用的,但是它在應用程序啟動幾秒鐘之后才激活,如 有必要可以使用JVM參數來關閉延遲:-XX:BiasedLockingStartupDelay=0。如果你確定應用程 序里所有的鎖通常情況下處于競爭狀態,可以通過JVM參數關閉偏向鎖:-XX:- UseBiasedLocking=false,那么程序默認會進入輕量級鎖狀態。
2.輕量級鎖
????(1)輕量級鎖加鎖:線程在執行同步塊之前,JVM會先在當前線程的棧楨中創建用于存儲鎖記錄的空間,并將對象頭中的Mark Word復制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用 CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
????(2)輕量級鎖解鎖:輕量級解鎖時,會使用原子的CAS操作將Displaced Mark Word替換回到對象頭,如果成功,則表示沒有競爭發生。如果失敗,表示當前鎖存在競爭,鎖就會膨脹成重量級鎖。
????因為自旋會消耗CPU,為了避免無用的自旋(比如獲得鎖的線程被阻塞住了),一旦鎖升級成重量級鎖,就不會再恢復到輕量級鎖狀態。當鎖處于這個狀態下,其他線程試圖獲取鎖時, 都會被阻塞住,當持有鎖的線程釋放鎖之后會喚醒這些線程,被喚醒的線程就會進行新一輪的奪鎖之爭。
3.各級別鎖的優缺點對比
還可以參考學習這篇文章https://blog.csdn.net/zqz_zqz/article/details/70233767
3.原子操作的實現
3.1 處理器實現原子操作
1.相關CPU術語
2.實現方式
????處理器提供總線鎖定和緩存鎖定兩個機制來保證復雜內存操作的原子性。
? ? (1)使用總線鎖定:如果多個處理器同時對共享變量進行讀改寫操作 (i++就是經典的讀改寫操作),那么共享變量就會被多個處理器同時進行操作,這樣讀改寫操作就不是原子的,操作完之后共享變量的值會和期望的不一致。舉個例子,如果i=1,我們進行兩次i++操作,我們期望的結果是3,但是有可能結果是2。原因可能是多個處理器同時從各自的緩存中讀取變量i,分別進行加1操作,然后分別寫入系統內存中。那么,想要保證讀改寫共享變量的操作是原子的,就必須保證CPU1讀改寫共享變量的時候,CPU2不能操作緩存了該共享變量內存地址的緩存。
????處理器使用總線鎖就是來解決這個問題的。所謂總線鎖就是使用處理器提供的一個 LOCK#信號,當一個處理器在總線上輸出此信號時,其他處理器的請求將被阻塞住,那么該處理器可以獨占共享內存。
//volatile 關鍵字使用的時緩存鎖來實現 public class KeyWord_volatile{int i=0;volatile int x=0;class Runner implements Runnable{public void run() {i++;System.out.println(Thread.currentThread().getName()+"計算的i為:"+i);x++;System.out.println(Thread.currentThread().getName()+"計算的x為:"+x);}}Runnable getRun(){return new Runner();}public static void main(String[] args) {KeyWord_volatile v=new KeyWord_volatile();Runner r1=(Runner) v.getRun();Runner r2=(Runner) v.getRun();Thread t1=new Thread(r1);Thread t2=new Thread(r1);t1.start();t2.start();} } //測試結果(隨機,可能會發生) Thread-1計算的i為:1 Thread-0計算的i為:1 Thread-1計算的x為:1 Thread-0計算的x為:2? ? (2)使用緩存鎖保證原子性:在同一時刻,我們只需保證對某個內存地址的操作是原子性即可,但總線鎖定把CPU和內存之間的通信鎖住了,這使得鎖定期間,其他處理器不能操作其他內存地址的數據,所以總線鎖定的開銷比較大,目前處理器在某些場合下使用緩存鎖定代替總線鎖定來進行優化。
? ? 處理器可以使用“緩存鎖定”的方式來實現復雜的原子性。所謂“緩存鎖定”是指內存區域如果被緩存在處理器的緩存行中,并且在Lock操作期間被鎖定,那么當它執行鎖操作回寫到內存時,處理器不在總線上發出LOCK#信號,而是修改內部的內存地址,并允許它的緩存一致性機制來保證操作的原子性,因為緩存一致性機制會阻止同時修改由兩個以上處理器緩存的內存區域數據,當其他處理器回寫已被鎖定的緩存行的數據時,會使緩存行無效。
? ? 有兩種情況處理器不能使用緩存鎖定:
? ? (1)第一種情況是:當操作的數據不能被緩存在處理器內部,或操作的數據跨多個緩存行 時,則處理器會調用總線鎖定。
????(2)第二種情況是:有些處理器不支持緩存鎖定。對于Intel 486和Pentium處理器,就算鎖定的內存區域在處理器的緩存行中也會調用總線鎖定。
3.2 Java中實現原子操作
????在Java中可以通過鎖和循環CAS的方式來實現原子操作。
1.使用循環CAS實現原子操作
????自旋CAS實現的基本思路就是循環進行CAS操作直到成功為止,以下代碼實現了一個基于CAS線程安全的計數器方法safeCount和一個非線程安全的計數器count。
import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger;public class Counter {private AtomicInteger atomicI = new AtomicInteger(0);private int i = 0;public static void main(String[] args) {final Counter cas = new Counter();List<Thread> ts = new ArrayList<Thread>(600);long start = System.currentTimeMillis();for (int j = 0; j < 100; j++) {Thread t = new Thread(new Runnable() {@Overridepublic void run() {for (int i = 0; i < 10000; i++) {cas.count();cas.safeCount();}}});ts.add(t);}for (Thread t : ts) {t.start();}// 等待所有線程執行完成for (Thread t : ts) {try {t.join();} catch (InterruptedException e) {e.printStackTrace();}}System.out.println(cas.i);System.out.println(cas.atomicI.get());System.out.println(System.currentTimeMillis() - start);}/** * 使用CAS實現線程安全計數器 */private void safeCount() {for (;;) {int i = atomicI.get();boolean suc = atomicI.compareAndSet(i, ++i);if (suc) {break;}}}/*** 非線程安全計數器*/private void count() {i++;}}? ? 循環CAS的三大問題:ABA問題,循環時間長開銷大,以及只能保證一個共享變量的原子操作。
2 使用鎖機制來實現原子性操作
????鎖機制保證了只有獲得鎖的線程才能夠操作鎖定的內存區域。JVM內部實現了很多種鎖 機制,有偏向鎖、輕量級鎖和互斥鎖。有意思的是除了偏向鎖,JVM實現鎖的方式都用了循環 CAS,即當一個線程想進入同步塊的時候使用循環CAS的方式來獲取鎖,當它退出同步塊的時 候使用循環CAS釋放鎖。
轉載于:https://my.oschina.net/ProgramerLife/blog/1824063
總結
以上是生活随笔為你收集整理的深入学习Java多线程——并发机制底层实现原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Flutter-Cookbook 非官方
- 下一篇: linux下重新启动oracle