最近看到有很多粉絲私信我說,能不能給整理出一份面試的要點出來,說本身複習的時候思緒很亂,總是找不到重點。那麼今天就先給你們分享一個面試幾乎必問的點,併發!在面試中問的頻率很高的一個是分佈式,一個就是併發,具體乾貨都在下方了。java
我:synchronized能夠保證方法或者代碼在運行時,同一時刻只有一個方法能夠進入到臨界區,同時還能夠保證共享變量的內存可見性。面試
我:咱們能夠把它理解爲一個同步工具,也能夠描述爲一種同步機制,它一般被描述爲一個對象。與一切皆對象同樣,全部的java對象是天生的Monitor, 每個java對象都有成爲Monitor的潛質,由於在Java的設計中,每個java對象自打孃胎出來就帶了一把看不見的鎖,它被叫作內部鎖或者Monitor鎖。數據庫
public static void main(String [] args) { Vector<String> vector = new Vector<>(); for (int i=0; i<10; i++) { vector.add(i+""); } System.out.println(vector); }
public static void test() { List<String> list = new ArrayList<>(); for (int i=0; i<10; i++) { synchronized (Demo.class) { list.add(i + ""); } } System.out.println(list); }
我:輕量級鎖提高程序同步性能的依據是:對於絕大部分的鎖,在整個同步週期內是不存在競爭的(區別於偏向鎖),這是一個經驗數據。若是沒有競爭,輕量級鎖使用CAS操做避免了使用互斥 量的開銷,但若是存在競爭,除了互斥量的開銷,還額外發生了CAS操做,所以在有競爭的狀況下,輕量級鎖比傳統的重量級鎖更慢。編程
二、拷貝對象頭中的Mark Word複製到鎖記錄(Lock Record)中。
三、拷貝成功後,虛擬機將使用CAS操做嘗試將鎖對象的Mark Word更新爲指向Lock Record的指針,並將線程棧幀中的Lock Record裏的owner指針指向Object的Mark Word。若是這個更新 動做成功了,那麼這個線程就擁有了該對象的鎖,而且對象Mark Word的鎖標誌位設置爲「00」,表示此對象處於輕量級鎖定狀態。緩存
四、若是這個更新操做失敗了,虛擬機首先會檢查對象的Mark Word是否指向當前線程的棧幀,若是是就說明當前線程已經擁有了這個對象的鎖,那就能夠直接進入同步塊繼續執行。不然說明 多個線程競爭鎖,輕量級鎖就要膨脹爲重量級鎖,鎖標誌位的狀態值變爲「10」,Mark Word中存儲的就是指向重量級鎖(互斥量)的指針,後面等待鎖的線程也要進入阻塞狀態。安全
我:偏向鎖的目的是消除數據在無競爭狀況下的同步原語,進一步提升程序的運行性能。偏向鎖會偏向於第一個得到它的線程,若是在接下來的執行過程當中,該鎖沒有被其餘線程獲取,那持有 偏向鎖的線程將永遠不須要同步。數據結構
我:偏向鎖、輕量級鎖都是樂觀鎖,重量級鎖是悲觀鎖。一個對象剛開始實例化的時候,沒有任何線程來訪問它時,它是可偏向的,意味着它認爲只可能有一個線程來訪問它,因此當第一個線程 訪問它的時候,它會偏向這個線程,此時,對象持有偏向鎖。偏向第一個線程,這個線程在修改對象頭成爲偏向鎖的時候使用CAS操做,並將對象頭中的ThreadID改爲本身的Id,以後再訪問這個對象只須要對比ID。一旦有第二個線程訪問這個對象,由於偏向鎖不會釋放,因此第二個線程看到對象是偏向狀態,代表在這個對象上存在競爭了,檢查原來持有該對象的線程是否依然存活,若是掛了,則能夠將對象變爲無鎖狀態,而後從新偏向新的線程。若是原來的線程依然存活,則立刻執行那個線程的操做棧,檢查該對象的使用狀況,若是仍然須要持有偏向鎖,則偏向鎖升級爲輕量級鎖(偏向鎖就是此時升級爲輕量級鎖)。若是不存在使用了,則能夠將對象恢復成無鎖狀態,而後從新偏向。多線程
我:在JSR113標準中有有一段對JMM的簡單介紹:Java虛擬機支持多線程執行。在Java中Thread類表明線程,建立一個線程的惟一方法就是建立一個Thread類的實例對象,當調用了對象的start方法後,相應的線程將會執行。線程的行爲有時會與咱們的直覺相左,特別是在線程沒有正確同步的狀況下。本規範描述了JMM平臺上多線程程序的語義,具體包含一個線程對共享變量的寫入什麼時候能被其餘線程看到。這是官方的接單介紹。併發
我:Java內存模型是內存模型在JVM中的體現。這個模型的主要目標是定義程序中各個共享變量的訪問規則,也就是在虛擬機中將變量存儲到內存以及從內存中取出變量這類的底層細節。經過這些規則來規範對內存的讀寫操做,保證了併發場景下的可見性、原子性和有序性。 JMM規定了多有的變量都存儲在主內存中,每條線程都有本身的工做內存,線程的工做內存保存了該線程中用到的主內存副本拷貝,線程對變量的全部操做都必須在工做內存中進行,而不是直接讀寫主內存。不一樣線程之間也沒法直接訪問對方工做內存中的變量,線程間變量的傳遞均須要本身的工做內存和主存之間 進行數據同步。而JMM就做用於工做內存和主存之間數據同步過程。他規定了如何作數據同步以及何時作數據同步。也就是說Java線程之間的通訊由Java內存模型控制,JMM決定一個線程對共享變量的寫入什麼時候對另外一個線程可見。app
我:不是的,它須要知足如下兩個條件:
一、在單線程環境下不能改變程序運行的結果。
二、存在數據依賴關係的不容許重排序。
其實這兩點能夠歸結爲一點:沒法經過happens-before原則推導出來的,JMM容許任意的排序。
int a=1; //A int b=2; //B int c=a+b; //C
A,B,C三個操做存在以下關係:A和B不存在數據依賴,A和C,B和C存在數據依賴,所以在重排序的時候:A和B能夠隨意排序,可是必須位於C的前面,但不管何種順序,最終結果C都是3.
public class RecordExample2 { int a = 0; boolean flag = false; /** * A線程執行 */ public void writer(){ a = 1; // 1 flag = true; // 2 } /** * B線程執行 */ public void read(){ if(flag){ // 3 int i = a + a; // 4 } }}
假如操做1和操做2之間重排序,可能會變成下面這種執行順序:
一、線程A執行flag=true;
二、線程B執行if(flag);
三、線程B執行int i = a+a;
四、線程A執行a=1。
按照這種執行順序線程B確定讀不到線程A設置的a值,在這裏多線程的語義就已經被重排序破壞了。操做3和操做4之間也能夠重排序,這裏就不闡述了。可是他們之間存在一個控制依賴的關係,由於只有操做3成立操做4纔會執行。當代碼中存在控制依賴性時,會影響指令序列的執行的並行度,因此編譯器和處理器會採用猜想執行來克服控制依賴對並行度的影響。假如操做3和操做4重排序了,操做4先執行,則先會把計算結果臨時保存到重排序緩衝中,當操做3爲真時纔會將計算結果寫入變量i中。
一、可見性:可見性是指線程之間的可見性,一個線程修改的狀態對另外一個線程是可見的。也就是一個線程的修改的結果,另外一個線程可以立刻看到。好比:用volatile修飾的變量,就會具備可見性,volatile修飾的變量不容許線程內部緩存和重排序,即直接修改內存,因此對其餘線程是可見的。但這裏要注意一個問題,volatile只能讓被他修飾的內容具備可見性,不能保證它具備原子性。好比 volatile int a=0; ++a;這個變量a具備可見性,可是a++是一個非原子操做,也就是這個操做一樣存在線程安全問題。在Java中,volatile/synchronized/final實現了可見性。
二、原子性:即一個操做或者多個操做要麼所有執行而且執行的過程不會被任何因素打斷,要麼都不執行。原子就像數據庫裏的事務同樣,他們是一個團隊,同生共死。看下面一個簡單的栗子:
i=0; //1 j=i; //2 i++; //3 i=j+1; //4
上面的四個操做,只有1是原子操做,其餘都不是原子操做。好比2包含了兩個操做:讀取i,將i值賦給j。在Java中synchronized/lock操做中保證原子性。
三、有序性:程序執行的順序按照代碼的前後順序執行。 前面JMM中提到了重排序,在java內存模型中,爲了效率是容許編譯器和處理器對指令進行重排序,並且重排序不會影響單線程的運行結果,可是對多線程有影響。Java中提供了volatile和synchronized保證有序性。
那麼volatile的內存語義是如何實現的呢?對於通常的變量會被重排序,而對於volatile則不能,這樣會影響其內存語義,因此爲了實現volatile的內存語義JMM會限制重排序。
volatile的重排序規則:
一、若是第一個操做爲volatile讀,則無論第二個操做是啥,都不能重排序。這個操做確保volatile讀以後的操做不會被編譯器重排序到volatile讀以前。
二、當第二個操做爲volatile寫,則無論第一個操做是啥,都不能重排序。這個操做確保了volatile寫以前的操做不會被編譯器重排序到volatile寫以後。
三、當第一個操做爲volatile寫,第二個操做爲volatile讀,不能重排序。
volatile的底層實現是經過插入內存屏障,可是對於編譯器來講,發現一個最優佈置來最小化插入內存屏障的總數幾乎是不可能的,因此JMM採用了保守策略。 以下:
一、在每個volatile寫操做前插入一個StoreStore屏障。
二、在每個volatile寫操做後插入一個StoreLoad屏障。
三、在每個volatile讀操做後插入一個LoadLoad屏障。
四、在每個volatile讀操做後插入一個LoadStore屏障。
總結:StoreStore屏障->寫操做->StoreLoad屏障->讀操做->LoadLoad屏障->LoadStore屏障。 下面經過一個例子簡單分析下: volatile原理分析
if (this.value == A) { this.value = B return true; } else { return false; }
private static final Unsafe unsafe = Unsafe.getUnsafe(); private static final long valueOffset; static { try { valueOffset = unsafe.objectFieldOffset (AtomicInteger.class.getDeclaredField("value")); } catch (Exception ex) { throw new Error(ex); } } private volatile int value;
如上是AtomicInteger的源碼: 一、Unsafe是CAS的核心類,Java沒法直接訪問底層操做系統,而是經過本地native方法訪問。不過儘管如此,JVM仍是開了個後門:Unsafe,它提供了 硬件級別的原子操做。
二、valueOffset:爲變量值在內存中的偏移地址,Unsafe就是經過偏移地址來獲得數據的原值的。
三、value:當前值,使用volatile修飾,保證多線程環境下看見的是同一個。
// AtomicInteger.java public final int addAndGet(int delta) { return unsafe.getAndAddInt(this, valueOffset, delta) + delta; } // Unsafe.java public final int getAndAddInt(Object var1, long var2, int var4) { int var5; do { var5 = this.getIntVolatile(var1, var2); } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4)); return var5; }
在方法compareAndSwapInt(var1, var2, var5, var5 + var4)中,有四個參數,分別表明:對象,對象的地址,預期值,修改值。
其實在面試裏,多線程,併發這塊問的仍是很是頻繁的,你們看完以後有什麼不懂的歡迎在評論區討論,也能夠私信問我,通常我看到以後都會回的!