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