Java代碼在編譯後會變成Java字節碼,字節碼被類加載器加載到JVM裏,JVM執行字節碼,最終須要轉化爲彙編指令在CPU上執行,Java中所使用的併發機制依賴於JVM的實現和CPU的指令。建議先對Java併發的內存模型進行了解。html
對於併發編程的底層實現,必需要保證明現三大特性:java
- 可見性:即多個線程訪問同一個變量時,一個線程修改了這個變量的值,其餘線程可以當即看獲得修改的值。
- 原子性:一個操做或者多個操做要麼所有執行而且執行的過程不會被任何因素打斷,或者一旦中斷就都不執行。
- 有序性:程序執行的順序按照代碼的前後順序執行。
在多線程併發編程中synchronized和volatile都扮演着重要的角色,volatile是輕量級的synchronized,它在多處理器開發中保證了共享變量的「可見性」。可見性的意思是當一個線程修改一個共享變量時,另一個線程能讀到這個修改的值。若是volatile變量修飾符使用恰當的話,它比synchronized的使用和執行成本更低,由於它不會引發線程上下文的切換和調度。編程
推薦博客:緩存
http://www.importnew.com/24082.html安全
http://www.cnblogs.com/dolphin0520/p/3920373.html多線程
實現可見性的底層原理,可經過觀察Java代碼與彙編代碼查看。併發
Java代碼:ide
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
(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
爲了減小得到鎖和釋放鎖帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」。鎖一共有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.各級別鎖的優缺點對比
還能夠參考學習這篇文章http://www.javashuo.com/article/p-eawupvdg-ba.html
處理器提供總線鎖定和緩存鎖定兩個機制來保證複雜內存操做的原子性。
(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處理器,就算鎖定的內存區域在處理器的緩存行中也會調用總線鎖定。
在Java中能夠經過鎖和循環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() { @Override public 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問題,循環時間長開銷大,以及只能保證一個共享變量的原子操做。
鎖機制保證了只有得到鎖的線程纔可以操做鎖定的內存區域。JVM內部實現了不少種鎖 機制,有偏向鎖、輕量級鎖和互斥鎖。有意思的是除了偏向鎖,JVM實現鎖的方式都用了循環 CAS,即當一個線程想進入同步塊的時候使用循環CAS的方式來獲取鎖,當它退出同步塊的時 候使用循環CAS釋放鎖。