Java面试手册——高频问题总结(二)
這里將Java集合和垃圾回收的知識總結,放到(二)中。對Java平臺的理解、Java基礎知識、面向對象請參考Java面試手冊——高頻問題總結(一)
Java高頻問題面試:
| 1 | Java面試手冊——高頻問題總結(一) | 
| 2 | Java面試手冊——高頻問題總結(二) | 
| 3 | Java基礎面試突擊 | 
| 4 | Java虛擬機——JVM總結 | 
| 5 | JVM面試突擊 | 
文章目錄
- 四、Java集合
- 1. Java集合框架的基礎接口有哪些?
- 2. Collection 和 Collections 有什么區別?
- 3. List、Set、Map是否繼承自Collection接口?
- 4. HashMap 和 HashTable 的區別?
- 5. ArrayList、Vector 和 LinkedList 的區別是什么?ArrayList 是否會越界?
- 6. Array 和 ArrayList 區別?
- 7. HashMap 和 ConcurrentHashMap 的區別?
- 8. HashMap的內部具體如何實現(高頻問題)?
- 9. HashMap 底層,負載因子,為啥是2^n?
- 10. ConcurrentHashMap的原理是什么?ConcurrentHashMap 鎖加在了什么地方?
- 11. TreeMap 底層,紅黑樹原理?
- 12. 什么是迭代器?Iterator 和 ListIterator 的區別是什么?
- 13. HashSet 的實現原理?
- 14. 快速失敗(fail-fast)和安全失敗(fail-safe)的區別是什么?
- 15. HashMap操作注意事項以及優化?
 
- 五、 JVM 和 垃圾回收 GC
- 1. JVM、JDK和JRE的關系
- 2. JVM回收算法和回收器,CMS采用哪種回收算法,怎么解決內存碎片問題?
- 3. 類加載過程
- 4. JVM分區,JVM內存結構
- 5. Java虛擬機的作用?
- 6. GC如何判斷對象需要被回收?
- 7. JVM內存模型是什么?
- 8. JVM是如何實現線程的?
- 9. JVM最大內存限制是多少?
- 10. 描述一下JVM加載class文件的原理機制?
- 11. 除了哪個區域外,虛擬機內存其它運行區域都會發生OutOfMemoryError? 什么情況下會出現堆內存溢出?
- 12. Java中內存泄漏是什么?什么時候會出現內存泄漏?
- 13. 垃圾回收器的原理是什么?
 
- 六、Java 多線程
- 1. 什么是進程?什么是線程?
- 2. 線程的實現方式?創建線程的方法?
- 3. Thread類中的 start() 和 run() 方法有什么區別?啟動一個線程是用哪個方法?
- 4. 解釋下線程的幾種可用狀態?
- 5. 多線程同步的方法?
- 6. 如何在兩個線程間共享數據?
- 7. 如何線程安全的實現一個計數器?
- 8. 線程池的種類?運行流程,參數,策略?線程池有什么好處?
- 9. 講一下AQS?
- 10. 如何理解Java多線程回調的方法?
- 11. 同步方法和同步代碼塊的區別?
- 12. sleep() 和 wait() 有什么區別?
- 13. 在監視器(Monitor)內部,如何做到線程同步的?程序應該做哪種級別的同步?
- 14. 同步和異步有何異同,在什么情況下分別使用他們?舉例說明。
- 15. 說出所知道的線程同步的方法?
- 16. 線程的sleep() 和yield() 方法有什么區別?
- 17. 如何保證線程安全?
- 18. CyclicBarrier 和 CountDownLatch的區別?
- 19. 講一下公平鎖和非公平鎖在reetranklock里的實現。
- 20. 講一下Synchronized,可重入是怎么實現的?
- 21. 鎖和同步的區別。
- 22. 什么是死鎖?如何確保N個線程可以訪問N個資源同時又不導致死鎖?
- 23. 簡述Synchronized 和 java.util.cncurrent.locks.Lock的異同?
 
- 七、IO 和 NIO
- 1. 什么是IO流?
- 2. Java中有幾種類型的流?
- 3. 字節流和字符流哪個好?怎么選擇?
- 4. IO模型有幾種?分別介紹一下。
- 5. NIO和IO的區別?以及使用場景?
- 6. NIO的核心組件是什么,分別介紹一下?
 
 
 
四、Java集合
1. Java集合框架的基礎接口有哪些?
Collection為集合層級的根接口。一個集合代表一組對象,這些對象即為它的元素。Java平臺不提供這個接口任何直接實現。
Set是一個不能包含重復元素的集合。
List是一個有序集合,可以包含重復元素??梢酝ㄟ^它的索引來訪問任何元素。List更像長度動態變換的數組。
Map是一個將key映射到value的對象。一個Map不能包含重復的key:每個key最多只能映射一個value。
一些其它的接口有Queue、Dequeue、SortedSet、SortedMap 和 ListIteator。
2. Collection 和 Collections 有什么區別?
Collection 是一個集合接口,它提供了對集合對象進行基本操作的通用接口方法,所有集合都是它的子類,比如List、Set 等。
Collections 是一個包裝類,包含了很多靜態方法,不能被實例化,就像一個工具類,比如提供排序方法:Collections.sort(list)。
3. List、Set、Map是否繼承自Collection接口?
List,Set是,Map不是。Map是鍵值對映射容器,與List和Set有明顯的區別,而Set存儲的零散的元素且不允許有重復的元素,List是線性結構的容器,適用于按數值索引訪問元素的情況。
4. HashMap 和 HashTable 的區別?
HashMap 和 HashTable 都實現了 Map 接口,很多特性相似。不同點如下:
注:HashTable同步,HashMap異步:同步指多線程安全,異步指多線程不安全。在Hashtable中,所有的public方法都加上了synchronized,所以說Hashtable是同步的,而HashMap沒有,所以HashMap是異步的。
詳細請參考:對比HashTable、HashMap、TreeMap有什么不同?
5. ArrayList、Vector 和 LinkedList 的區別是什么?ArrayList 是否會越界?
ArrayList 和 LinkedList的區別:
綜合來說,在需要頻繁讀取集合中的元素時,更推薦使用ArrayList,而在插入和刪除操作較多時,更推薦使用LinkedList。
ArrayList 和 Vector 的異同:
ArrayList和Vector在很多時候都很類似。
以下是ArrayList和Vector的不同點。
詳細請參考:對比Vector、ArrayList、LinkedList有何區別?
ArrayList 并發 add() 可能出現數組下標越界異常。
6. Array 和 ArrayList 區別?
7. HashMap 和 ConcurrentHashMap 的區別?
HashMap是線程不安全的,put時在多線程情況下,會形成環從而導致死循環。CoucurrentHashMap 是線程安全的,采用分段鎖機制,減少鎖的粒度。
8. HashMap的內部具體如何實現(高頻問題)?
從結構上來講,HashMap 是數組+鏈表+紅黑樹 (JDK1.8增加了紅黑樹部分)實現的,如下圖所示:
 兩個問題:數據底層具體存儲的是什么?這樣的存儲方式有什么優點?
(1)HashMap類中有一個非常重要的字段就是Node[] table, 即哈希桶數組,明顯它是一個Node數組??匆幌翹ode[JDK1.8]:
 Node是HashMap的一個內部類,實現了Map.Entry接口,本質上就是一個映射(鍵值對)。上圖中每個黑色圓點就是一個Node對象。
(2)HashMap就是使用哈希表來存儲的。哈希表為解決哈希沖突,Java中HashMap采用鏈地址法。
如果哈希桶數組很大,即使較差的Hash算法也會比較分散,如果哈希桶數組數組很小,即使好的Hash算法也會出現較多碰撞,所以就需要在空間成本和時間成本之間權衡,其實就是在根據實際情況確定哈希桶數組的大小,并在此基礎上設計好的hash算法減少Hash碰撞。那么通過什么方式來控制map使得Hash碰撞的概率又小,哈希桶數組(Node] table)占用空間又少呢?答案就是好的Hash算法和擴容機制。
9. HashMap 底層,負載因子,為啥是2^n?
負載因子默認是0.75,2^n 是為了讓散列更加均勻,例如出現極端情況都散列在數組中的一個下標,那么HashMap會由 O(1) 復雜度退化為 O(n) 的。
10. ConcurrentHashMap的原理是什么?ConcurrentHashMap 鎖加在了什么地方?
(1)ConcurrentHashMap的原理是什么?
 ConcurrentHashMap類中包含兩個靜態內部類HashEntry和Segment。
HashEntry 用來封裝映射表的鍵/值對;Segment用來充當鎖的角色,每個Segment 對象守護整個散列映射表的若干個桶。每個桶是由若干個HashEntry對象鏈接起來的鏈表。一個ConcurrentHashMap 實例中包含由若干個Segment對象組成的數組。HashEntry 用來封裝散列映射表中的鍵值對。在HashEntry類中, key,hash 和 next域都被聲明為final 型,value域被聲明為 volatile型。
在ConcurrentHashMap中,在散列時如果產生“碰撞”,將采用“分離鏈接法”來處理“碰撞”:把“碰撞”的 HashEntry對象鏈接成一個鏈表。由于HashEntry的next域為final 型,所以新節點只能在鏈表的表頭處插入。下圖是在一個空桶中依次插入A,B,C三個HashEntry對象后的結構圖:
插入三個節點后桶的結構示意圖:
 
 注意:由于只能在表頭插入,所以鏈表中節點的順序和插入的順序相反。
Segment類繼承于ReentrantLock 類,從而使得Segment對象能充當鎖的角色。每個Segment 對象用來守護其(成員對象 table 中)包含的若干個桶。
(2)ConcurrentHashMap 鎖加在了什么地方?
加在每個 Segment 上面。
(3)ConcurrentHashMap有什么優勢,1.7,1.8區別?
Concurrenthashmap線程安全的,1.7是在 jdk1.7中采用Segment + HashEntry的方式進行實現的,lock 加在Segment上面。1.7size計算是先采用不加鎖的方式,連續計算元素的個數,最多計算3次:
1.8 中放棄了Segment臃腫的設計,取而代之的是采用Node + CAS+ Synchronized來保證并發安全進行實現,1.8中使用一個volatile類型的變量baseCount記錄元素的個數,當插入新數據或則刪除數據時,會通過addCount()方法更新baseCount,通過累加baseCount和CounterCell數組中的數量,即可得到元素的總個數。
11. TreeMap 底層,紅黑樹原理?
TreeMap 的實現就是紅黑樹數據結構,也就說是一棵自平衡的排序二叉樹,這樣就可以保證當需要快速檢索指定節點。
紅黑樹的插入、刪除、遍歷時間復雜度都為0(log N),所以性能上低于哈希表。但是哈希表無法提供鍵值對的有序輸出,紅黑樹因為是排序插入的,可以按照鍵的值的大小有序輸出。
紅黑樹性質:
12. 什么是迭代器?Iterator 和 ListIterator 的區別是什么?
什么是迭代器?
Iterator 提供了統一遍歷操作集合元素的統一接口, Collection接口實現Iterable接口。
每個集合都通過實現Iterable接口中iterator()方法返回Iterator接口的實例,然后對集合的元素進行迭代操作。
有一點需要注意的是:在迭代元素的時候不能通過集合的方法刪除元素,否則會拋出ConcurrentModificationException異常.但是可以通過Iterator接口中的remove()方法進行刪除。
lterator和 ListIterator的區別是:
13. HashSet 的實現原理?
HashSet 是基于HashMap實現的,HashSet的底層使用HashMap來保存所有元素,因為HashSet的實現比較簡單,相關HashSet的操作,基本上都是直接調用底層HashMap的相關方法來完成的,HashSet不允許有重復的值。
14. 快速失敗(fail-fast)和安全失敗(fail-safe)的區別是什么?
Iterator的安全失敗是基于對底層集合做拷貝,因此,它不受源集合上修改的影響。
java.util包下面的所有的集合類都是快速失敗的,而java.util.concurrent包下面的所有的類都是安全失敗的。
快速失敗的迭代器會拋出ConcurrentModificationException異常,而安全失敗的迭代器永遠不會拋出這樣的異常。
15. HashMap操作注意事項以及優化?
五、 JVM 和 垃圾回收 GC
1. JVM、JDK和JRE的關系
見Java面試手冊——高頻問題總結(一)中JVM、JDK和JRE的關系。
2. JVM回收算法和回收器,CMS采用哪種回收算法,怎么解決內存碎片問題?
見Java虛擬機——JVM總結中垃圾回收算法。
3. 類加載過程
見Java虛擬機——JVM總結中類加載過程。
4. JVM分區,JVM內存結構
見Java虛擬機——JVM總結中JVM分區,JVM內存結構。
5. Java虛擬機的作用?
解釋運行字節碼程序,消除平臺相關性。
JVM將Java字節碼解釋為具體平臺的具體指令。一般的高級語言如要在不同的平臺上運行,至少需要編譯成不同的目標代碼。而引入JVM后,Java語言在不同平臺上運行時不需要重新編譯。Java語言使用模式Java虛擬機屏蔽了與具體平臺相關的信息,使得Java語言編譯程序只需生成在Java虛擬機上運行的目標代碼(字節碼),就可以在多種平臺上不加修改地運行。Java虛擬機在執行字節碼時,把字節碼解釋成具體平臺上的機器指令執行。
假設一個場景,要求stop the world時間非常短,你會怎么設計垃圾回收機制?
絕大多數新創建的對象分配在Eden 區。
在Eden區發生一次GC后,存活的對象移到其中一個Survivor 區。
在Eden區發生一次GC后,對象是存放到Survivor區,這個Survivor區已經存在其他存活的對象。
一旦一個Survivor區已滿,存活的對象移動到另外一個Survivor區。然后之前那個空間已滿Survivor區將置為空,沒有任何數據。
經過重復多次這樣的步驟后依舊存活的對象將被移到老年代。
6. GC如何判斷對象需要被回收?
即使在可達性分析算法中不可達的對象,也并非是“非回收不可”的, 這時候它們暫時處于“等待”階段,要真正宣告一個對象回收,至少要經歷兩次標記過程:如果對象在進行可達性分析后發現沒有與GC Roots相連接的引用鏈,那它將會被第一次標記并且進行一次篩選,篩選的條件是此對象是否有必要執行finalize()方法。當對象沒有覆蓋finalize()方法,或者finalize()方法已經被虛擬機調用過,虛擬機將這兩種情況都視為“沒有必要執行”。(即意味著直接回收)
如果這個對象被判定為有必要執行finalize()方法,那么這個對象將會放置在一個叫做F-Queue 的隊列之中,并在稍后由一個由虛擬機自動建立的、低優先級的Finalizer線程去執行它。這里所謂的“執行”是指虛擬機會觸發這個方法,但并不承諾會等待它運行結束,這樣做的原因是,如果一個對象在finalize()方法中執行緩慢,或者發生了死循環(更極端的情況),將很可能會導致F-Queue 隊列中其他對象永久處于等待,甚至導致整個內存回收系統崩潰。
finalize()方法是對象逃脫回收的最后一次機會,稍后GC將對F-Queue中的對象進行第二次小規模的標記,如果對象要在finalize()中跳出回收一一只要重新與引用鏈上的任何一個對象建立關聯即可,譬如把自己(this關鍵字)賦值給某個類變量或者對象的成員變量,那在第二次標記時它將被移除出“即將回收”的集合;如果對象這時候還沒有逃脫,那基本上它就真的被回收了。
7. JVM內存模型是什么?
Java內存模型(簡稱JMM),JMM決定一個線程對共享變量的寫入何時對另一個線程可見。從抽象的角度來看,JMM定義了線程和主內存之間的抽象關系:線程之間的共享變量存儲在主內存(main memory)中,每個線程都有一個私有的本地內存(local memory),本地內存中存儲了該線程以讀/寫共享變量的副本。
本地內存是JMM的一個抽象概念,并不真實存在。它涵蓋了緩存,寫緩沖區,寄存器以及其他的硬件和編譯器優化。其關系模型圖如下圖所示:
8. JVM是如何實現線程的?
線程是比進程更輕量級的調度執行單位。線程可以把一個進程的資源分配和執行調度分開。一個進程里可以啟動多條線程,各個線程可共享該進程的資源(內存地址,文件I0等),又可以獨立調度。線程是CPU調度的基本單位。
主流OS都提供線程實現。Java語言提供對線程操作的同一API,每個已經執行start(),且還未結束的java.lang.Thread類的實例,代表了一個線程。
Thread類的關鍵方法,都聲明為Native。這意味著這個方法無法或沒有使用平臺無關的手段來實現,也可能是為了執行效率。
實現線程的方式:
使用內核線程實現,內核線程(Kernel-Level Thread, KLT)就是直接由操作系統內核支持的線程。
9. JVM最大內存限制是多少?
(1) 堆內存分配
JVM初始分配的內存由-Xms指定,默認是物理內存的1/64;JVM最大分配的內存由-Xmx指定,默認是物理內存的1/4。默認空余堆內存小于40%時,JVM就會增大堆直到-Xmx的最大限制;空余堆內存大于70%時,JVM會減少堆直到-Xms的最小限制。因此服務器一般設置-Xms、一Xmx相等以避免在每次GC后調整堆的大小。
(2) 非堆內存分配
JVM使用-XX:PermSize 設置非堆內存初始值,默認是物理內存的1/64;由XX:MaxPermSize設置最大非堆內存的大小,默認是物理內存的1/4。
(3) VM最大內存
首先JVM內存限制于實際的最大物理內存,假設物理內存無限大的話,JVM內存的最大值跟操作系統有很大的關系。簡單的說就32位處理器雖然可控內存空間有4GB,但是具體的操作系統會給一個限制,這個限制一般是2GB-3GB(一般來說Windows系統下為1.5G-2G,Linux系統下為2G-3G),而64bit 以上的處理器就不會有限制了。
10. 描述一下JVM加載class文件的原理機制?
JVM中類的裝載是由ClassLoader和它的子類來實現的,Java ClassLoader是一個重要的Java運行時系統組件。它負責在運行時查找和裝入類文件的類。
Java中的所有類,都需要由類加載器裝載到JVM中才能運行。類加載器本身也是一個類,而它的工作就是把class文件從硬盤讀取到內存中。在寫程序的時候,我們幾乎不需要關心類的加載,因為這些都是隱式裝載的,除非我們有特殊的用法,像是反射,就需要顯式的加載所需要的類。
11. 除了哪個區域外,虛擬機內存其它運行區域都會發生OutOfMemoryError? 什么情況下會出現堆內存溢出?
程序計數器。
堆內存存儲對象的實例。我們只要不斷的創建對象,并保證gc roots到對象之間有可達路徑來避免垃圾回收機制清除這些對象,就會在對象數量達到最大,堆容量限制后,產生內存溢出異常。
空間什么情況下會拋出OutOfMemoryError?
如果虛擬機在擴展棧時無法申請到足夠的內存空間,則拋出OutOfMemoryError。
12. Java中內存泄漏是什么?什么時候會出現內存泄漏?
Java中的內存泄漏,廣義并通俗的說:不再會被使用的對象的內存不能被回收,就是內存泄漏。
如果長生命周期的對象持有短生命周期的引用,就可能出現內存泄漏。
13. 垃圾回收器的原理是什么?
對于GC來說,當程序員創建對象時,GC就開始監控這個對象的地址、大小以及使用情況。通常,GC采用有向圖的方式記錄和管理堆(heap)中的所有對象。通過這種方式確定哪些對象是”可達的”,哪些對象是”不可達的”。當GC確定一些對象為”不可達”時,GC就有責任回收這些內存空間。可以。程序員可以手動執行System.gc(),通知GC運行,但是Java 語言規范并不保證GC一定會執行。
六、Java 多線程
1. 什么是進程?什么是線程?
進程是系統中正在運行的一個程序,程序一旦運行就是進程。
進程可以看成程序執行的一個實例。進程是系統資源分配的獨立實體,每個進程都擁有獨立的地址空間。一個進程無法訪問另一個進程的變量和數據結構,如果想讓一個進程訪問另一個進程的資源,需要使用進程間通信,比如管道,文件,套接字等。
線程是操作系統能夠進行運算調度的最小單位。它被包含在進程之中,是進程中的實際運作單位。一條線程指的是進程中一個單一順序的控制流,一個進程中可以并發多個線程,每條線程并行執行不同的任務。
2. 線程的實現方式?創建線程的方法?
創建線程有如下三種方式:
一、繼承Thread類創建線程類
- (1)定義Thread類的子類,并重寫該類的run方法,該run方法的方法體就代表了線程要完成的任務。因此把run()方法稱為執行體。
- (2)創建Thread子類的實例,即創建了線程對象。
- (3)調用線程對象的start()方法來啟動該線程。
二、通過Runnable接口創建線程類
- (1)定義runnable接口的實現類,并重寫該接口的run()方法,該run()方法的方法體同樣是該線程的線程執行體。
- (2)創建Runnable實現類的實例,并依此實例作為Thread的target來創建Thread對象,該Thread對象才是真正的線程對象。
- (3)調用線程對象的start()方法來啟動該線程。
三、通過Callable和 Future創建線程
-  (1)創建Callable接口的實現類,并實現call()方法,該call()方法將作為線程執行體,并且有返回值。 
-  (2)創建Callable實現類的實例,使用FutureTask類來包裝Callable對象,該FutureTask對象封裝了該Callable對象的call()方法的返回值。 
-  (3)使用FutureTask對象作為Thread對象的target創建并啟動新線程。(4)調用FutureTask對象的get()方法來獲得子線程執行結束后的返回值。 
3. Thread類中的 start() 和 run() 方法有什么區別?啟動一個線程是用哪個方法?
start() 和 run() 方法的區別:
啟動一個線程是調用 start()方法,使線程所代表的虛擬處理機處于可運行狀態,這意味著它可以由JVM調度并執行。這并不意味著線程就會立即運行。run() 方法可以產生必須退出的標志來停止一個線程。
4. 解釋下線程的幾種可用狀態?
1.新建( new ) : 新創建了一個線程對象。
2.可運行( runnable ) : 線程對象創建后,其他線程(比如 main線程)調用了該對象的start()方法。該狀態的線程位于可運行線程池中,等待被線程調度選中,獲取cpu的使用權。
3.運行( running ) : 可運行狀態( runnable )的線程獲得了cpu時間片( timeslice ) ,執行程序代碼。
4.阻塞( block ) : 阻塞狀態是指線程因為某種原因放棄了cpu使用權,也即讓出了cpu timeslice ,暫時停止運行。直到線程進入可運行( runnable )狀態,才有機會再次獲得cputimeslice轉到運行( running )狀態。阻塞的情況分三種:
- (一).等待阻塞:運行( running )的線程執行o . wait()方法,JVM 會把該線程放入等待隊列( waitting queue )中。
- (二).同步阻塞:運行(running )的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則JVM會把該線程放入鎖池( lock pool )中。
- (三).其他阻塞:運行( running )的線程執行Thread . sleep ( long ms )或t . join()方法,或者發出了I/0請求時,JVM會把該線程置為阻塞狀態。當sleep()狀態超時、join()等待線程終止或者超時、或者Ⅰ/0處理完畢時,線程重新轉入可運行(runnable)狀態。
5.死亡( dead ) : 線程run ()、 main()方法執行結束,或者因異常退出了run()方法,則該線程結束生命周期。死亡的線程不可再次復生。
5. 多線程同步的方法?
可以使用synchronized、lock、volatile和ThreadLocal來實現同步。
6. 如何在兩個線程間共享數據?
如果一個類繼承Thread,則不適合資源共享。但是如果實現了Runable接口的話,則很容易的實現資源共享。實現Runnable接口或callable接口,適合多個相同或不同的程序代碼的線程去共享同一個資源。
多個線程共享數據分兩種情況:
7. 如何線程安全的實現一個計數器?
可以使用加鎖, 比如 synchronized 或者 lock。 也可以使用 Concurrent 包下的原子類。
8. 線程池的種類?運行流程,參數,策略?線程池有什么好處?
線程池的種類:
1、newFixedThreadPool 創建一個指定工作線程數量的線程池。每當提交一個任務就創建一個工作線程,如果工作線程數量達到線程池初始的最大數,則將提交的任務存入到池隊列中。
2、newCachedThreadPool 創建一個可緩存的線程池。這種類型的線程池特點是:
- 工作線程的創建數量幾乎沒有限制(其實也有限制的,數目為Interger. MAX_VALUE), 這樣可靈活的往線程池中添加線程。
- 如果長時間沒有往線程池中提交任務,即如果工作線程空閑了指定的時間(默認為1分鐘),則該工作線程將自動終止。終止后,如果你又提交了新的任務,則線程池重新創建一個工作線程。
3、newSingleThreadExecutor 創建一個單線程化的Executor,即只創建唯一的工作者線程來執行任務,如果這個線程異常結束,會有另一個取代它,保證順序執行(我覺得這點是它的特色)。單工作線程最大的特點是可保證順序地執行各個任務,并且在任意給定的時間不會有多個線程是活動的。
4、newScheduleThreadPool 創建一個定長的線程池,而且支持定時的以及周期性的任務執行,類似于Timer。(這種線程池原理暫還沒完全了解透徹)
線程池的運行流程,參數,策略:
線程池主要就是指定線程池核心線程數大小,最大線程數,存儲的隊列,拒絕策略,空閑線程存活時長。當需要任務大于核心線程數時候,就開始把任務往存儲任務的隊列里,當存儲隊列滿了的話,就開始增加線程池創建的線程數量,如果當線程數量也達到了最大,就開始執行拒絕策略,比如說記錄日志,直接丟棄,或者丟棄最老的任務。
線程池的好處:
第一:降低資源消耗。通過重復利用己創建的線程降低線程創建和銷毀造成的消耗。
第二:提高響應速度。當任務到達時,任務可以不需要等到線程創建就能執行。
第三:提高線程的可管理性,線程是稀缺資源,如果無限制地創建,不僅會消耗系統資源,還會降低系統的穩定性,使用線程池可以進行統一分配、調優和監控。
9. 講一下AQS?
AQS其實就是一個可以給我們實現鎖的框架。
內部實現的關鍵是:先進先出的隊列、state狀態定義了內部類ConditionObject。
擁有兩種線程模式獨占模式和共享模式。
在LOCK包中的相關鎖(常用的有ReentrantLock、 ReadWriteLock)都是基于AQS來構建,一般我們叫 AQS為同步器。
10. 如何理解Java多線程回調的方法?
所謂回調,就是客戶程序C調用服務程序S中的某個方法A,然后S又在某個時候反過來調用C中的某個方法B,對于C來說,這個B便叫做回調方法。
11. 同步方法和同步代碼塊的區別?
區別:
同步方法默認用 this 或者當前類 class 對象作為鎖;
同步代碼塊可以選擇以什么來加鎖,比同步方法要更細粒度,我們可以選擇只同步會發生同步的問題的部分代碼而不是整個方法。
12. sleep() 和 wait() 有什么區別?
sleep 是線程類(Thread )的方法,導致此線程暫停執行指定時間,把執行機會給其他線程,但是監控狀態依然保持,到時后會自動恢復。調用sleep不會釋放對象鎖。
wait 是Object類的方法,對此對象調用wait方法導致本線程放棄對象鎖,進入等待此對象的等待鎖定池,只有針對此對象發出 notify 方法(或notifyAll)后本線程才進入對象鎖定池準備獲得對象鎖進入運行狀態。
sleep用Thread調用,在非同步狀態下就可以調用, wait用同步監視器調用,必須在同名代碼中調用。
13. 在監視器(Monitor)內部,如何做到線程同步的?程序應該做哪種級別的同步?
監視器和鎖在Java 虛擬機中是一塊使用的。監視器監視一塊同步代碼塊,確保一次只有一個線程執行同步代碼塊。每一個監視器都和一個對象引用相關聯。線程在獲取鎖之前不允許執行同步代碼。
14. 同步和異步有何異同,在什么情況下分別使用他們?舉例說明。
如果數據將在線程間共享。例如正在寫的數據以后可能被另一個線程讀到,或者正在讀的數據可能已經被另一個線程寫過了,那么這些數據就是共享數據,必須進行同步存取。
當應用程序在對象上調用了一個需要花費很長時間來執行的方法,并且不希望讓程序等待方法的返回時,就應該使用異步編程,在很多情況下采用異步途徑往往更有效率。
15. 說出所知道的線程同步的方法?
wait()∶使一個線程處于等待狀態,并且釋放所持有的對象的lock。
sleep(): 使一個正在運行的線程處于睡眠狀態,是一個靜態方法,調用此方法要捕捉InterruptedException異常。
notify(): 喚醒一個處于等待狀態的線程,注意的是在調用此方法的時候,并不能確切的喚醒某一個等待狀態的線程,而是由JVM確定喚醒哪個線程,而且不是按優先級。
Allnotity(): 喚醒所有處入等待狀態的線程,注意并不是給所有喚醒線程一個對象的鎖,而是讓它們競爭。
16. 線程的sleep() 和yield() 方法有什么區別?
17. 如何保證線程安全?
通過合理的時間調度,避開共享資源的存取沖突。另外,在并行任務設計上可以通過適當的策略,保證任務與任務之間不存在共享資源,設計一個規則來保證一個客戶的計算工作和數據訪問只會被一個線程或一臺工作機完成,而不是把一個客戶的計算工作分配給多個線程去完成。
18. CyclicBarrier 和 CountDownLatch的區別?
CountDownLatch:
計數器: 計數器只能使用一次。
等待: 一個線程或多個等待另外n個線程完成之后才能執行。
CyclicBarrier:
計數器: 計數器可以重置(通過reset()方法)。
等待: n個線程相互等待,任何一個線程完成之前,所有的線程都必須等待。
19. 講一下公平鎖和非公平鎖在reetranklock里的實現。
如果一個鎖是公平的,那么鎖的獲取順序就應該符合請求的絕對時間順序,FIFO。
對于非公平鎖,只要CAS設置同步狀態成功,則表示當前線程獲取了鎖,而公平鎖還需要判斷當前節點是否有前驅節點,如果有,則表示有線程比當前線程更早請求獲取鎖,因此需要等待前驅線程獲取并釋放鎖之后才能繼續獲取鎖。
20. 講一下Synchronized,可重入是怎么實現的?
每個鎖關聯一個線程持有者和一個計數器。
當計數器為0時表示該鎖沒有被任何線程持有,那么任何線程都都可能獲得該鎖而調用相應方法。
當一個線程請求成功后,JVM會記下持有鎖的線程,并將計數器計為1。此時其他線程請求該鎖,則必須等待。而該持有鎖的線程如果再次請求這個鎖,就可以再次拿到這個鎖,同時計數器會遞增。
當線程退出一個synchronized方法/塊時,計數器會遞減,如果計數器為0, 則釋放該鎖。
21. 鎖和同步的區別。
(1)用法上的不同:
synchronized 既可以加在方法上,也可以加載特定代碼塊上,而lock需要顯示地指定起始位置和終止位置。
synchronized是托管給JVM執行的,lock的鎖定是通過代碼實現的,它有比synchronized更精確的線程語義。
(2)性能上的不同:
 lock接口的實現類ReentrantLock,不僅具有和synchronized相同的并發性和內存語義,還多了超時的獲取鎖、定時鎖、等候和中斷鎖等。
在競爭不是很激烈的情況下,synchronized的性能優于ReentrantLock,競爭激烈的情況下synchronized 的性能會下降的非常快,而ReentrantLock 則基本不變。
(3)鎖機制不同:
synchronized獲取鎖和釋放鎖的方式都是在塊結構中,當獲取多個鎖時,必須以相反的順序釋放,并且是自動解鎖。而Lock則需要開發人員手動釋放,并且必須在finally中釋放,否則會引起死鎖。
22. 什么是死鎖?如何確保N個線程可以訪問N個資源同時又不導致死鎖?
什么是死鎖?
兩個線程或兩個以上線程都在等待對方執行完畢才能繼續往下執行的時候就發生了死鎖。結果就是這些線程都陷入了無限的等待中。
例如,如果線程1鎖住了A,然后嘗試對B進行加鎖,同時線程2已經鎖住了B,接著嘗試對A進行加鎖,這時死鎖就發生了。線程1永遠得不到B,線程2也永遠得不到A,并且它們永遠也不會知道發生了這樣的事情。為了得到彼此的對象(A和B),它們將永遠阻塞下去。這種情況就是一個死鎖。
如何確保N個線程可以訪問N個資源同時又不導致死鎖?
使用多線程的時候,一種非常簡單的避免死鎖的方式就是: 指定獲取鎖的順序,并強制線程按照指定的順序獲取鎖。因此,如果所有的線程都是以同樣的順序加鎖和釋放鎖,就不會出現死鎖了。
預防死鎖,預先破壞產生死鎖的四個條件。互斥不可能破壞,所以有如下三種方法:
23. 簡述Synchronized 和 java.util.cncurrent.locks.Lock的異同?
主要相同點: Lock 能完成synchronized 所實現的所有功能。
主要不同點: Lock有比 synchronized更精確的線程語義和更好的性能。synchronized會自動釋放鎖,而Lock一定要求程序員手工釋放,并且必須在finally 從句中釋放。
七、IO 和 NIO
1. 什么是IO流?
它是一種數據的流從源頭流到目的地。比如文件拷貝,輸入流和輸出流都包括了。輸入流從文件中讀取數據存儲到進程(process)中,輸出流從進程中讀取數據然后寫入到目標文件。
2. Java中有幾種類型的流?
按照單位大小:字符流、字節流。
 按照流的方向:輸入流、輸出流。
3. 字節流和字符流哪個好?怎么選擇?
字節流是, 選擇BufferedInputStream 和 BufferedOutputStream。
 字符流是,選擇BufferedReader 和 BufferedWriter。
4. IO模型有幾種?分別介紹一下。
阻塞IO、非阻塞IO、多路復用IO、信號驅動IO以及異步IO。
阻塞IO(blocking IO)
應用程序調用一個IO函數,導致應用程序阻塞,如果數據已經準備好,從內核拷貝到用戶空間,否則一直等待下去。
一個典型的讀操作流程大致如下圖,當用戶進程調用recvfrom這個系統調用時,kernel就開始了IO的第一個階段:準備數據,就是數據被拷貝到內核緩沖區中的一個過程〈很多網絡IO數據不會那么快到達,如沒收一個完整的UDP包),等數據到操作系統內核緩沖區了,就到了第二階段:將數據從內核緩沖區拷貝到用戶內存,然后kernel返回結果,用戶進程才會解除block狀態,重新運行起來。
blocking IO的特點就是在IO執行的兩個階段用戶進程都會block住。
 非阻塞IO(nonblocking IO)
非阻塞I/О模型,我們把一個套接口設置為非阻塞就是告訴內核,當所請求的I/O操作無法完成時,不要將進程睡眠,而是返回一個錯誤。這樣我們的I/O操作函數將不斷的測試數據是否已經準備好,如果沒有準備好,繼續測試,直到數據準備好為止。在這個不斷測試的過程中,會大量的占用CPU的時間。
當用戶進程發出read操作時,如果kernel中數據還沒準備好,那么并不會block用戶進程,而是立即返回error,用戶進程判斷結果是error,就知道數據還沒準備好,用戶可以再次發read,直到kernel中數據準備好,并且用戶再一次發read操作,產生system call,那么kernel 馬上將數據拷貝到用戶內存,然后返回;
所以nonblocking IO的特點是用戶進程需要不斷的主動詢問kernel數據好了沒有。
阻塞IO一個線程只能處理一個IO流事件,要想同時處理多個IO流事件要么多線程要么多進程,這樣做效率顯然不會高,而非阻塞IO可以一個線程處理多個流事件,只要不停地詢所有流事件即可,當然這個方式也不好,當大多數流沒數據時,也是會大量浪費CPU資源;為了避免CPU空轉,引進代理(select和poll,兩種方式相差不大),代理可以觀察多個流I/O事件,空閑時會把當前線程阻塞掉,當有一個或多個I/O事件時,就從阻塞態醒過來,把所有IO流都輪詢一遍,于是沒有IO事件我們的程序就阻塞在select方法處,即便這樣依然存在問題,我們從select出只是知道有IO事件發生,卻不知道是哪幾個流,還是只能輪詢所有流,epoll這樣的代理就可以把哪個流發生怎樣的IO事件通知我們。
多路復用IO模型(IO multiplexing)
I/O多路復用就在于單個進程可以同時處理多個網絡連接IO,基本原理就是select,poll,epoll這些個函數會不斷輪詢所負責的所有socket,當某個socket有數據到達了,就通知用戶進程,這三個functon會阻塞進程,但和IO阻塞不同,這些函數可以同時阻塞多個IO操作,而且可以同時對多個讀操作,寫操作IO進行檢驗,直到有數據到達,才真正調用IO操作函數,調用過程如下圖;
所以IO多路復用的特點是通過一種機制一個進程能同時等待多個文件描述符,而這些文件描述符(套接字描述符)其中任意一個進入就緒狀態,select函數就可以返回。
IO多路復用的優勢在于并發數比較高的IO操作情況,可以同時處理多個連接,和bloking IO一樣socket是被阻塞的,只不過在多路復用中socket是被select阻塞,而在阻塞IO中是被socket IO給阻塞。
信號驅動IO模型
可以用信號,讓內核在描述符就緒時發送SIGIO信號通知我們,通過sigaction系統調用安裝一個信號處理函數。該系統調用將立即返回,我們的進程繼續工作,也就是說它沒有被阻塞。當數據報準備好讀取時,內核就為該進程產生一個SIGIO信號。我們隨后既可以在信號處理函數中調用recvfrom讀取數據報,并通知主循環數據已經準備好待處理。
特點:等待數據報到達期間進程不被阻塞。主循環可以繼續執行,只要等待來自信號處理函數的通知:既可以是數據已準備好被處理,也可以是數據報已準備好被讀取。
異步IO(asynchronous IO)
異步IO告知內核啟動某個操作,并讓內核在整個操作(包括將內核數據復制到我們自己的緩沖區)完成后通知我們,調用aio_read (Posix異步I/O函數以aio或lio開頭)函數,給內核傳遞描述字、緩沖區指針、緩沖區大小(與read相同的3個參數)、文件偏移以及通知的方式,然后系統立即返回。我們的進程不阻塞于等待I/O操作的完成。當內核將數據拷貝到緩沖區后,再通知應用程序。
用戶進程發起read操作之后,立刻就可以開始去做其它的事。而另一方面,從kernel的角度,當它受到一個asynchronous read之后,首先它會立刻返回,所以不會對用戶進程產生任何block。然后,kernel會等待數據準備完成,然后將數據拷貝到用戶內存,當這一切都完成之后,kernel會給用戶進程發送一個signal,告訴它read操作完成了。
5. NIO和IO的區別?以及使用場景?
NIO即New IO,這個庫是在JDK1.4中才引入的。NIO和IO有相同的作用和目的,但實現方式不同,NIO主要用到的是塊,所以NIO的效率要比IO高很多。在Java API中提供了兩套NIO,一套是針對標準輸入輸出NIO,另一套就是網絡編程NIO。
NIO是為彌補傳統IO的不足而誕生的,但是尺有所短寸有所長,**NIO也有缺點,因為NIO是面向緩沖區的操作,每一次的數據處理都是對緩沖區進行的,那么就會有一個問題,在數據處理之前必須要判斷緩沖區的數據是否完整或者已經讀取完畢,如果沒有,假設數據只讀取了一部分,那么對不完整的數據處理沒有任何意義。**所以每次數據處理之前都要檢測緩沖區數據。
那么NIO和IO各適用的場景是什么呢?
如果需要管理同時打開的成千上萬個連接,這些連接每次只是發送少量的數據,例如聊天服務器,這時候用NIO處理數據可能是個很好的選擇。
而如果只有少量的連接,而這些連接每次要發送大量的數據,這時候傳統的IO更合適。使用哪種處理數據,需要在數據的響應等待時間和檢查緩沖區數據的時間上作比較來權衡選擇。
6. NIO的核心組件是什么,分別介紹一下?
channel、buffer、selector
什么是Channel?
一個Channel(通道)代表和某一實體的連接,這個實體可以是文件、網絡套接字等。也就是說,通道是Java NIO提供的一座橋梁,用于我們的程序和操作系統底層I/O服務進行交互。
通道是一種很基本很抽象的描述,和不同的I/O服務交互,執行不同的I/O操作,實現不一樣,因此具體的有FileChannel、SocketChannel等。
通道使用起來跟Stream比較像,可以讀取數據到Buffer中,也可以把Buffer中的數據寫入通道。
 當然,也有區別,主要體現在如下兩點:
Java NIO 中最常用的通道實現?
Buffer是什么?
NIO中所使用的緩沖區不是一個簡單的byte數組,而是封裝過的Buffer類,通過它提供的API,我們可以靈活的操縱數據。
與Java基本類型相對應,NIO提供了多種Buffer類型,如ByteBuffer、CharBuffer、IntBuffer等,區別就是讀寫緩沖區時的單位長度不一樣(以對應類型的變量為單位進行讀寫)。
核心Buffer的實現有哪些?
核心的buffer實現有哪些:ByteBuffer、CharBuffer、DoubleBuffer、FloatBufferr、IntBuffer、LongBuffer、ShortBuffer,涵蓋了所有的基本數據類型(4類8種,除了Boolean)。也有其他的buffer如MappedByteBuffer。
Buffer讀寫數據基本操作:
在寫buffer的時候,buffer會跟蹤寫入了多少數據,需要讀buffer的時候,需要調用flip()來將buffer從寫模式切換成讀模式,讀模式中只能讀取寫入的數據,而非整個buffer。
當數據都讀完了,你需要清空buffer以供下次使用,可以有2種方法來操作:調用clear()或者調用compact()。
區別: clear方法清空整個buffer,compact方法只清除你已經讀取的數據,未讀取的數據會被移到buffer的開頭,此時寫入數據會從當前數據的末尾開始。
Selector是什么?
Selector(選擇器)是一個特殊的組件,用于采集各個通道的狀態(或者說事件)。我們先將通道注冊到選擇器,并設置好關心的事件,然后就可以通過調用select() 方法,靜靜地等待事件發生。
通道可以監聽哪幾個事件?
通道有4個事件可供監聽:
為什么要用Selector?
如果用阻塞I/О,需要多線程(浪費內存)﹐如果用非阻塞I/O,需要不斷重試(耗費CPU)。Selector的出現解決了這尷尬的問題,非阻塞模式下,通過Selector,我們的線程只為已就緒的通道工作,不用盲目的重試了。比如,當所有通道都沒有數據到達時,也就沒有Read事件發生,我們的線程會在select()方法處被掛起,從而讓出了CPU資源。
Selector處理多Channel
要使用一個Selector,要注冊這個Selector的Channels。然后調用Selector的select()方法。這個方法會阻塞,直到它注冊的Channels當中有一個準備好了的事件發生。 當select() 方法返回的時候,線程可以處理這些事件,如新的連接的到來,數據收到了等。
總結
以上是生活随笔為你收集整理的Java面试手册——高频问题总结(二)的全部內容,希望文章能夠幫你解決所遇到的問題。
 
                            
                        - 上一篇: vs工具
- 下一篇: 透析BAT人工智能生态图谱:AI大战一触
