1. 程式人生 > >手把手教你如何玩轉面試題(Java基礎)

手把手教你如何玩轉面試題(Java基礎)

        下面的這些題目,主要是根據自己的親身經歷以及在學習的過程中碰到比較典型的內容,所以把這些進行整理,方便於更多的人進行學習和交流。  內容有點多,可能你會很反感,但是,我相信,如果你能認真的看完我這些,當你回頭再回想整個Java內容的時候,你就會清晰很多。因為,這是自己的學習經歷,我相信有很多的人跟自己都一樣,所以,給點信心,別怕多,這麼多學習乾貨,為什麼要回避呢?

下面是其他方面的知識點,歡迎大家進行瀏覽

1:Object類中含有哪些方法,分別的作用是什麼?

答:一共是有12個方法,可以分為如下幾類:

(1)構造方法:Object()

(2)判斷物件相等:hashCode()和equals(object)

(3)執行緒相關:wait(),wait(long),wait(long,int),notify(),notifyAll()

(4)複製物件:clone()

(5)垃圾回收:finalize()

(6)物件本身相關內容:toString() 和getClass()

2:物件重寫equals方法需要注意什麼?

(1)自反性:對於任何非空引用值 x,x.equals(x) 都應返回 true (2)對稱性:對於任何非空引用值 x 和 y,當且僅當 y.equals(x) 返回 true 時,x.equals(y) 才應返回 true (3)傳遞性:對於任何非空引用值 x、y 和 z,如果 x.equals(y) 返回 true,並且 y.equals(z) 返回 true,那麼 x.equals(z) 應返回 true (4)一致性:對於任何非空引用值 x 和 y,多次呼叫 x.equals(y) 始終返回 true 或始終返回 false,前提是物件上 equals 比較中所用的資訊沒有被修改。 對於任何非空引用值 x,x.equals(null) 都應返回 false 對於這個問題,我們用得比較多的就是String類了,它這裡面就是對equals方法進行了重寫;

溫馨提示:一般如果重寫了equals()方法,那麼也最好將hashcode()方法進行重寫;

3:HashMap中的容量為什麼是以2的冪次大小?(預設是16)

答:這裡其實主要是為了進行hash的時候能夠更加的均勻,因為在這裡面不是直接進行取模,而是利用了取容量大小的N位來進行“&”操作的,就比如,初始預設是16=2的4次方,所以進行hash的時候進行與“1111”進行“&”操作而得到的hash,這樣的好處就使得hash更加均勻。

4:HashMap在1.8之後添加了紅黑樹的結構,而在1.7是以陣列和連結串列的結構,這樣的好處在於?

答:主要是為了解決連結串列hash衝突過多,這樣的時間複雜度就是O(n),所以出現的使用紅黑樹來進行解決。

5:HashMap中進行擴容為什麼是擴充套件為原來大小的2倍?

答:其實這個與它本身的容量大小值和它的hash演算法有關係;

(1)首先是容量大小:預設的時候是16,即2的4次方,即是2的冪次方的關係,那麼進行擴容操作是滿足2的倍數進行則更加好計算大小;

(2)Hash演算法:因為在HashMap中,它進行hash判斷索引的時候,是通過的與當前容量的2的冪次方的N位來進行“&”操作,所以這就要求擴充套件的容量必須是2的倍數,否則進行的hash取值就不能夠進行最大化的均勻;就比如:初始值是16=2的4次方,所以後面就是與“1111”進行相“&”,而現在擴容之後是32=2的5次方,則進行的是與“11111”進行相“&”,所以如果不是以2的倍數進行擴容,那麼就違背了它本身的hash運算的規律;

6:請說一下快排的原理和實現

答:時間複雜度:O(nlogN),是一種非穩定性的排序演算法;

實現程式碼:

/**
	 * 快速排序
	 * @param number 排序陣列
	 * @param geshu  陣列個數-1為陣列的最後一個索引位置
	 * @return
	 */
	private static int[] quicksortWay(int[] number, int geshu) {
		quickSort(number , 0 , geshu -1 );
		return number;
	}
	/**
	 * 進行快速排序的方法
	 * @param number  排序陣列
	 * @param low     排序陣列的起始位置索引
	 * @param hight   排序陣列的終止位置索引
	 */
	private static void quickSort(int[] number, int low, int hight) {
		int begin = 0;
		int end = 0 ;
		if(low > hight){
			return ;
		}
		begin = low ;
		end = hight ;
		int index = number[low];  //獲取到需要排序的第一個位置的內容為基準值
		while(begin < end){
			while(begin < end && number[end] >= index){  //找到比基準小的數
				end-- ;
			}
			if(begin < end){
				number[begin++] = number[end]; //將小於基準的位置的內容換到第一個位置,然後後面的從第二個位置繼續開始排序
			}
			while(begin < end && number[begin] < index){
				begin++;      //一直找到不小於基準數的位置,這樣的話,前面的都是小於基準的值
			}
			if(begin < end){
				number[end--] = number[begin] ;
			}
		}
		number[begin] = index;
		quickSort(number, low, begin-1);
		quickSort(number, begin+1, hight);
		
	}

7:請說一下堆排序的原理和實現

答:時間複雜度O(nlogN),是一種非穩定性的排序演算法;

實現程式碼:

//堆排序
	private static int[] duiNumberWay(int[] number, int geshu) {
		int suoyin=geshu-1;   //陣列的最大下標
        for(int i=0;i<geshu;i++){                    //(優化)其實排序的次數為i<geshu-1就可以了,因為最後一趟其實都不用排了
        	creatMaxHead(number,suoyin-i);  				//得到每次的最大堆的排序
        	getOrderArray(number,0,suoyin-i);               //得到每次最大的數都和之前無序的陣列的最後一個無序陣列的位置的索引進行替換
        }
		return number;
	}

	/*
	 * 將無序的陣列逐次變成有序的,每次找到一個最大的,則將最大的放到無序陣列的最後一個(這樣從後面的就是一個有序的,從大到小的順序)
	 * 引數:start表示的是,因為每次找到的堆中都是陣列0的值最大
	 *      end表示的是最後一個無序陣列的索引
	 *      (這個的方法作用和swapMaxVaule的其實是一樣都是交換最大的值,只是這樣寫區分一下,那是對每個小樹的值的交換)
	 */
    private static void getOrderArray(int[] number, int start, int end) {
		int temp=number[end];       
		number[end]=number[start];
		number[start]=temp;		
	}

	/*
     * 得到每次堆排序的最大數的值,並且都放在索引為0的位置(這是堆排序的精髓的地方)
     */
	private static void creatMaxHead(int[] number, int lastIndex) {
		int currentIndex=0;   //儲存當前的索引下標
		int bigMaxIndex=0;
		for(int i=(lastIndex-1)/2;i>=0;i--){
			currentIndex=i;        //當前根的下標
			if((currentIndex*2+1)<=lastIndex){  //判斷當前結點是否有子節點
				bigMaxIndex=currentIndex*2+1;    //左結點的下標
				if(bigMaxIndex<lastIndex){     //表示有右結點
					if(number[bigMaxIndex]<number[bigMaxIndex+1]){  //左結點小於右結點的值
						bigMaxIndex=bigMaxIndex+1;
					}
				}
				if(number[currentIndex]<number[bigMaxIndex]){      //用左右結點的大值和根的值進行比較
						swapMaxVaule(currentIndex,bigMaxIndex,number);       //根結點的值小於左右結點中大的點
						currentIndex=bigMaxIndex;
				}
			}
		}
		
	}
    /*
     * 堆排序中,得到每一個小樹的最大的值
     */
	private static void swapMaxVaule(int currentIndex, int bigMaxIndex,int[] number) {
		int temp=number[currentIndex];       
		number[currentIndex]=number[bigMaxIndex];
		number[bigMaxIndex]=temp;			
	}

8:Java中的執行緒的型別?

答:使用者執行緒和守護執行緒(Daemon);----------注意一點:執行緒是JVM級別的,而靜態變數是屬於ClassLoader級別,所以在Web應用停止的時候,靜態變數會被移除,但是執行緒並不是,所以執行緒的生命週期和Web程式的生命週期並不是一致的;所以這個也是需要守護執行緒的一個原因;

關於使用者執行緒就是平常寫的比較多的繼承Thread和實現runnable介面的方式,對於守護執行緒可以看看這篇博文守護執行緒到底是個什麼東西?

9:Java中的佇列有哪些?哪些是執行緒安全的?

答:佇列主要是實現了Queue介面,有ArrayBlocakingQueue,LinkedBlockingQueue,ConcurrentLinkedQueue,PriorityBlockingQueue,(這 四個是執行緒安全的)PriorityQueue,SynchronousQueue。

注意一點:另外還有個介面就是Deque,這是一個雙向佇列。

10:Java中的內部類有哪些?各自的特點是什麼?

(1)靜態內部類: 特點:1:靜態內部類是不需要依賴於外部類的,這點和類的靜態成員屬性有點類似,並且它不能使用外部類的非static成員變數或者方法,這點很好理解,因為在沒有外部類的物件的情況下,可以建立靜態內部類的物件,如果允許訪問外部類的非static成員就會產生矛盾,因為外部類的非static成員必須依附於具體的物件 2:建立靜態內部類物件的一般形式為:  外部類類名.內部類類名 xxx = new 外部類類名.內部類類名() (2)成員內部類:成員內部類是最普通的內部類,它的定義為位於另一個類的內部, 特點:1:成員內部類可以無條件訪問外部類的所有成員屬性和成員方法(包括private成員和靜態成員)。這個原因在於:成員內部類是依賴於外部類的,之所以,能夠訪問外部類的內容是因為,在編譯的時候,會預設的給成員內部類新增一個有參的建構函式(即使,自己定義了一個無參的構造器),而這個引數也正是外部類的物件的引用,所以,就能夠引用外部類的成員變數和方法了。 2:內部類可以擁有private訪問許可權、protected訪問許可權、public訪問許可權及包訪問許可權 3:成員內部類是依附外部類而存在的,也就是說,如果要建立成員內部類的物件,前提是必須存在一個外部類的物件 4:當成員內部類擁有和外部類同名的成員變數或者方法時,會發生隱藏現象,即預設情況下訪問的是成員內部類的成員。如果要訪問外部類的同名成員,需要以下面的形式進行訪問: 外部類.this.成員變數 外部類.this.成員方法 5:建立成員內部類物件的一般形式為:  外部類類名.內部類類名 xxx = 外部類物件名.new 內部類類名() (3)區域性內部類:區域性內部類是定義在一個方法或者一個作用域裡面的類 特點:區域性內部類就像是方法裡面的一個區域性變數一樣,是不能有public、protected、private以及static修飾符的 (4)匿名內部類: 特點: 1:匿名內部類也是不能有訪問修飾符和static修飾符的 2:匿名內部類是唯一一種沒有構造器的類 3:匿名內部類用於繼承其他類或是實現介面,並不需要增加額外的方法,只是對繼承方法的實現或是重寫常見的關於內部類的題目~!

一:為什麼成員內部類可以無條件訪問外部類的成員? 答:這個答案在我介紹成員內部類的特點中已經進行了講解;

二:為什麼區域性內部類和匿名內部類只能訪問區域性final變數?

答:比如一個例子:

 如果區域性變數的值在編譯期間就可以確定,則直接在匿名內部裡面建立一個拷貝。如果區域性變數的值無法在編譯期間確定,則通過構造器傳參的方式來對拷貝進行初始化賦值。   反編譯上面的程式碼之後,可以看到,在run方法中訪問的變數a根本就不是test方法中的區域性變數a。這樣一來就解決了前面所說的 生命週期不一致的問題。但是新的問題又來了,既然在run方法中訪問的變數a和test方法中的變數a不是同一個變數,當在run方法中改變變數a的值的話,會出現什麼情況?   對,會造成資料不一致性,這樣就達不到原本的意圖和要求。為了解決這個問題,java編譯器就限定必須將變數a限制為final變數,不允許對變數a進行更改(對於引用型別的變數,是不允許指向新的物件),這樣資料不一致性的問題就得以解決了。所以就必須使用final進行修飾;

三:靜態內部類有特殊的地方嗎?

答:靜態內部類是不依賴於外部類的,也就說可以在不建立外部類物件的情況下建立內部類的物件。另外,靜態內部類是不持有指向外部類物件的引用的,這個讀者可以自己嘗試反編譯class檔案看一下就知道了,是沒有Outter this&0引用的。

11:Java中導致JVM持久區發生溢位的原因有?導致JVM年老代發生溢位的原因有?

JVM中的堆分為持久代和年老代, 導致持久代發生溢位的原因:動態載入大量的java類 導致年老代發生溢位的原因:1:迴圈上萬次的字串處理2:建立上千萬個物件 3:在一段程式碼內申請上百M甚至上G的記憶體

12:Java中的多型性是什麼?

答:Java中的多型性有三個形式: (1)方法的過載:(2)通過繼承實現的方法的重寫(3)通過實現介面的方法 Java中多型的條件: (1)要有繼承(2)要有重寫(3)父類引用紙箱子類---也就是向上轉型 Java中多型的分類: (1)靜態多型:其中編譯 時多型是靜態的,主要是指方法的過載,它是根據引數列表的不同來區分不同的函式,通過編譯之後會變成兩個不同的函式,在執行時談不上多型。 (2)動態多型:它是通過動態繫結來實現的,也就是我們平常所說的多型性

13:Java中複製陣列的方法和效率是如何?

答:方法和效率如下順序:

System.arraycopy>clone>Arrays.copyOf>for迴圈遍歷

14:Java中面向物件的設計原則有哪些?

答:七個基本原則: 

(1)單一職責原則(Single-Resposibility Principle):一個類,最好只做一件事,只有一個引起它的變化。單一職責原則可以看做是低耦合、高內聚在面向物件原則上的引申,將職責定義為引起變化的原因,以提高內聚性來減少引起變化的原因。  (2)開放封閉原則(Open-Closed principle):軟體實體應該是可擴充套件的,而不可修改的。也就是,對擴充套件開放,對修改封閉的。  (3)里氏替換原則(Liskov-Substituion Principle):子類必須能夠替換其基類。這一思想體現為對繼承機制的約束規範,只有子類能夠替換基類時,才能保證系統在執行期內識別子類,這是保證繼承複用的基礎。  (4)依賴倒置原則(Dependecy-Inversion Principle):依賴於抽象。具體而言就是高層模組不依賴於底層模組,二者都同依賴於抽象;抽象不依賴於具體,具體依賴於抽象。  (5)介面隔離原則(Interface-Segregation Principle):使用多個小的專門的介面,而不要使用一個大的總介面 (6)迪米特原則:一個物件對於其他的物件儲存儘量少的瞭解 (7)組合/聚合原則:類間關係儘量使用關聯關係(組合,聚合),而少使用繼承;

15:Java中常見的OOM和導致OOM的原因有哪些?

答:常見的OOM型別有如下幾種:

(1)堆記憶體溢位 (2)虛擬機器棧和本地方法棧溢位 (3)執行時常量池溢位 (4)方法區溢位

常見的OOM以及解決思路:

(1) java.lang.OutOfMemoryError: unable to create new native thread 當呼叫new Thread時,如已建立不了執行緒了,則會丟擲此錯誤,如果是JDK內部必須建立成功的執行緒,那麼會造成Java程序退出,如果是使用者執行緒,則僅丟擲OOM,建立不了的原因通常是建立了太多執行緒,耗盡了記憶體,通常可通過減少建立的執行緒數,或通過-Xss調小執行緒所佔用的棧大小來減少對Java 對外記憶體的消耗。 (2)java.lang.OutOfMemoryError: request bytes for . Out of swap space? 當JNI模組或JVM內部進行malloc操作(例如GC時做mark)時,需要消耗堆外的記憶體,如此時Java程序所佔用的地址空間超過限制(例如windows: 2G,linux: 3G),或實體記憶體、swap區均使用完畢,那麼則會出現此錯誤,當出現此錯誤時,Java程序將會退出。 (3)java.lang.OutOfMemoryError: Java heap space(堆溢位) ,這是最常見的OOM錯誤 【解決思路】  a.增加Java虛擬機器中Xms(初始堆大小)和Xmx(最大堆大小)引數的大小  b.檢查是否發生記憶體洩漏  c.看是否有死迴圈或不必要地重複建立大量物件  (4) java.lang.OutOfMemoryError: GC overhead limit execeeded 當通過new建立物件或陣列時,如Java Heap空間不足,且GC所使用的時間佔了程式總時間的98%,且Heap剩餘空間小於2%,則丟擲此錯誤,以避免Full GC一直執行,可通過UseGCOverheadLimit來決定是否開啟這種策略,可通過GCTimeLimit和GCHeapFreeLimit來控制百分比。 (5) java.lang.OutOfMemoryError: PermGen space(方法區或者執行時常量池溢位) 【解決思路】  a.增加java虛擬機器中的XX:PermSize和XX:MaxPermSize引數的大小  b.頻繁使用CGLib,動態代理,反射GeneratedConstructorAccessor需要強大的方法區來支撐  (6)java.lang.StackOverflowError 虛擬機器棧和本地方法棧溢位  說明:對於以上幾種OOM錯誤,其中容易造成嚴重後果的是Out of swap space這種,因為這種會造成Java程序退出,而其他幾種只要不是在main執行緒丟擲的,就不會造成Java程序退出

導致OOM的原因: (1)記憶體洩漏(連線未關閉,單例類中不正確引用了物件) (2)程式碼中存在死迴圈或迴圈產生過多重複的物件實體,即會發生java.lang.stackoverflow異常 (3)Space大小設定不正確,即會導致outofMemory:permgenspace的這種方法區溢位 (4)記憶體中載入的資料量過於龐大,如一次從資料庫取出過多資料,即會導致outofMemory:permgenspace的這種方法區溢位 (5)集合類中有對物件的引用,使用完後未清空,使得JVM不能回收,即會發生記憶體洩露 (6)無限遞迴次數,會導致執行緒棧溢位,即發生java.lang.stackoverflow異常 (7)程式載入的類過多,或者使用反射和cglib技術產生過多的類,即會導致outofMemory:permgenspace的這種方法區溢位

16:請問,你有進行過JVM調優嗎?

答:一般主要回答一下:JVM的記憶體結構;堆的劃分;GC的清除方法,GC的回收器;程式異常的型別(Error和Exception);常見的OOM的處理;等等資訊

17:ConcurrentHashMap中的jdk1.7和jdk1.8的區別

jdk1.7版本: HashTable容器在競爭激烈的併發環境下表現出效率低下的原因,是因為所有訪問HashTable的執行緒都必須競爭同一把鎖,那假如容器裡有多把鎖,每一把鎖用於鎖容器其中一部分資料,那麼當多執行緒訪問容器裡不同資料段的資料時,執行緒間就不會存在鎖競爭,從而可以有效的提高併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術,首先將資料分成一段一段的儲存,然後給每一段資料配一把鎖,當一個執行緒佔用鎖訪問其中一個段資料的時候,其他段的資料也能被其他執行緒訪問。       那麼ConcurrentHashMap是如何判斷在統計的時候容器是否發生了變化呢?使用modCount變數,在put , remove和clean方法裡操作元素前都會將變數modCount進行加1,那麼在統計size前後比較modCount是否發生變化,從而得知容器的大小是否發生變化。    底層是由:陣列和連結串列實現的;jdk1.8版本的改進: 1、不採用segment而採用node,鎖住node來實現減小鎖粒度。  2、設計了MOVED狀態 當resize的中過程中 執行緒2還在put資料,執行緒2會幫助resize。  3、使用3個CAS操作來確保node的一些操作的原子性,這種方式代替了鎖。  4、sizeCtl的不同值來代表不同含義,起到了控制的作用。 5.底層是由:陣列和連結串列+紅黑樹實現的;

18:CopyOnWriteArraylist的底層是什麼?適用什麼情況?與Collections.synchrnizedlist的區別

答:CopyOnWriteArrayList這是一個ArrayList的執行緒安全的變體,其原理大概可以通俗的理解為:初始化的時候只有一個容器,很常一段時間,這個容器資料、數量等沒有發生變化的時候,大家(多個執行緒),都是讀取(假設這段時間裡只發生讀取的操作)同一個容器中的資料,所以這樣大家讀到的資料都是唯一、一致、安全的,但是後來有人往裡面增加了一個數據,這個時候CopyOnWriteArrayList 底層實現新增的原理是先copy出一個容器(可以簡稱副本),再往新的容器裡新增這個新的資料,最後把新的容器的引用地址賦值給了之前那個舊的的容器地址,但是在新增這個資料的期間,其他執行緒如果要去讀取資料,仍然是讀取到舊的容器裡的資料。

優點:

1.解決的開發工作中的多執行緒的併發問題。2:用於讀多寫少的併發場景

缺點: 1.記憶體佔有問題:很明顯,兩個陣列同時駐紮在記憶體中,如果實際應用中,資料比較多,而且比較大的情況下,佔用記憶體會比較大,針對這個其實可以用ConcurrentHashMap來代替。

2.資料一致性:CopyOnWrite容器只能保證資料的最終一致性,不能保證資料的實時一致性。所以如果你希望寫入的的資料,馬上能讀到,請不要使用CopyOnWrite容器

不同點:CopyOnWriteArrayList和Collections.synchronizedList是實現執行緒安全的列表的兩種方式。兩種實現方式分別針對不同情況有不同的效能表現,其中CopyOnWriteArrayList的寫操作效能較差,而多執行緒的讀操作效能較好。而Collections.synchronizedList的寫操作效能比CopyOnWriteArrayList在多執行緒操作的情況下要好很多,而讀操作因為是採用了synchronized關鍵字的方式,其讀操作效能並不如CopyOnWriteArrayList。因此在不同的應用場景下,應該選擇不同的多執行緒安全實現類。

19:Java執行緒中呼叫的方法的狀態轉變

20:Reentrantlock和Sychronized的區別?

(1)等待可中斷:前者能夠對在等待的執行緒,當等待足夠長的時間後可以進行可中斷的操作;而後者不可以,必須一直等待擁有資源的執行緒進行釋放資源; (2)是否可設定公平鎖:前者能夠在進行建構函式的時候,傳入true(預設是false,非公平鎖),表示進行的是公平鎖,也就是說對於先進行等待的執行緒先執行,而不是像後者一樣進行隨機的選擇執行的執行緒; (3)是否可以繫結多個Condition:前者是能夠對多個condition進行繫結的,而後者則不行 (4)實現的層次:前者是屬於JDK中的,其底層就是通過自旋鎖,而後者是屬於JVM來進行實現的; (5)是否需要手動釋放:前者是通過lock()方法進行加鎖,一般是要在finily()方法進行unlock()方法的釋放,而後者一般是不需要手動進行釋放鎖;

21:序列化和反序列的含義和底層原理?

答:含義(底層原理): (1)Java序列化是指把Java物件轉換為位元組序列的過程,而Java反序列化是指把位元組序列恢復為Java物件的過程; (2)序列化:物件序列化的最主要的用處就是在傳遞和儲存物件的時候,保證物件的完整性和可傳遞性。序列化是把物件轉換成有序位元組流,以便在網路上傳輸或者儲存在本地檔案中。序列化後的位元組流儲存了Java物件的狀態以及相關的描述資訊。序列化機制的核心作用就是物件狀態的儲存與重建。 (3)反序列化:客戶端從檔案中或網路上獲得序列化後的物件位元組流後,根據位元組流中所儲存的物件狀態及描述資訊,通過反序列化重建物件。 (4)本質上講,序列化就是把實體物件狀態按照一定的格式寫入到有序位元組流,反序列化就是從有序位元組流重建物件,恢復物件狀態。 作用: (1)永久性儲存物件,儲存物件的位元組序列到本地檔案或者資料庫中;  (2)通過序列化以位元組流的形式使物件在網路中進行傳遞和接收;  (3)通過序列化在程序間傳遞物件; 實現的方式:(三種) 假定一個User類,它的物件需要序列化,可以有如下三種方法: (1)若User類僅僅實現了Serializable介面,則可以按照以下方式進行序列化和反序列化 ObjectOutputStream採用預設的序列化方式,對User物件的非transient的例項變數進行序列化。  ObjcetInputStream採用預設的反序列化方式,對對User物件的非transient的例項變數進行反序列化。 (2)若User類僅僅實現了Serializable介面,並且還定義了readObject(ObjectInputStream in)和writeObject (ObjectOutputSteam out),則採用以下方式進行序列化與反序列化。 ObjectOutputStream呼叫User物件的writeObject(ObjectOutputStream out)的方法進行序列化。  ObjectInputStream會呼叫User物件的readObject(ObjectInputStream in)的方法進行反序列化。 (3)若User類實現了Externalnalizable介面,且User類必須實現readExternal(ObjectInput in)和writeExternal (ObjectOutput out)方法,則按照以下方式進行序列化與反序列化。 ObjectOutputStream呼叫User物件的writeExternal(ObjectOutput out))的方法進行序列化。  ObjectInputStream會呼叫User物件的readExternal(ObjectInput in)的方法進行反序列化。 注意事項: (1)要進行序列化的類,必須實現Serialazable介面(相比實現Externalnalizable介面好) (2)序列化時,只對物件的狀態進行儲存,而不管物件的方法; (3)當一個父類實現序列化,子類自動實現序列化,不需要顯式實現Serializable介面; (4)當一個物件的例項變數引用其他物件,序列化該物件時也把引用物件進行序列化; (5)宣告為static和transient型別的成員資料不能被序列化。因為static代表類的狀態,transient代表物件的臨時資料。 (6)序列化執行時使用一個稱為 serialVersionUID 的版本號與每個可序列化類相關聯,該序列號在反序列化過程中用於驗證序列化物件的傳送者和接收者是否為該物件載入了與序列化相容的類。為它賦予明確的值。顯式地定義serialVersionUID有兩種用途:         在某些場合,希望類的不同版本對序列化相容,因此需要確保類的不同版本具有相同的serialVersionUID;         在某些場合,不希望類的不同版本對序列化相容,因此需要確保類的不同版本具有不同的serialVersionUID。 (7)如果一個物件的成員變數是一個物件,那麼這個物件的資料成員也會被儲存!這是能用序列化解決深拷貝的重要原因;

22:Java同步框架中的AQS?

23:Synchronized的底層原理?

答:使用的形式有三種: 對於普通同步方法,鎖是當前例項物件。 對於靜態同步方法,鎖是當前類的Class物件。 對於同步方法塊,鎖是Synchonized括號裡配置的物件。實現的原理: synchronized是基於Monitor來實現同步的。 Monitor從兩個方面來支援執行緒之間的同步: 互斥執行 協作 1、Java 使用物件鎖 ( 使用 synchronized 獲得物件鎖 ) 保證工作在共享的資料集上的執行緒互斥執行。 2、使用 notify/notifyAll/wait 方法來協同不同執行緒之間的工作。 3、Class和Object都關聯了一個Monitor。Monitor 的工作機理 1:執行緒進入同步方法中。 2:為了繼續執行臨界區程式碼,執行緒必須獲取 Monitor 鎖。如果獲取鎖成功,將成為該監視者物件的擁有者。任一時刻內,監視者物件只屬於一個活動執行緒(The Owner) 3:擁有監視者物件的執行緒可以呼叫 wait() 進入等待集合(Wait Set),同時釋放監視鎖,進入等待狀態。 4:其他執行緒呼叫 notify() / notifyAll() 介面喚醒等待集合中的執行緒,這些等待的執行緒需要重新獲取監視鎖後才能執行 wait() 之後的程式碼。 5:同步方法執行完畢了,執行緒退出臨界區,並釋放監視鎖synchronized的鎖優化

鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖。這種鎖升級卻不能降級的策略,目的是為了提高獲得鎖和釋放鎖的效率。其中主要就是偏向鎖,輕量級鎖,重量級鎖。

24:Java中造成記憶體洩露的情況有哪些?

關於Java中的記憶體溢位的知識點,請參考上面的第15個知識點

25:CyclicBarrier和CountDownLatch的區別

答:兩個看上去有點像的類,都在java.util.concurrent下,都可以用來表示程式碼執行到某個點上,二者的區別在於: (1)CyclicBarrier的某個執行緒執行到某個點上之後,該執行緒即停止執行,直到所有的執行緒都到達了這個點,所有執行緒才重新執行;CountDownLatch則不是,某執行緒執行到某個點上之後,只是給某個數值-1而已,該執行緒繼續執行 (2)CyclicBarrier只能喚起一個任務,CountDownLatch可以喚起多個任務 (3)CyclicBarrier可重用,CountDownLatch不可重用,計數值為0該CountDownLatch就不可再用了

26:什麼是執行緒安全?

答:執行緒安全也是有幾個級別的: (1)不可變 像String、Integer、Long這些,都是final型別的類,任何一個執行緒都改變不了它們的值,要改變除非新建立一個,因此這些不可變物件不需要任何同步手段就可以直接在多執行緒環境下使用 (2)絕對執行緒安全 不管執行時環境如何,呼叫者都不需要額外的同步措施。要做到這一點通常需要付出許多額外的代價,Java中標註自己是執行緒安全的類,實際上絕大多數都不是執行緒安全的,不過絕對執行緒安全的類,Java中也有,比方說CopyOnWriteArrayList、CopyOnWriteArraySet (3)相對執行緒安全 相對執行緒安全也就是我們通常意義上所說的執行緒安全,像Vector這種,add、remove方法都是原子操作,不會被打斷,但也僅限於此,如果有個執行緒在遍歷某個Vector、有個執行緒同時在add這個Vector,99%的情況下都會出現ConcurrentModificationException,也就是fail-fast機制。 (4)執行緒非安全 這個就沒什麼好說的了,ArrayList、LinkedList、HashMap等都是執行緒非安全的類

27:為什麼需要執行緒池?執行緒池的構造引數有哪些?分別的意思代表什麼?

答:執行緒池的作用:

(1)減少建立和銷燬執行緒的次數,每個工作執行緒可以多次使用 (2)可根據系統情況調整執行的執行緒數量,防止消耗過多記憶體 (3)方便對執行緒進行管理

ThreadPoolExecutor類執行緒池的引數: (1)corePoolSize:核心執行緒數,核心執行緒會一直存活,即使沒有任務需要處理。當執行緒數小於核心執行緒數時,即使現有的執行緒空閒,執行緒池也會優先建立新執行緒來處理任務,而不是直接交給現有的執行緒處理 (2)maxPoolSize:當執行緒數大於或等於核心執行緒,且任務佇列已滿時,執行緒池會建立新的執行緒,直到執行緒數量達到maxPoolSize。如果執行緒數已等於maxPoolSize,且任務佇列已滿,則已超出執行緒池的處理能力,執行緒池會拒絕處理任務而丟擲異常。 (3)keepAliveTime:當執行緒空閒時間達到keepAliveTime,該執行緒會退出,直到執行緒數量等於corePoolSize。如果allowCoreThreadTimeout設定為true,則所有執行緒均會退出直到執行緒數量為0。 (4)allowCoreThreadTimeout:是否允許核心執行緒空閒退出,預設值為false (5)queueCapacity:任務佇列容量。從maxPoolSize的描述上可以看出,任務佇列的容量會影響到執行緒的變化,因此任務佇列的長度也需要恰當的設定,一般的話都是使用無邊界的阻塞佇列,比如LinkedBlockQueue執行緒池按以下行為執行任務:(需要注意第二點和第三點) 當執行緒數小於核心執行緒數時,建立執行緒。 當執行緒數大於等於核心執行緒數,且任務佇列未滿時,將任務放入任務佇列。 當執行緒數大於等於核心執行緒數,且任務佇列已滿 若執行緒數小於最大執行緒數,建立執行緒 若執行緒數等於最大執行緒數,丟擲異常,拒絕任務

Java執行緒池的型別:

newCacheThreadPool()----緩衝個數的執行緒池 newFixedThreadPool()-----固定個數的執行緒池 newSingleThreadPool()------單一執行緒池,即只有一個執行緒(注意:如果這裡面的那個執行緒死了,就馬上會建立一個新的執行緒繼續工作) newScheduledThreadPool()------定時任務的執行緒池

執行緒池的shutDown()和shutDownNow()方法的區別:

兩者都是銷燬執行緒池,前者是當執行緒池中所有的執行緒任務執行完成之後,就會把執行緒池進行銷燬,而後者當執行到這句話之後,就會把執行緒池銷燬,而不會關注到執行緒池中的執行緒任務是否已經執行完成。

執行緒池的excute()和submit()方法的區別:

兩者都是新增執行緒,前者在新增完一個執行緒之後,沒有帶返回值,而後者在新增之後有返回值,返回的是一個Future物件,通過這個物件的相關方法可以判斷當前執行緒是否執行完成或者如果執行緒執行過程中出現異常,通過這個物件也可以檢視到出現異常的原因。

28:Java編寫一個會死鎖的程式?

29:Hashtable的size()方法中明明只有一條語句"return count",為什麼還要做同步?

答:主要原因有兩點: (1)同一時間只能有一條執行緒執行固定類的同步方法,但是對於類的非同步方法,可以多條執行緒同時訪問。所以,這樣就有問題了,可能執行緒A在執行Hashtable的put方法新增資料,執行緒B則可以正常呼叫size()方法讀取Hashtable中當前元素的個數,那讀取到的值可能不是最新的,可能執行緒A添加了完了資料,但是沒有對size++,執行緒B就已經讀取size了,那麼對於執行緒B來說讀取到的size一定是不準確的。而給size()方法加了同步之後,意味著執行緒B呼叫size()方法只有在執行緒A呼叫put方法完畢之後才可以呼叫,這樣就保證了執行緒安全性 (2)CPU執行程式碼,執行的不是Java程式碼,這點很關鍵,一定得記住。Java程式碼最終是被翻譯成機器碼執行的,機器碼才是真正可以和硬體電路互動的程式碼。即使你看到Java程式碼只有一行,甚至你看到Java程式碼編譯之後生成的位元組碼也只有一行,也不意味著對於底層來說這句語句的操作只有一個。一句"return count"假設被翻譯成了三句彙編語句執行,一句彙編語句和其機器碼做對應,完全可能執行完第一句,執行緒就切換了。

30:高併發、任務執行時間短的業務怎樣使用執行緒池?併發不高、任務執行時間長的業務怎樣使用執行緒池?併發高、業務執行時間長的業務怎樣使用執行緒池?

答:(1)高併發、任務執行時間短的業務,執行緒池執行緒數可以設定為CPU核數+1,減少執行緒上下文的切換 (2)併發不高、任務執行時間長的業務要區分開看:   a)假如是業務時間長集中在IO操作上,也就是IO密集型的任務,因為IO操作並不佔用CPU,所以不要讓所有的CPU閒下來,可以加大執行緒池中的執行緒數目,讓CPU處理更多的業務   b)假如是業務時間長集中在計算操作上,也就是計算密集型任務,這個就沒辦法了,和(1)一樣吧,執行緒池中的執行緒數設定得少一些,減少執行緒上下文的切換 (3)併發高、業務執行時間長,解決這種型別任務的關鍵不在於執行緒池而在於整體架構的設計,看看這些業務裡面某些資料是否能做快取是第一步,增加伺服器是第二步,至於執行緒池的設定,設定參考(2)。最後,業務執行時間長的問題,也可能需要分析一下,看看能不能使用中介軟體對任務進行拆分和解耦

31:單例模式和靜態方法的區別(使用情景)?

答:這個問題是在騰訊二面的時候被問到的,被深層次的懟了,所以,自己就好好整理了下;

(1)單例模式的建立方式,五種,餓漢,懶漢,雙重檢查,靜態內部類,列舉(Effective Java推薦這種);----要掌握 (2)單例類可以實現介面和繼承類,這樣能夠進行更多的業務和功能擴充套件,而對於靜態方法來說,你每要進行擴充套件一個功能,那麼就需要進行新增; (3)生命週期:對於單例模式產生的那一個唯一例項,是不會被GC(因為單例中的那個變數是static的,是不會被回收)只有當JVM停止之後,才會被回收;靜態方法裡面的變數,當靜態方法執行完後,都會被回收,所以對於會重複進行初始化使用的物件的話,這樣當呼叫一次就要進行初始化一次,並且靜態方法的類是在程式碼被編譯的時候就會被載入; (4)記憶體:單例模式在執行的時候需要new一個物件出來儲存在堆疊中,可以被延遲初始化;而靜態方法是不需要的,它不依賴於物件,而可以通過類進行直接呼叫,它是以程式碼塊的形式進行儲存; (5)單例模式是一種面向物件的程式設計模式,而靜態方法則是一種面向過程的模式; (6)單例模式保證了其中的物件只會存在一個例項物件;

32:GC中利用可達性分析方法中,能夠作為GC Root的有哪些物件?

答:在《深度理解Java 虛擬機器》書中,主要就是提到下面這幾種:

虛擬機器棧中的引用物件 方法區中類靜態屬性引用的物件 方法區中常量引用物件 本地方法棧中JNI引用物件

33:說說類載入機制和雙親委派模型?

(1)類載入的過程:載入,連線(驗證->準備->解析),初始化,使用,解除安裝;

(2)類載入器的種類(預定義三種 + 一種自定義):

1、Bootstrap ClassLoader:啟動類載入器,也叫根類載入器,它負責載入Java的核心類庫,載入如(%JAVA_HOME%/lib)目錄下的rt.jar(包含System、String這樣的核心類)這樣的核心類庫。根類載入器非常特殊,它不是java.lang.ClassLoader的子類,它是JVM自身內部由C/C++實現的,並不是Java實現的。 2、Extension ClassLoader:擴充套件類載入器,它負責載入擴充套件目錄(%JAVA_HOME%/jre/lib/ext)下的jar包,使用者可以把自己開發的類打包成jar包放在這個目錄下即可擴充套件核心類以外的新功能。 3、System ClassLoader\APP ClassLoader:系統類載入器或稱為應用程式類載入器,是載入CLASSPATH環境變數所指定的jar包與類路徑。一般來說,使用者自定義的類就是由APP ClassLoader載入的。 4、自定義的類載入器,主要就是通過繼承ClassLoad類,然後進行重寫裡面的findClass()方法

(3)如何判斷兩個類是否為同一個類:

1、兩個類來自同一個Class檔案 2、兩個類是由同一個虛擬機器載入 3、兩個類是由同一個類載入器載入所以,在JVM中,判斷兩個類是否是相等的,就需要判斷 類載入器 + 類名 的形式

(4)雙親委派模型(重點):當一個類收到了類載入請求,他首先不會嘗試自己去載入這個類,而是把這個請求委派給父類去完成,每一個層次類載入器都是如此,因此所有的載入請求都應該傳送到啟動類載入其中,只有當父類載入器反饋自己無法完成這個請求的時候(在它的載入路徑下沒有找到所需載入的Class),子類載入器才會嘗試自己去載入。

(5)雙親委派模型的作用:

(1)主要是為了安全性,避免使用者自己編寫的類動態替換 Java的一些核心類,比如 String。 (2)同時也避免了類的重複載入,因為 JVM中區分不同類,不僅僅是根據類名,相同的 class檔案被不同的 ClassLoader載入就是不同的兩個類。

(6)Class.forName()和ClassLoader.loadClass()的區別    Class.forName():將類的.class檔案載入到jvm中之外,還會對類進行解釋,執行類中的static塊;(它也有可以控制static塊是否執行的forName()函式);    ClassLoader.loadClass():只幹一件事情,就是將.class檔案載入到jvm中,不會執行static中的內容,只有在newInstance才會去執行static塊。

34:Tomcat的類載入與JVM的類載入有什麼不同?(很重要的知識)

答:最主要的就是Tomcat的類載入不是採用雙親委派模型,這個是非常重要的,而原因是為什麼主要就是下面的:

好好參考看一下這兩篇文章,分別從簡單到詳細的介紹:

35:Java執行緒實現同步的方式有哪些?

36:Java執行緒進行通訊的方式有哪些?

37:Java單例模式的實現方式有哪些?各自的特點又有什麼?

38:Java中的BIO,NIO,AIO的含義和特點?

39:子類覆蓋父類方法的注意事項有哪些?(這個問題很多人並不全瞭解)

(1)覆蓋的方法名,引數,返回型別必須一致 (2)子類不能縮小父類方法的訪問許可權 (3)子類不能丟擲比父類大的異常 (4)方法覆蓋只發生在子類和父類,而同一個類中只會發生方法過載 (5)父類的靜態方法不能被子類覆蓋為非靜態方法 (6)父類的非靜態方法可以被子類覆蓋為靜態方法 (7)子類可以定義和父類的靜態方法同名的靜態方法----這時候就要注意使用的是父類還是子類的例項物件了,這時候就是一種動態繫結,看左邊 (8)父類的私有方法不能被子類覆蓋-------因為私有方法是不會被子類繼承的 (9)父類的抽象方法可以被子類通過兩種途徑覆蓋;其一:通過實現抽象方法;其二:通過將子類作為抽象類,重新宣告父類的抽象方法 (10)父類的非抽象方法可以被子類覆蓋為抽象方法

40:請說說你對Java中的原子性,可見性和順序性的理解?(Java程式內部解析模組知識)

(1)原子性

         原子性是拒絕多執行緒操作的,不論是多核還是單核,具有原子性的量,同一時刻只能有一個執行緒來對它進行操作。簡而言之,在整個操作過程中不會被執行緒排程器中斷的操作,都可認為是原子性。例如 a=1是原子性操作,但是a++和a +=1就不是原子性操作。

(2)可見性

定義:當一個執行緒對共享變數進行了修改,但是還沒有進行更新到記憶體的時候,此時,另一個執行緒,對該變數進行了操作,然而,由於還沒有進行更新,所以讀取的還是初始的變數的值,從而會發現變數不一致,出現覆蓋的情況;而這個正是由於對可見性的一種體現。

解決方法:1:對操作方法進行同步,使用Sychonize關鍵字(2)對方法進行加鎖處理,比如ReentLock(3)使用volatile關鍵字修改共享變數,但是注意,當且僅當是對該共享變數進行的原子性操作,這個方法才有效,而對於非原子性操作同樣無法保證可見性;

(3)順序性

定義:對於程式中的程式碼順序,如果不具有相關性約束,那麼在程式進行解析執行的時候,並不一定需要按照順序執行,但是一定需要保證前後執行的結果是一致;主要就是為了提高程式碼的執行效率

比如程式碼:int a = 1; int b = a+2; int c = 3; int d = c+1;

解析:對於上面的程式碼,雖然int c =3,是在第三句話,但是,由於與前面的沒有約束關係,所以,這句程式碼並不一樣在前面兩句程式碼後面執行,但是一定保證,int d 在 int c的後面,因為d中用到了c的變數,同理對於int b也是一樣的道理;

擴充套件知識點1:Happen-Before規則

(1)程式順序原則:一個執行緒內保證語義的序列性 a = 1 ; b = a+1 (2)volatile規則:valatile變數的寫,先發生於讀,這保證了volatile變數的可見性 (3)鎖規則:解鎖(unlock)必然發生在隨後的加鎖(lock)前 (4)傳遞性:A先於B,B先於C,那麼A必然先於C (5)執行緒的start()方法先於它的每一個動作 (6)執行緒的所有操作先於執行緒的終結(Thread.join()) (7)執行緒的中斷(interrupt())先於被中斷執行緒的程式碼

(8)物件的建構函式執行結束先於finalize()方法

擴充套件知識點2:fail-fast機制

定義: 當多個執行緒對同一個集合的內容進行操作時,就可能會產生fail-fast事件。例如:當某一個執行緒A通過iterator去遍歷某集合的過程中,若該集合的內容被其他執行緒所改變了;那麼執行緒A訪問集合時,就會丟擲ConcurrentModificationException異常,產生fail-fast事件。產生原因(原始碼角度):通過檢視迭代器Iterator的原始碼可以看到,當modCount != expectedModCount的時候,會丟擲併發修改異常.expectedModCount是在建立迭代器物件的時候,將modCount賦值初始化來的,所以當 list在建立迭代器之後,再次發生modCount變化的時候就會出現與期望的count不一致的情況。基本是add,remove,clear的時候會出現modCount的變化,於是     1,在單執行緒操作不符合規則的時候,list加入元素,迭代器不知道,modCount發生變化,與期望不等     2,多執行緒操作的時候,執行緒1在獲取到當前modCount的時候,執行緒2進行了一些涉及元素個數變化的操作使得modCount發生了變化,與期望的count不等然後會丟擲ConcurrentModificationException異常,產生fail-fast機制。解決的方法(歸根到底就是利用同步的形式來解決非同步的處理): 1:在遍歷過程中所有涉及到改變modCount值得地方全部加上synchronized或者直接使用Collections.synchronizedList,這樣就可以解決。但是不推薦,因為增刪造成的同步鎖可能會阻塞遍歷操作。 2:使用CopyOnWriteArrayList來替換ArrayList。 CopyOnWriteArrayList是java.util.concurrent包下的類,支援多執行緒操作。其底層實現和ArrayList一樣也是陣列實現,同樣有add remove等操作方法。

41:JVM的調式工具有用過什麼?(JVM模組知識)

(1)jps:用來檢視基於HotSpot JVM裡面所有程序的具體狀態, 包括程序ID,程序啟動的路徑等等。使用jps時,不需要傳遞程序號做為引數。 Jps也可以顯示遠端系統上的JAVA程序,這需要遠端服務上開啟了jstat服務,以及RMI注及服務,不過常用都是對本對的JAVA程序的檢視。 命令格式 jps [ options ] [ hostid ](2)jstak:Jstat是JDK自帶的一個輕量級小工具。它位於java的bin目錄下,主要利用JVM內建的指令對Java應用程式的資源和效能進行實時的命令列的監控,包括了對Heap size和垃圾回收狀況的監控。詳細檢視堆內各個部分的使用量,以及載入類的數量。使用時,需加上檢視程序的程序id,和所選引數,它主要是用來顯示GC及PermGen相關的資訊。 命令格式 jstat [ generalOption | outputOptions vmid [interval[s|ms] [count]] ](3)jstack:jstack用於打印出給定的java程序ID或core file或遠端除錯服務的Java堆疊資訊,如果是在64位機器上,需要指定選項"-J-d64", 命令格式 jstack [ option ] pid jstack [ option ] executable core jstack [ option ] [[email protected]]remote-hostname-or-IP(4)jmap:打印出某個java程序(使用pid)記憶體內的,所有物件的情況(如:產生那些物件,及其數量)。可以輸出所有記憶體中物件的工具,甚至可以將VM 中的heap,以二進位制輸出成文字。(5)jconsole:jconsole:一個java GUI監視工具,可以以圖表化的形式顯示各種資料。並可通過遠端連線監視遠端的伺服器VM。用java寫的GUI程式,用來監控VM,並可監控遠端的VM,非常易用,而且功能非常強。命令列裡打 jconsole,選則程序就可以了。需要注意的就是在執行jconsole之前,必須要先設定環境變數DISPLAY,否則會報錯誤,Linux下設定環境變數如下:export DISPLAY=:0.0。(另外還有一個jvisualvm類似功能,只是介面好看一點) 命令格式 SYNOPSIS jmap [ option ] pid jmap [ option ] executable core jmap [ option ] [[email protected]]remote-hostname-or-IP

42:AtomicInteger,AtomicIntegerArray,AtomicIntegerFieldUpdate三者之間的差別(併發模組知識)

答:相同點:它們三者的操作都是屬於原子性的操作,對於多執行緒的操作中都是安全的,可以不需要同步方法,因為內容進行了CAS的處理。 (1)AtomicInteger:是對單個Integer型別的變數進行了原子性操作 (2)AtomicIntegerArray:是對Integer型別陣列的相關原子性操作 (3)AtomicIntegerFieldUpdate:是對於物件型別中的某個Integer域變數的原子性操作

43:請說說你對於回撥的理解

答:所謂回撥:就是A類中呼叫B類中的某個方法C,然後B類中反過來呼叫A類中的方法D,D這個方法就叫回調方法。

回撥的實現步驟:

  • Class A實現介面CallBack callback——背景1
  • class A中包含一個class B的引用b ——背景2
  • class B有一個引數為callback的方法f(CallBack callback) ——背景3
  • A的物件a呼叫B的方法 f(CallBack callback) ——A類呼叫B類的某個方法 C
  • 然後b就可以在f(CallBack callback)方法中呼叫A的方法 ——B類呼叫A類的某個方法D

44:請說說對於Java併發程式設計中的死鎖,活鎖以及飢餓的含義?(併發模組知識)

死鎖:發生在一個執行緒需要獲取多個資源的時候,這時由於兩個執行緒互相等待對方的資源而被阻塞,死鎖是最常見的活躍性問題。 “死鎖”的例子1:如果執行緒A鎖住了記錄R1並等待記錄R2,而執行緒B鎖住了記錄R2並等待記錄R1,這樣兩個執行緒A和B就發生了死鎖現象。 “死鎖”的例子2:兩個山羊過一個獨木橋,兩隻羊同時走到橋中間,一個山羊等另一個山羊過去了然後再過橋,另一個山羊等這一個山羊過去,結果兩隻山羊都堵在中間動彈不得。飢餓:指的執行緒無法訪問到它需要的資源而不能繼續執行時,引發飢餓最常見資源就是CPU時鐘週期。雖然在Thread API中由指定執行緒優先順序的機制,但是隻能作為作業系統進行執行緒排程的一個參考,換句話說就是作業系統在進行執行緒排程是平臺無關的,會盡可能提供公平的、活躍性良好的排程,那麼即使在程式中指定了執行緒的優先順序,也有可能在作業系統進行排程的時候對映到了同一個優先順序。通常情況下,不要區修改執行緒的優先順序,一旦修改程式的行為就會與平臺相關,並且會導致飢餓問題的產生。在程式中使用的Thread.yield或者Thread.sleep表明該程式試圖客服優先順序調整問題,讓優先順序更低的執行緒擁有被CPU排程的機會。 例項:資源在其中兩個或以上執行緒或程序相互使用,第三方執行緒或程序始終得不到。想像一下三個人傳球,其中兩個人傳來傳去,第三個人始終得不到活鎖:指的是執行緒不斷重複執行相同的操作,但每次操作的結果都是失敗的。儘管這個問題不會阻塞執行緒,但是程式也無法繼續執行。活鎖通常發生在處理事務訊息的應用程式中,如果不能成功處理這個事務那麼事務將回滾整個操作。解決活鎖的辦法是在每次重複執行的時候引入隨機機制,這樣由於出現的可能性不同使得程式可以繼續執行其他的任務。 例項:個人在一個很宅的衚衕裡。 一次只能並排過兩個人。 兩人比較禮貌,都要給對方讓路。 結果一起要麼讓到左邊,要麼讓到右邊,結果仍然是誰也過不去。 類似於原地踏步或者震盪狀態

45:請說說在多執行緒程式設計中,瞭解過ABA問題嗎?(併發模組知識)

ABA問題即是:執行緒1準備用CAS將變數的值由A替換為B,在此之前,執行緒2將變數的值由A替換為C,又由C替換為A,然後執行緒1執行CAS時發現變數的值仍然為A,所以CAS成功。但實際上這時的現場已經和最初不同了,儘管CAS成功,但可能存在潛藏的問題。解決辦法:AtomicStampedRerence增加一個時間戳,進行CAS操作時不僅需要維護物件值,還需要維護時間戳。物件值和時間戳都必須滿足期望值,才能更新新值。

46:為什麼說區域性變數是執行緒安全的呢?(JVM模組知識)

答:這個問題就是也是考察對JVM記憶體模型的瞭解程度。

      JVM在執行Java程式時,會根據其資料用途把管理的記憶體劃分為若干資料區域,包括方法區,堆,棧(JVM棧、本地方法棧),程式計數器。其中前兩者是有所有java執行緒所共有的,而後兩者是每個執行緒所獨有的,因此,棧是執行緒私有的,一個執行緒一個棧。並且棧由一系列棧幀組成,棧幀儲存了一個方法的區域性變量表(包括引數和區域性變數)、運算元棧、常量池指標等,每一次方法的呼叫實際上是建立一個棧幀,並且壓棧。        所以方法的呼叫實際是棧幀在棧中入棧和出棧的操作。因為棧是執行緒私有的,所以每個棧之間是獨立的,所以棧幀對於多個執行緒棧來說不存在共享問題,也就不會存線上程安全問題了。

47:請說說你對volatile關鍵字的理解和工作原理?(Java可見性知識)

答:這個問題其實考察的就是Java中程式指令的原子性,可見性和順序性以及JVM的記憶體模型的知識點。       根據JVM規範的規定,volatile變數依然有工作記憶體的拷貝,但是由於它特殊的操作順序規定,所以看起來如同直接在主記憶體中讀寫一般,因此這裡的描述對於volatile也沒有例外。volatile具體實現細節如下:        如果對聲明瞭 volatile 的變數進行寫操作,JVM 就會向處理器傳送一條 Lock 字首的指令,將這個變數所在工作記憶體的資料寫回到主記憶體。但是,就算寫回到主記憶體,如果其他執行緒工作記憶體的值還是舊的,再執行計算操作就會有問題。所以,在多處理器下,為了保證各個處理器的快取(快取中的值來至於工作記憶體)是一致的,就會實現快取一致性協議,每個處理器通過嗅探在總線上傳播的資料來檢查自己快取的值是不是過期了,當處理器發現自己快取行對應的記憶體地址被修改,就會將當前處理器的快取行設定成無效狀態,當處理器對這個資料進行修改操作的時候,會重新從主記憶體中把資料讀到處理器快取裡。

48:說說關於JVM中四種物件引用的特點和應用場景?(JVM模組知識)

1:強引用特點:我們平常編碼的Object obj = new Object()中的obj就是強引用。通過關鍵字new建立的物件所關聯的引用就是強引用。當JVM記憶體不足,JVM寧願丟擲OOM異常,使程式異常終止,也不會靠隨意回收具有強引用的“存活”物件來解決記憶體不足的問題。對於一個普通的物件,如果沒有其他的引用關係,只要超過了引用的作用域或者顯式地將相應(強)引用賦值為null,就是可以被垃圾收集的了,具體回收時機還是要看垃圾收集策略。2:軟引用特點:軟引用通過SoftReference類師兄。軟引用的生命週期比強引用短一些。只有當JVM認為記憶體不足時,才會去試圖回收軟引用指向的物件:即JVM會確保在丟擲OOM之前,清理軟引用指向的物件。軟引用可以和一個引用佇列(ReferenceQueue)聯合使用,如果軟引用所引用的物件被垃圾回收器回收,Java虛擬機器就會把這個軟引用加入到與之關聯的引用佇列中。後續,我們可以呼叫ReferenceQueue的poll()方法來檢查是否有它所關心的物件被回收。如果佇列為空,將返回一個null,否則該方法返回佇列中前面的一個Reference物件。場景:軟引用通常用來實現記憶體敏感的快取。如果還有空閒記憶體,就可以暫時保留快取,當記憶體不足時清理掉。這樣就保證了使用快取的同時,不會耗盡記憶體。3:弱引用特點:弱引用通過WeakReference類師兄。弱引用的生命週期比軟引用短。在垃圾回收器執行緒掃描它所管轄的記憶體區域的過程中,一旦發現了具有弱引用的物件,不管當前記憶體空間是否足夠,都會回收它的記憶體,由於垃圾回收器是一個優先順序很低的執行緒,因此不一定會很快回收弱引用的物件。弱引用可以和一個引用佇列聯合使用,如果弱引用所引用的物件被垃圾回收,Java虛擬機器就會把這個弱引用加入到與之關聯的引用佇列中。場景:弱引用同樣可用於記憶體敏感的快取。4:虛引用特點:虛引用也叫做幻想引用,通過PhantomReference類來實現。無法通過虛引用訪問物件的任何屬性或函式。幻象引用僅僅是提供了一種確保物件被finalize以後,做某些事情的機制。如果一個物件僅持有虛引用,那麼它就和沒有任何引用一樣,在任何時候都可能被垃圾回收器回收。虛引用必須和引用佇列聯合使用。當垃圾回收器準備回收一個物件時,如果發現它還有虛引用,就會在回收物件的記憶體之前,把這個虛引用加入到與之關聯的引用佇列中。程式可以通過判斷引用佇列中是否已經加入了虛引用。來了解唄引用的物件是否將要被垃圾回收。如果程式發現某個虛引用已經被加入到引用佇列,那麼就可以在所引用的物件的記憶體被回收之前採取一些程式行動。場景:可用來跟蹤物件被垃圾回收器回收的活動,當一個虛引用關聯的物件被垃圾收集器回收之前會收到一條系統通知。

49:JVM中的方法區會被進行GC機制的處理嗎?(JVM模組知識)

     JVM規範中確實明確說過可以不要求JVM在方法區實現GC,而且在方法區進行GC的“價效比”一般比較低,相對於在堆記憶體中,尤其是在新生代中,常規應用進行一次GC一般可以回收70% ~ 95%的空間,而方法區(HotSpot中又稱為永久代)的GC效率遠低於此;但是,此部分記憶體區域也是可以被回收的。方法區的垃圾回收主要有兩種,分別是對廢棄常量的回收和對無用類的回收。      當一個常量物件不再任何地方被引用的時