在併發編程中,多個線程之間採起什麼機制進行通訊(信息交換),什麼機制進行數據的同步?程序員
在Java語言中,採用的是共享內存模型來實現多線程之間的信息交換和數據同步的。面試
線程之間經過共享程序公共的狀態,經過讀-寫內存中公共狀態的方式來進行隱式的通訊。同步指的是程序在控制多個線程之間執行程序的相對順序的機制,在共享內存模型中,同步是顯式的,程序員必須顯式指定某個方法/代碼塊須要在多線程之間互斥執行。算法
在說Java內存模型以前,咱們先說一下Java的內存結構,也就是運行時的數據區域:編程
Java虛擬機在執行Java程序的過程當中,會把它管理的內存劃分爲幾個不一樣的數據區域,這些區域都有各自的用途、建立時間、銷燬時間。數組
Java運行時數據區分爲下面幾個內存區域:緩存
1.PC寄存器/程序計數器:安全
嚴格來講是一個數據結構,用於保存當前正在執行的程序的內存地址,因爲Java是支持多線程執行的,因此程序執行的軌跡不可能一直都是線性執行。當有多個線程交叉執行時,被中斷的線程的程序當前執行到哪條內存地址必然要保存下來,以便用於被中斷的線程恢復執行時再按照被中斷時的指令地址繼續執行下去。爲了線程切換後能恢復到正確的執行位置,每一個線程都須要有一個獨立的程序計數器,各個線程之間計數器互不影響,獨立存儲,咱們稱這類內存區域爲「線程私有」的內存,這在某種程度上有點相似於「ThreadLocal」,是線程安全的。數據結構
2.Java棧 Java Stack:多線程
Java棧老是與線程關聯在一塊兒的,每當建立一個線程,JVM就會爲該線程建立對應的Java棧,在這個Java棧中又會包含多個棧幀(Stack Frame),這些棧幀是與每一個方法關聯起來的,每運行一個方法就建立一個棧幀,每一個棧幀會含有一些局部變量、操做棧和方法返回值等信息。每當一個方法執行完成時,該棧幀就會彈出棧幀的元素做爲這個方法的返回值,而且清除這個棧幀,Java棧的棧頂的棧幀就是當前正在執行的活動棧,也就是當前正在執行的方法,PC寄存器也會指向該地址。只有這個活動的棧幀的本地變量能夠被操做棧使用,當在這個棧幀中調用另一個方法時,與之對應的一個新的棧幀被建立,這個新建立的棧幀被放到Java棧的棧頂,變爲當前的活動棧。一樣如今只有這個棧的本地變量才能被使用,當這個棧幀中全部指令都完成時,這個棧幀被移除Java棧,剛纔的那個棧幀變爲活動棧幀,前面棧幀的返回值變爲這個棧幀的操做棧的一個操做數。架構
因爲Java棧是與線程對應起來的,Java棧數據不是線程共有的,因此不須要關心其數據一致性,也不會存在同步鎖的問題。
在Java虛擬機規範中,對這個區域規定了兩種異常情況:若是線程請求的棧深度大於虛擬機所容許的深度,將拋出StackOverflowError異常;若是虛擬機能夠動態擴展,若是擴展時沒法申請到足夠的內存,就會拋出OutOfMemoryError異常。在Hot Spot虛擬機中,可使用-Xss參數來設置棧的大小。棧的大小直接決定了函數調用的可達深度。
3.堆 Heap:
堆是JVM所管理的內存中國最大的一塊,是被全部Java線程鎖共享的,不是線程安全的,在JVM啓動時建立。堆是存儲Java對象的地方,這一點Java虛擬機規範中描述是:全部的對象實例以及數組都要在堆上分配。Java堆是GC管理的主要區域,從內存回收的角度來看,因爲如今GC基本都採用分代收集算法,因此Java堆還能夠細分爲:新生代和老年代;新生代再細緻一點有Eden空間、From Survivor空間、To Survivor空間等。
4.方法區Method Area:
方法區存放了要加載的類的信息(名稱、修飾符等)、類中的靜態常量、類中定義爲final類型的常量、類中的Field信息、類中的方法信息,當在程序中經過Class對象的getName.isInterface等方法來獲取信息時,這些數據都來源於方法區。方法區是被Java線程鎖共享的,不像Java堆中其餘部分同樣會頻繁被GC回收,它存儲的信息相對比較穩定,在必定條件下會被GC,當方法區要使用的內存超過其容許的大小時,會拋出OutOfMemory的錯誤信息。方法區也是堆中的一部分,就是咱們一般所說的Java堆中的永久區 Permanet Generation,大小能夠經過參數來設置,能夠經過-XX:PermSize指定初始值,-XX:MaxPermSize指定最大值。
5.常量池Constant Pool:
常量池自己是方法區中的一個數據結構。常量池中存儲瞭如字符串、final變量值、類名和方法名常量。常量池在編譯期間就被肯定,並保存在已編譯的.class文件中。通常分爲兩類:字面量和應用量。字面量就是字符串、final變量等。類名和方法名屬於引用量。引用量最多見的是在調用方法的時候,根據方法名找到方法的引用,並以此定爲到函數體進行函數代碼的執行。引用量包含:類和接口的權限定名、字段的名稱和描述符,方法的名稱和描述符。
6.本地方法棧Native Method Stack:
本地方法棧和Java棧所發揮的做用很是類似,區別不過是Java棧爲JVM執行Java方法服務,而本地方法棧爲JVM執行Native方法服務。本地方法棧也會拋出StackOverflowError和OutOfMemoryError異常。
主內存和工做內存:
Java內存模型的主要目標是定義程序中各個變量的訪問規則,即在JVM中將變量存儲到內存和從內存中取出變量這樣的底層細節。此處的變量與Java編程裏面的變量有所不一樣步,它包含了實例字段、靜態字段和構成數組對象的元素,但不包含局部變量和方法參數,由於後者是線程私有的,不會共享,固然不存在數據競爭問題(若是局部變量是一個reference引用類型,它引用的對象在Java堆中可被各個線程共享,可是reference引用自己在Java棧的局部變量表中,是線程私有的)。爲了得到較高的執行效能,Java內存模型並無限制執行引發使用處理器的特定寄存器或者緩存來和主內存進行交互,也沒有限制即時編譯器進行調整代碼執行順序這類優化措施。
JMM規定了全部的變量都存儲在主內存(Main Memory)中。每一個線程還有本身的工做內存(Working Memory),線程的工做內存中保存了該線程使用到的變量的主內存的副本拷貝,線程對變量的全部操做(讀取、賦值等)都必須在工做內存中進行,而不能直接讀寫主內存中的變量(volatile變量仍然有工做內存的拷貝,可是因爲它特殊的操做順序性規定,因此看起來如同直接在主內存中讀寫訪問通常)。不一樣的線程之間也沒法直接訪問對方工做內存中的變量,線程之間值的傳遞都須要經過主內存來完成。
線程1和線程2要想進行數據的交換通常要經歷下面的步驟:
1.線程1把工做內存1中的更新過的共享變量刷新到主內存中去。
2.線程2到主內存中去讀取線程1刷新過的共享變量,而後copy一份到工做內存2中去。
Java內存模型是圍繞着併發編程中原子性、可見性、有序性這三個特徵來創建的,那咱們依次看一下這三個特徵:
原子性(Atomicity):一個操做不能被打斷,要麼所有執行完畢,要麼不執行。在這點上有點相似於事務操做,要麼所有執行成功,要麼回退到執行該操做以前的狀態。
基本類型數據的訪問大都是原子操做,long 和double類型的變量是64位,可是在32位JVM中,32位的JVM會將64位數據的讀寫操做分爲2次32位的讀寫操做來進行,這就致使了long、double類型的變量在32位虛擬機中是非原子操做,數據有可能會被破壞,也就意味着多個線程在併發訪問的時候是線程非安全的。
下面咱們來演示這個32位JVM下,對64位long類型的數據的訪問的問題:
public class NotAtomicity { //靜態變量t public static long t = 0; //靜態變量t的get方法 public static long getT() { return t; } //靜態變量t的set方法 public static void setT(long t) { NotAtomicity.t = t; } //改變變量t的線程 public static class ChangeT implements Runnable{ private long to; public ChangeT(long to) { this.to = to; } public void run() { //不斷的將long變量設值到 t中 while (true) { NotAtomicity.setT(to); //將當前線程的執行時間片斷讓出去,以便由線程調度機制從新決定哪一個線程能夠執行 Thread.yield(); } } } //讀取變量t的線程,若讀取的值和設置的值不一致,說明變量t的數據被破壞了,即線程不安全 public static class ReadT implements Runnable{ public void run() { //不斷的讀取NotAtomicity的t的值 while (true) { long tmp = NotAtomicity.getT(); //比較是不是本身設值的其中一個 if (tmp != 100L && tmp != 200L && tmp != -300L && tmp != -400L) { //程序若執行到這裏,說明long類型變量t,其數據已經被破壞了 System.out.println(tmp); } ////將當前線程的執行時間片斷讓出去,以便由線程調度機制從新決定哪一個線程能夠執行 Thread.yield(); } } } public static void main(String[] args) { new Thread(new ChangeT(100L)).start(); new Thread(new ChangeT(200L)).start(); new Thread(new ChangeT(-300L)).start(); new Thread(new ChangeT(-400L)).start(); new Thread(new ReadT()).start(); } }
咱們建立了4個線程來對long類型的變量t進行賦值,賦值分別爲100,200,-300,-400,有一個線程負責讀取變量t,若是正常的話,讀取到的t的值應該是咱們賦值中的一個,可是在32的JVM中,事情會出乎預料。若是程序正常的話,咱們控制檯不會有任何的輸出,可實際上,程序一運行,控制檯就輸出了下面的信息:
-4294967096
4294966896
-4294967096
-4294967096
4294966896
之因此會出現上面的狀況,是由於在32位JVM中,64位的long數據的讀和寫都不是原子操做,即不具備原子性,併發的時候相互干擾了。
32位的JVM中,要想保證對long、double類型數據的操做的原子性,能夠對訪問該數據的方法進行同步,就像下面的:
public class Atomicity { //靜態變量t public static long t = 0; //靜態變量t的get方法,同步方法 public synchronized static long getT() { return t; } //靜態變量t的set方法,同步方法 public synchronized static void setT(long t) { Atomicity.t = t; } //改變變量t的線程 public static class ChangeT implements Runnable{ private long to; public ChangeT(long to) { this.to = to; } public void run() { //不斷的將long變量設值到 t中 while (true) { Atomicity.setT(to); //將當前線程的執行時間片斷讓出去,以便由線程調度機制從新決定哪一個線程能夠執行 Thread.yield(); } } } //讀取變量t的線程,若讀取的值和設置的值不一致,說明變量t的數據被破壞了,即線程不安全 public static class ReadT implements Runnable{ public void run() { //不斷的讀取NotAtomicity的t的值 while (true) { long tmp = Atomicity.getT(); //比較是不是本身設值的其中一個 if (tmp != 100L && tmp != 200L && tmp != -300L && tmp != -400L) { //程序若執行到這裏,說明long類型變量t,其數據已經被破壞了 System.out.println(tmp); } ////將當前線程的執行時間片斷讓出去,以便由線程調度機制從新決定哪一個線程能夠執行 Thread.yield(); } } } public static void main(String[] args) { new Thread(new ChangeT(100L)).start(); new Thread(new ChangeT(200L)).start(); new Thread(new ChangeT(-300L)).start(); new Thread(new ChangeT(-400L)).start(); new Thread(new ReadT()).start(); } }
這樣作的話,能夠保證對64位數據操做的原子性。
可見性:一個線程對共享變量作了修改以後,其餘的線程當即可以看到(感知到)該變量這種修改(變化)。
Java內存模型是經過將在工做內存中的變量修改後的值同步到主內存,在讀取變量前從主內存刷新最新值到工做內存中,這種依賴主內存的方式來實現可見性的。
不管是普通變量仍是volatile變量都是如此,區別在於:volatile的特殊規則保證了volatile變量值修改後的新值馬上同步到主內存,每次使用volatile變量前當即從主內存中刷新,所以volatile保證了多線程之間的操做變量的可見性,而普通變量則不能保證這一點。
除了volatile關鍵字能實現可見性以外,還有synchronized,Lock,final也是能夠的。
使用synchronized關鍵字,在同步方法/同步塊開始時(Monitor Enter),使用共享變量時會從主內存中刷新變量值到工做內存中(即從主內存中讀取最新值到線程私有的工做內存中),在同步方法/同步塊結束時(Monitor Exit),會將工做內存中的變量值同步到主內存中去(即將線程私有的工做內存中的值寫入到主內存進行同步)。
使用Lock接口的最經常使用的實現ReentrantLock(重入鎖)來實現可見性:當咱們在方法的開始位置執行lock.lock()方法,這和synchronized開始位置(Monitor Enter)有相同的語義,即便用共享變量時會從主內存中刷新變量值到工做內存中(即從主內存中讀取最新值到線程私有的工做內存中),在方法的最後finally塊裏執行lock.unlock()方法,和synchronized結束位置(Monitor Exit)有相同的語義,即會將工做內存中的變量值同步到主內存中去(即將線程私有的工做內存中的值寫入到主內存進行同步)。
final關鍵字的可見性是指:被final修飾的變量,在構造函數數一旦初始化完成,而且在構造函數中並無把「this」的引用傳遞出去(「this」引用逃逸是很危險的,其餘的線程極可能經過該引用訪問到只「初始化一半」的對象),那麼其餘線程就能夠看到final變量的值。
有序性:對於一個線程的代碼而言,咱們老是覺得代碼的執行是從前日後的,依次執行的。這麼說不能說徹底不對,在單線程程序裏,確實會這樣執行;可是在多線程併發時,程序的執行就有可能出現亂序。用一句話能夠總結爲:在本線程內觀察,操做都是有序的;若是在一個線程中觀察另一個線程,全部的操做都是無序的。前半句是指「線程內表現爲串行語義(WithIn Thread As-if-Serial Semantics)」,後半句是指「指令重排」現象和「工做內存和主內存同步延遲」現象。
Java提供了兩個關鍵字volatile和synchronized來保證多線程之間操做的有序性,volatile關鍵字自己經過加入內存屏障來禁止指令的重排序,而synchronized關鍵字經過一個變量在同一時間只容許有一個線程對其進行加鎖的規則來實現,
在單線程程序中,不會發生「指令重排」和「工做內存和主內存同步延遲」現象,只在多線程程序中出現。
happens-before原則:
Java內存模型中定義的兩項操做之間的次序關係,若是說操做A先行發生於操做B,操做A產生的影響能被操做B觀察到,「影響」包含了修改了內存中共享變量的值、發送了消息、調用了方法等。
下面是Java內存模型下一些」自然的「happens-before關係,這些happens-before關係無須任何同步器協助就已經存在,能夠在編碼中直接使用。若是兩個操做之間的關係不在此列,而且沒法從下列規則推導出來的話,它們就沒有順序性保障,虛擬機能夠對它們進行隨意地重排序。
a.程序次序規則(Pragram Order Rule):在一個線程內,按照程序代碼順序,書寫在前面的操做先行發生於書寫在後面的操做。準確地說應該是控制流順序而不是程序代碼順序,由於要考慮分支、循環結構。
b.管程鎖定規則(Monitor Lock Rule):一個unlock操做先行發生於後面對同一個鎖的lock操做。這裏必須強調的是同一個鎖,而」後面「是指時間上的前後順序。
c.volatile變量規則(Volatile Variable Rule):對一個volatile變量的寫操做先行發生於後面對這個變量的讀取操做,這裏的」後面「一樣指時間上的前後順序。
d.線程啓動規則(Thread Start Rule):Thread對象的start()方法先行發生於此線程的每個動做。
e.線程終於規則(Thread Termination Rule):線程中的全部操做都先行發生於對此線程的終止檢測,咱們能夠經過Thread.join()方法結束,Thread.isAlive()的返回值等做段檢測到線程已經終止執行。
f.線程中斷規則(Thread Interruption Rule):對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生,能夠經過Thread.interrupted()方法檢測是否有中斷髮生。
g.對象終結規則(Finalizer Rule):一個對象初始化完成(構造方法執行完成)先行發生於它的finalize()方法的開始。
g.傳遞性(Transitivity):若是操做A先行發生於操做B,操做B先行發生於操做C,那就能夠得出操做A先行發生於操做C的結論。
一個操做」時間上的先發生「不表明這個操做會是」先行發生「,那若是一個操做」先行發生「是否就能推導出這個操做一定是」時間上的先發生 「呢?也是不成立的,一個典型的例子就是指令重排序。因此時間上的前後順序與happens-before原則之間基本沒有什麼關係,因此衡量併發安全問題一切必須以happens-before 原則爲準。
感謝看完文章
也沒有什麼東西送你們的了,就分享一點學習資源和麪試資源吧
+q q-q u n:94 83 68 76 9,免費領取圖上的下圖ziyuan
高清架構技術視頻教程及BAT 面試攻略