Java面試題總結(下)

六、極客時間

1秧均、Exception和Error有什么區(qū)別?

Error類和Exception類的父類都是throwable類,他們的區(qū)別是:

Error類一般是指與JVM虛擬機相關的問題簸淀,如系統(tǒng)崩潰,虛擬機錯誤毒返,內(nèi)存空間不足错洁,方法調(diào)用棧溢等法瑟。對于這類錯誤的導致的應用程序中斷,僅靠程序本身無法恢復和和預防,遇到這樣的錯誤盛撑,建議讓程序終止噪猾。

Exception類表示程序可以處理的異常,可以捕獲且可能恢復。遇到這類異常歉眷,應該盡可能處理異常,使程序恢復運行颤枪,而不應該隨意終止異常汗捡。

1.1 舉例說明常見的Error和Exception?

image

Exception和Error都是繼承了Throwable類畏纲,在Java中只有Throwable類型的實例才可以被拋出(throw)或者捕獲(catch)扇住,它是異常處理機制的基本組成類型。

1.2 ClassNotFoundException和NoClassDefFoundError的區(qū)別盗胀?

NoClassDefFoundError是一個錯誤(Error)艘蹋,而ClassNOtFoundException是一個異常,在Java中錯誤和異常是有區(qū)別的票灰,我們可以從異常中恢復程序但卻不應該嘗試從錯誤中恢復程序女阀。

ClassNotFoundException的產(chǎn)生原因:Java支持使用Class.forName方法來動態(tài)地加載類,任意一個類的類名如果被作為參數(shù)傳遞給這個方法都將導致該類被加載到JVM內(nèi)存中米间,如果這個類在類路徑中沒有被找到强品,那么此時就會在運行時拋出ClassNotFoundException異常。

NoClassDefFoundError的產(chǎn)生原因:當一個類已經(jīng)某個類加載器加載到內(nèi)存中了屈糊,此時另一個類加載器又嘗試著動態(tài)地從同一個包中加載這個類的榛。

1.3 使用try...catche的注意事項?

  1. 盡量不要捕獲類似Exception這樣的通用異常逻锐,而是應該捕獲特定異常夫晌。當然更不要捕獲Throwable和Error類型的異常。
  2. 不要生吞(swallow)異常
  3. 引起異常的東西最好提前判斷
public void readPreferences(String fileName){
    // 如果此處fileName為空昧诱,就會導致NPE晓淀,就會蒙B,不知道是哪里出錯了盏档。所以需要在前面判斷一下
    InputStream in = new FileInputStream(fileName);
}
  1. 捕獲到的異常不要直接使用e.printStackTrace()打印

這個輸出是不受log4j或者logback管理的凶掰。是屬于標準輸出,一般直接輸出給了tomcat蜈亩,由于不受log4j或者logback管理懦窘,你就無法控制它的輸出位置和格式。如果做日志分析稚配,你無法控制它的輸出位置和格式畅涂,你就無法分析。

1.4 異常帶來的性能開銷道川?

1午衰、try-catch 代碼段會產(chǎn)生額外的性能開銷立宜,換個角度說,它往往會影響JVM對代碼進行優(yōu)化臊岸,所以建議僅捕獲有必要的代碼段橙数,盡量不要一個大的 try 包住整段的代碼;更不要利用異成鹊ィ控制代碼流程商模,這遠比我們通常意義上的條件語句(if/else、switch)要低效蜘澜。

2、Java每實例化一個 Exception响疚,都會對當時的棧進行快照鄙信,這是一個相對比較重的操作。如果發(fā)生的非常頻繁忿晕,這個開銷可就不能被忽略了装诡。

1.5 對于異常處理編程,不同的編程范式也會影響到異常處理策略践盼,比如鸦采,現(xiàn)在非常火熱的反應式編程(Reactive Stream)咕幻,因為其本身是異步渔伯、基于事件機制的,所以出現(xiàn)異常情況肄程,決不能簡單拋出去锣吼;另外,由于代碼堆棧不再是同步調(diào)用那種垂直的結構蓝厌,這里的異常處理和日志需要更加小心玄叠,我們看到的往往是特定executor的堆棧,而不是業(yè)務方法調(diào)用關系拓提。對于這種情況读恃,你有什么好的辦法嗎?

我之前做過一個項目代态,里面使用到了異步進行調(diào)來調(diào)去寺惫,如果里面拋出異常就會導致一下兩個問題:

  1. 如果同時啟動2個任務,就會導致打印的日志非常亂胆数,不知道是誰打印的肌蜻。

使其全程攜帶一個唯一變量(方法參數(shù)),打印日志的時候同時輸出必尼。

  1. 由于新建了一個線程蒋搜,當拋出異常的時候異常棧不包括調(diào)用它的那個(因為已經(jīng)切換線程了)篡撵。

可以將調(diào)用的方法傳入(入?yún)ⅲ┙o被調(diào)用者,也可以直接修改新建的線程名豆挽。

2育谬、final、finally帮哈、 finalize有什么不同膛檀?

final可以用來修飾類、方法娘侍、變量咖刃,分別有不同的意義,final修飾的class代表不可以繼承擴展憾筏,final的變量是不可以修改的嚎杨,而final的方法也是不可以重寫的(override)。

finally則是Java保證重點代碼一定要被執(zhí)行的一種機制氧腰。我們可以使用try-finally或者try-catch-finally來進行類似關閉JDBC連接枫浙、保證unlock鎖等動作。

finalize是基礎類java.lang.Object的一個方法古拴,它的設計目的是保證對象在被垃圾收集前完成特定資源的回收箩帚。finalize機制現(xiàn)在已經(jīng)不推薦使用,并且在JDK 9開始被標記為deprecated黄痪。

注意:下面的代碼不會被執(zhí)行的紧帕。

try {
  // do something
  System.exit(1);
} finally{
  System.out.println(“Print from finally”);
}

3、String满力、StringBuffer焕参、StringBuilder有什么區(qū)別?

String是Java語言非秤投睿基礎和重要的類叠纷,提供了構造和管理字符串的各種基本邏輯。它是典型的Immutable類(只讀)潦嘶,被聲明成為final class(防止擴展成不只讀的)涩嚣,所有屬性也都是final的。也由于它的不可變性掂僵,類似拼接航厚、裁剪字符串等動作,都會產(chǎn)生新的String對象锰蓬。由于字符串操作的普遍性幔睬,所以相關操作的效率往往對應用性能有明顯影響。

StringBuffer是為解決上面提到拼接產(chǎn)生太多中間對象的問題而提供的一個類芹扭,我們可以用append或者add方法麻顶,把字符串添加到已有序列的末尾或者指定位置赦抖。StringBuffer本質(zhì)是一個線程安全的可修改字符序列,它保證了線程安全辅肾,也隨之帶來了額外的性能開銷队萤,所以除非有線程安全的需要,不然還是推薦使用它的后繼者矫钓,也就是StringBuilder要尔。

StringBuilder是Java 1.5中新增的,在能力上和StringBuffer沒有本質(zhì)區(qū)別新娜,但是它去掉了線程安全的部分赵辕,有效減小了開銷,是絕大部分情況下進行字符串拼接的首選杯活。

3.1 直接拼接字符串在jdk8和9中的優(yōu)化匆帚?

String str = "1" + "a" + ...

在JDK 8中,字符串拼接操作會自動被javac轉(zhuǎn)換為StringBuilder操作旁钧,而在JDK 9里面則是因為Java 9為了更加統(tǒng)一字符串操作優(yōu)化,提供了StringConcatFactory互拾,作為一個統(tǒng)一的入口歪今。javac自動生成的代碼,雖然未必是最優(yōu)化的颜矿,但普通場景也足夠了寄猩,你可以酌情選擇。

3.2 字符串緩存骑疆?

在常見的應用系統(tǒng)中田篇,基本上堆空間中平均25%的對象是字符串,并且其中約半數(shù)是重復的箍铭。如果能避免創(chuàng)建重復字符串泊柬,可以有效降低內(nèi)存消耗和對象創(chuàng)建開銷。

String在Java 6以后提供了intern()方法诈火,目的是提示JVM把相應字符串緩存起來兽赁,以備重復使用。在我們創(chuàng)建字符串對象并調(diào)用intern()方法的時候冷守,如果已經(jīng)有緩存的字符串刀崖,就會返回緩存里的實例,否則將其緩存起來拍摇。一般來說亮钦,JVM會將所有的類似“abc”這樣的文本字符串,或者字符串常量之類緩存起來充活。

看起來很不錯是吧蜂莉?但實際情況估計會讓你大跌眼鏡蜡娶。一般使用Java 6這種歷史版本,并不推薦大量使用intern巡语,為什么呢翎蹈?魔鬼存在于細節(jié)中,被緩存的字符串是存在所謂PermGen里的男公,也就是臭名昭著的“永久代”荤堪,這個空間是很有限的,也基本不會被FullGC之外的垃圾收集照顧到枢赔。所以澄阳,如果使用不當,OOM就會光顧踏拜。

在后續(xù)版本中碎赢,這個緩存被放置在堆中,這樣就極大避免了永久代占滿的問題速梗,甚至永久代在JDK 8中被MetaSpace(元數(shù)據(jù)區(qū))替代了肮塞。而且,默認緩存大小也在不斷地擴大中姻锁,從最初的1009枕赵,到7u40以后被修改為60013。

Intern是一種顯式地排重機制位隶,但是它也有一定的副作用拷窜,因為需要開發(fā)者寫代碼時明確調(diào)用,一是不方便涧黄,每一個都顯式調(diào)用是非常麻煩的篮昧;另外就是我們很難保證效率,應用開發(fā)階段很難清楚地預計字符串的重復情況笋妥,有人認為這是一種污染代碼的實踐懊昨。

幸好在Oracle JDK 8u20之后,推出了一個新的特性挽鞠,也就是G1 GC下的字符串排重疚颊。它是通過將相同數(shù)據(jù)的字符串指向同一份數(shù)據(jù)來做到的,是JVM底層的改變信认,并不需要Java類庫做什么修改材义。

注意這個功能目前是默認關閉的,你需要使用下面參數(shù)開啟嫁赏,并且記得指定使用G1 GC:

-XX:+UseStringDeduplication

3.3 String自身的演化

如果你仔細觀察過Java的字符串其掂,在歷史版本中,它是使用char數(shù)組來存數(shù)據(jù)的潦蝇,這樣非常直接款熬。但是Java中的char是兩個bytes大小深寥,拉丁語系語言的字符,根本就不需要太寬的char贤牛,這樣無區(qū)別的實現(xiàn)就造成了一定的浪費惋鹅。密度是編程語言平臺永恒的話題,因為歸根結底絕大部分任務是要來操作數(shù)據(jù)的殉簸。

在Java 9中闰集,我們引入了Compact Strings的設計,對字符串進行了大刀闊斧的改進般卑。將數(shù)據(jù)存儲方式從char數(shù)組武鲁,改變?yōu)橐粋€byte數(shù)組加上一個標識編碼的所謂coder。

在通用的性能測試和產(chǎn)品實驗中蝠检,我們能非常明顯地看到緊湊字符串帶來的優(yōu)勢沐鼠,即更小的內(nèi)存占用、更快的操作速度叹谁。

4饲梭、Java反射機制,動態(tài)代理是基于什么原理焰檩?

反射可以讓我們在運行時動態(tài)創(chuàng)建對象排拷、獲取類中聲明的屬性和方法。

實現(xiàn)動態(tài)代理的方式很多锅尘,比如JDK自身提供的動態(tài)代理,就是主要利用了反射機制布蔗。還有其他的實現(xiàn)方式藤违,比如利用傳說中更高性能的字節(jié)碼操作機制,類似ASM纵揍、cglib(基于ASM)顿乒、Javassist等。

rpc調(diào)用和aop編程都使用了動態(tài)代理的技術泽谨。

4.1 Java是動態(tài)類型語言還是靜態(tài)類型語言璧榄?

動態(tài)類型和靜態(tài)類型就是其中一種分類角度,簡單區(qū)分就是語言類型信息是在運行時檢查吧雹,還是編譯期檢查骨杂。

Java是靜態(tài)的強類型語言,但是因為提供了類似反射等機制雄卷,也具備了部分動態(tài)類型語言的能力搓蚪。

4.2 什么時候使用反射?Jdbc為什么要用反射丁鹉?

簡單工廠妒潭,讓生成的對象可配置悴能,降低代碼依賴(解耦)。

4.3 反射的使用場景

  1. 自動生成get雳灾、set方法
  2. 繞過API訪問控制漠酿。我們?nèi)粘i_發(fā)時可能被迫要調(diào)用內(nèi)部API去做些事情,比如谎亩,自定義的高性能NIO框架需要顯式地釋放DirectBuffer炒嘲,使用反射繞開限制是一種常見辦法。

4.4 動態(tài)代理解決的問題

代理可以看作是對調(diào)用目標的一個包裝团驱,這樣我們對目標代碼的調(diào)用不是直接發(fā)生的摸吠,而是通過代理完成。

通過代理可以讓調(diào)用者與實現(xiàn)者之間解耦嚎花。比如進行RPC調(diào)用寸痢,框架內(nèi)部的尋址、序列化紊选、反序列化等啼止,對于調(diào)用者往往是沒有太大意義的,通過代理兵罢,可以提供更加友善的界面献烦。

增加代碼的擴展性

4.5 JDK代理與ASM代理如何選用?

JDK Proxy的優(yōu)勢:

  • 最小化依賴關系卖词,減少依賴意味著簡化開發(fā)和維護巩那,JDK本身的支持,可能比cglib更加可靠此蜈。
  • 平滑進行JDK版本升級即横,而字節(jié)碼類庫通常需要進行更新以保證在新版Java上能夠使用。
  • 代碼實現(xiàn)簡單裆赵。

基于類似cglib框架的優(yōu)勢:

  • 有的時候調(diào)用目標可能不便實現(xiàn)額外接口止毕,從某種角度看劣坊,限定調(diào)用者實現(xiàn)接口是有些侵入性的實踐比然,類似cglib動態(tài)代理就沒有這種限制背稼。
  • 只操作我們關心的類,而不必為其他相關類增加工作量植兰。
  • 高性能份帐。

5、int和Integer有什么區(qū)別钉跷?談談Integer的值緩存范圍弥鹦。

基本數(shù)據(jù)類型不是對象,本著萬物皆對象的理念,java為我們提供了基本類型的包裝類彬坏,并提供一些方法朦促。而且原始數(shù)據(jù)類型和Java泛型并不能配合使用

值緩存的范圍:-128~127

5.1 自動拆裝箱是什么栓始?發(fā)生在編譯階段還是運行時务冕?

裝箱:基本 -> 包裝

拆箱:包裝 -> 基本

自動裝箱實際上算是一種語法糖。什么是語法糖幻赚?可以簡單理解為Java平臺為我們自動進行了一些轉(zhuǎn)換禀忆,保證不同的寫法在運行時等價,它們發(fā)生在編譯階段落恼,也就是生成的字節(jié)碼是一致的箩退。**

像前面提到的整數(shù),javac替我們自動把裝箱轉(zhuǎn)換為Integer.valueOf()佳谦,把拆箱替換為Integer.intValue()戴涝。

自動拆裝箱發(fā)生在編譯階段

5.2 其它包裝類型有緩存機制嗎?

Boolean钻蔑,緩存了true/false對應實例啥刻,確切說,只會返回兩個常量實例Boolean.TRUE/FALSE咪笑。

Short可帽,同樣是緩存了-128到127之間的數(shù)值。

Byte窗怒,數(shù)值有限映跟,所以全部都被緩存。

Character扬虚,緩存范圍’\u0000’ 到 ‘\u007F’申窘。

5.3 使用拆裝箱注意點?

建議避免無意中的裝箱孔轴、拆箱行為,尤其是在性能敏感的場合碎捺,創(chuàng)建10萬個Java對象和10萬個整數(shù)的開銷可不是一個數(shù)量級的路鹰,不管是內(nèi)存使用還是處理速度,光是對象頭的空間占用就已經(jīng)是數(shù)量級的差距了收厨。

5.4 既然都有了包裝類晋柱,那么基本數(shù)據(jù)類型還有必要存在嗎?

在性能敏感的場合诵叁,創(chuàng)建10萬個Java對象和10萬個整數(shù)的開銷可不是一個數(shù)量級的

5.5 int與Integer之間的比較

1雁竞、Integer是int的包裝類,int則是java的一種基本數(shù)據(jù)類型 
2、Integer變量必須實例化后才能使用碑诉,而int變量不需要 
3彪腔、Integer實際是對象的引用,當new一個Integer時进栽,實際上是生成一個指針指向此對象德挣;而int則是直接存儲數(shù)據(jù)值 
4、Integer的默認值是null快毛,int的默認值是0

延伸: 
關于Integer和int的比較 
1格嗅、由于Integer變量實際上是對一個Integer對象的引用,所以兩個通過new生成的Integer變量永遠是不相等的(因為new生成的是兩個對象唠帝,其內(nèi)存地址不同)屯掖。

Integer i = new Integer(100);
Integer j = new Integer(100);
System.out.print(i == j); //false

2、Integer變量和int變量比較時襟衰,只要兩個變量的值是向等的贴铜,則結果為true(因為包裝類Integer和基本數(shù)據(jù)類型int比較時,java會自動拆包裝為int右蒲,然后進行比較阀湿,實際上就變?yōu)閮蓚€int變量的比較)

Integer i = new Integer(100);
int j = 100;
System.out.print(i == j); //true

3瑰妄、非new生成的Integer變量和new Integer()生成的變量比較時陷嘴,結果為false。(因為非new生成的Integer變量指向的是java常量池中的對象间坐,而new Integer()生成的變量指向堆中新建的對象灾挨,兩者在內(nèi)存中的地址不同)

Integer i = new Integer(100);
Integer j = 100;
System.out.print(i == j); //false

4、對于兩個非new生成的Integer對象竹宋,進行比較時劳澄,如果兩個變量的值在區(qū)間-128到127之間,則比較結果為true蜈七,如果兩個變量的值不在此區(qū)間秒拔,則比較結果為false

Integer i = 100;
Integer j = 100;
System.out.print(i == j); //true

Integer i = 128;
Integer j = 128;
System.out.print(i == j); //false

對于第4條的原因: 
java在編譯Integer i = 100 ;時,會翻譯成為Integer i = Integer.valueOf(100)飒硅;砂缩,而java API中對Integer類型的valueOf的定義如下:

public static Integer valueOf(int i){
    assert IntegerCache.high >= 127;
    if (i >= IntegerCache.low && i <= IntegerCache.high){
        return IntegerCache.cache[i + (-IntegerCache.low)];
    }
    return new Integer(i);
}

java對于-128到127之間的數(shù),會進行緩存三娩,Integer i = 127時庵芭,會將127進行緩存,下次再寫Integer j = 127時雀监,就會直接從緩存中取双吆,就不會new了

6、Vector、ArrayList好乐、LinkedList有何區(qū)別匾竿?

這三者都是實現(xiàn)集合框架中的List,也就是所謂的有序集合曹宴,因此具體功能也比較近似搂橙,比如都提供按照位置進行定位、添加或者刪除的操作笛坦,都提供迭代器以遍歷其內(nèi)容等区转。但因為具體的設計區(qū)別,在行為版扩、性能废离、線程安全等方面,表現(xiàn)又有很大不同礁芦。

Vector是Java早期提供的線程安全的動態(tài)數(shù)組蜻韭,如果不需要線程安全,并不建議選擇柿扣,畢竟同步是有額外開銷的肖方。Vector內(nèi)部是使用對象數(shù)組來保存數(shù)據(jù),可以根據(jù)需要自動的增加容量未状,當數(shù)組已滿時俯画,會創(chuàng)建新的數(shù)組,并拷貝原有數(shù)組數(shù)據(jù)司草。

ArrayList是應用更加廣泛的動態(tài)數(shù)組實現(xiàn)艰垂,它本身不是線程安全的,所以性能要好很多埋虹。與Vector近似猜憎,ArrayList也是可以根據(jù)需要調(diào)整容量,不過兩者的調(diào)整邏輯有所區(qū)別搔课,Vector在擴容時會提高1倍胰柑,而ArrayList則是增加50%。(插入元素時間復雜度:o(1)~o(n)爬泥,查找:o(1))

LinkedList顧名思義是Java提供的雙向鏈表旦事,所以它不需要像上面兩種那樣調(diào)整容量,它也不是線程安全的急灭。(插入元素時間復雜度:o(1),查找:o(1)~o(n))

6.1 集合們的基本特征和典型使用場景谷遂,以Set的幾個實現(xiàn)為例:

TreeSet支持自然順序訪問(通過比較器進行排序)葬馋,但是添加、刪除、包含等操作要相對低效(log(n)時間)畴嘶。

HashSet則是利用哈希算法蛋逾,理想情況下,如果哈希散列正常窗悯,可以提供常數(shù)時間的添加区匣、刪除、包含等操作蒋院,但是它不保證有序亏钩。

LinkedHashSet,內(nèi)部構建了一個記錄插入順序的雙向鏈表欺旧,因此提供了按照插入順序遍歷的能力姑丑,與此同時,也保證了常數(shù)時間的添加辞友、刪除栅哀、包含等操作,這些操作性能略低于HashSet称龙,因為需要維護鏈表的開銷留拾。

在遍歷元素時,HashSet性能受自身容量影響鲫尊,所以初始化時痴柔,除非有必要,不然不要將其背后的HashMap容量設置過大马昨。而對于LinkedHashSet竞帽,由于其內(nèi)部鏈表提供的方便,遍歷性能只和元素多少有關系鸿捧。

6.2

image

7屹篓、對比Hashtable、HashMap匙奴、TreeMap有什么不同堆巧?談談你對HashMap的掌握。

Hashtable是早期Java類庫提供的一個哈希表實現(xiàn)泼菌,本身是同步的谍肤,不支持null鍵和值,由于同步導致的性能開銷哗伯,所以已經(jīng)很少被推薦使用荒揣。

HashMap是應用更加廣泛的哈希表實現(xiàn),行為上大致上與HashTable一致焊刹,主要區(qū)別在于HashMap不是同步的系任,支持null鍵和值等恳蹲。通常情況下,HashMap進行put或者get操作俩滥,可以達到常數(shù)時間的性能嘉蕾,所以它是絕大部分利用鍵值對存取場景的首選,比如霜旧,實現(xiàn)一個用戶ID和用戶信息對應的運行時存儲結構错忱。

TreeMap則是基于紅黑樹的一種提供順序訪問的Map,和HashMap不同挂据,它的get以清、put、remove之類操作都是O(log(n))的時間復雜度棱貌,具體順序可以由指定的Comparator來決定玖媚,或者根據(jù)鍵的自然順序來判斷。

7.1 日常中使用哪種map最多婚脱?使用它有什么要求今魔?

大部分使用Map的場景,通常就是放入障贸、訪問或者刪除错森,而對順序沒有特別要求,HashMap在這種情況下基本是最好的選擇篮洁。HashMap的性能表現(xiàn)非常依賴于哈希碼的有效性涩维,請務必掌握hashCode和equals的一些基本約定,比如:

  1. equals相等袁波,hashCode一定要相等瓦阐。
  2. 重寫了hashCode也要重寫equals。
  3. hashCode需要保持一致性篷牌,狀態(tài)改變返回的哈希值仍然要一致睡蟋。
  4. equals的對稱、反射枷颊、傳遞等特性戳杀。

7.2 HashMap內(nèi)部實現(xiàn)基本點分析?HashMap內(nèi)部實現(xiàn)基本點分析夭苗?樹化 信卡?

首先,我們來一起看看HashMap內(nèi)部的結構傍菇,它可以看作是數(shù)組(Node<K,V>[] table)和鏈表結合組成的復合結構丢习,數(shù)組被分為一個個桶(bucket)泛领,通過哈希值決定了鍵值對在這個數(shù)組的尋址;哈希值相同的鍵值對敛惊,則以鏈表形式存儲渊鞋,你可以參考下面的示意圖。這里需要注意的是瞧挤,如果鏈表大小超過閾值(TREEIFY_THRESHOLD, 8)锡宋,圖中的鏈表就會被改造為樹形結構。

image

7.2.1 初始化

從構造函數(shù)的實現(xiàn)來看特恬,這個表格(數(shù)組)似乎并沒有在最初就初始化好执俩,僅僅設置了一些初始值而已。

public HashMap(int initialCapacity, float loadFactor){  
    // ... 
    this.loadFactor = loadFactor;
    this.threshold = tableSizeFor(initialCapacity);
}

所以癌刽,我們深刻懷疑役首,HashMap也許是按照lazy-load原則,在首次使用時被初始化(拷貝構造函數(shù)除外显拜,我這里僅介紹最通用的場景)衡奥。既然如此远荠,我們?nèi)タ纯磒ut方法實現(xiàn),似乎只有一個putVal的調(diào)用:

public V put(K key, V value) {
    return putVal(hash(key), key, value, false, true);
}

看來主要的秘密似乎藏在putVal里面守伸,到底有什么秘密呢校辩?為了節(jié)省空間,我這里只截取了putVal比較關鍵的幾部分庭砍。

final V putVal(int hash, K key, V value, boolean onlyIfAbent,
               boolean evit) {
    Node<K,V>[] tab; Node<K,V> p; int , i;
    if ((tab = table) == null || (n = tab.length) = 0)
        n = (tab = resize()).length;
    if ((p = tab[i = (n - 1) & hash]) == ull)
        tab[i] = newNode(hash, key, value, nll);
    else {
        // ...
        if (binCount >= TREEIFY_THRESHOLD - 1) // -1 for first 
           treeifyBin(tab, hash);
        //  ... 
     }
}

從putVal方法最初的幾行钳宪,我們就可以發(fā)現(xiàn)幾個有意思的地方:

  1. 如果表格是null,resize方法會負責初始化它疚俱,這從tab = resize()可以看出登馒。

  2. resize方法兼顧兩個職責秦忿,創(chuàng)建初始存儲表格,或者在容量不滿足需求的時候,進行擴容(resize)钩述。

  3. 在放置新的鍵值對的過程中方面,如果發(fā)生下面條件操禀,就會發(fā)生擴容蔑水。

if (++size > threshold)
    resize();
  1. 具體鍵值對在哈希表中的位置(數(shù)組index)取決于下面的位運算:
i = (n - 1) & hash

仔細觀察哈希值的源頭歇父,我們會發(fā)現(xiàn)翎冲,它并不是key本身的hashCode,而是來自于HashMap內(nèi)部的另外一個hash方法。注意,為什么這里需要將高位數(shù)據(jù)移位到低位進行異或運算呢?這是因為有些數(shù)據(jù)計算出的哈希值差異主要在高位,而HashMap里的哈希尋址是忽略容量以上的高位的,那么這種處理就可以有效避免類似情況下的哈希碰撞牲剃。**

static final int hash(Object kye) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>>16;
}
  1. 我前面提到的鏈表結構(這里叫bin)聪舒,會在達到一定門限值時止吁,發(fā)生樹化

7.2.2 resize方法

final Node<K,V>[] resize() {
    // ...
    else if ((newCap = oldCap << 1) < MAXIMUM_CAPACIY &&
                oldCap >= DEFAULT_INITIAL_CAPAITY)
        newThr = oldThr << 1; // double there
       // ... 
    else if (oldThr > 0) // initial capacity was placed in threshold
        newCap = oldThr;
    else {  
        // zero initial threshold signifies using defaultsfults
        newCap = DEFAULT_INITIAL_CAPAITY;
        newThr = (int)(DEFAULT_LOAD_ATOR* DEFAULT_INITIAL_CAPACITY宏怔;
    }
    if (newThr ==0) {
        float ft = (float)newCap * loadFator;
        newThr = (newCap < MAXIMUM_CAPACITY && ft < (float)MAXIMUM_CAPACITY ?(int)ft : Integer.MAX_VALUE);
    }
    threshold = neThr;
    Node<K,V>[] newTab = (Node<K,V>[])new Node[newap];
    table = n妨猩;
    // 移動到新的數(shù)組結構e數(shù)組結構 
   }

依據(jù)resize源碼,不考慮極端情況(容量理論最大極限由MAXIMUM_CAPACITY指定约谈,數(shù)值為 1<<30炬灭,也就是2的30次方),我們可以歸納為:

  1. 門限值等于(負載因子)x(容量)厦凤,如果構建HashMap的時候沒有指定它們鼻吮,那么就是依據(jù)相應的默認常量值。
  2. 門限通常是以倍數(shù)進行調(diào)整 (newThr = oldThr << 1)较鼓,我前面提到狈网,根據(jù)putVal中的邏輯讼积,當元素個數(shù)超過門限大小時阻问,則調(diào)整Map大小肆饶。
  3. 擴容后裆馒,需要將老的數(shù)組中的元素重新放置到新的數(shù)組订雾,這是擴容的一個主要開銷來源切油。

7.2.3 容量祠挫、負載因子

容量和負載系數(shù)決定了可用的桶的數(shù)量填大,空桶太多會浪費空間收壕,如果使用的太滿則會嚴重影響操作的性能。極端情況下手销,假設只有一個桶,那么它就退化成了鏈表毫捣,完全不能提供所謂常數(shù)時間存的性能详拙。

既然容量和負載因子這么重要,我們在實踐中應該如何選擇呢蔓同?

如果能夠知道HashMap要存取的鍵值對數(shù)量饶辙,可以考慮預先設置合適的容量大小。具體數(shù)值我們可以根據(jù)擴容發(fā)生的條件來做簡單預估斑粱,根據(jù)前面的代碼分析弃揽,我們知道它需要符合計算條件:

負載因子 * 容量 > 元素數(shù)量

而對于負載因子,我建議:

  1. 如果沒有特別需求则北,不要輕易進行更改鹏溯,因為JDK自身的默認負載因子是非常符合通用場景的需求的辈毯。

  2. 如果確實需要調(diào)整瓷产,建議不要設置超過0.75的數(shù)值催跪,因為會顯著增加沖突,降低HashMap的性能惑艇。

  3. 如果使用太小的負載因子蒿辙,按照上面的公式拇泛,預設容量值也進行調(diào)整滨巴,否則可能會導致更加頻繁的擴容,增加無謂的開銷俺叭,本身訪問性能也會受影響恭取。

7.2.4 樹化

樹化改造,對應邏輯主要在putVal和treeifyBin中熄守。

final void treeifyBin(Node<K,V>[] tab, int hash) {
    int n, index; Node<K,V> e;
    if (tab == null || (n = tab.length) < MIN_TREEIFY_CAPACITY)
        resize();
    else if ((e = tab[index = (n - 1) & hash]) != null) {
        //樹化改造邏輯
    }
}

上面是精簡過的treeifyBin示意蜈垮,綜合這兩個方法耗跛,樹化改造的邏輯就非常清晰了,可以理解為攒发,當bin的數(shù)量大于TREEIFY_THRESHOLD時:

  • 如果容量小于MIN_TREEIFY_CAPACITY调塌,只會進行簡單的擴容。
  • 如果容量大于MIN_TREEIFY_CAPACITY 惠猿,則會進行樹化改造羔砾。

那么,為什么HashMap要樹化呢偶妖?

本質(zhì)上這是個安全問題姜凄。因為在元素放置過程中,如果一個對象哈希沖突趾访,都被放置到同一個桶里态秧,則會形成一個鏈表,我們知道鏈表查詢是線性的扼鞋,會嚴重影響存取的性能申鱼。

而在現(xiàn)實世界,構造哈希沖突的數(shù)據(jù)并不是非常復雜的事情藏鹊,惡意代碼就可以利用這些數(shù)據(jù)大量與服務器端交互润讥,導致服務器端CPU大量占用,這就構成了哈希碰撞拒絕服務攻擊盘寡,國內(nèi)一線互聯(lián)網(wǎng)公司就發(fā)生過類似攻擊事件楚殿。

7.3 解決哈希沖突的常用方法?

http://www.reibang.com/p/4d3cb99d7580

8竿痰、ConcurrentHashMap如何實現(xiàn)高效地線程安全脆粥?

8.1 JDK7之前

早期ConcurrentHashMap,其實現(xiàn)是基于:

  1. 分離鎖影涉,也就是將內(nèi)部進行分段(Segment)变隔,里面則是HashEntry的數(shù)組,和HashMap類似蟹倾,哈希相同的條目也是以鏈表形式存放匣缘。其中Segment繼承ReentrantLock用來充當鎖的角色

  2. HashEntry內(nèi)部使用volatile的value字段來保證可見性,也利用了不可變對象的機制以改進利用Unsafe提供的底層能力鲜棠,比如volatile access肌厨,去直接完成部分操作,以最優(yōu)化性能豁陆,畢竟Unsafe中的很多操作都是JVM intrinsic優(yōu)化過的柑爸。

早期ConcurrentHashMap內(nèi)部結構的示意圖,其核心是利用分段設計盒音,在進行并發(fā)操作的時候表鳍,只需要鎖定相應段馅而,這樣就有效避免了類似Hashtable整體同步的問題,大大提高了性能譬圣。

image

在構造的時候瓮恭,Segment的數(shù)量由所謂的concurrentcyLevel決定,默認是16厘熟,也可以在相應構造函數(shù)直接指定偎血。注意,Java需要它是2的冪數(shù)值盯漂,如果輸入是類似15這種非冪值颇玷,會被自動調(diào)整到16之類2的冪數(shù)值。

具體情況就缆,我們一起看看一些Map基本操作的源碼帖渠,這是JDK 7比較新的get代碼。針對具體的優(yōu)化部分竭宰,為方便理解空郊,我直接注釋在代碼段里,get操作需要保證的是可見性切揭,所以并沒有什么同步邏輯狞甚。

public V get(Object key) {
    Segment<K,V> s; // manually integrate access methods to reduce overhead
    HashEntry<K,V>[] tab;
    int h = hash(key.hashCode());
    //利用位操作替換普通數(shù)學運算
    long u = (((h >>> segmentShift) & segmentMask) << SSHIFT) + SBASE;
    // 以Segment為單位,進行定位
    // 利用Unsafe直接進行volatile access
    if ((s = (Segment<K,V>)UNSAFE.getObjectVolatile(segments, u)) != null &&
        (tab = s.table) != null) {
        //省略
        }
    return null;
}

而對于put操作廓旬,首先是通過二次哈希避免哈希沖突哼审,然后以Unsafe調(diào)用方式,直接獲取相應的Segment孕豹,然后進行線程安全的put操作:

 public V put(K key, V value) {
    Segment<K,V> s;
    if (value == null)
        throw new NullPointerException();
    // 二次哈希涩盾,以保證數(shù)據(jù)的分散性,避免哈希沖突
    int hash = hash(key.hashCode());
    int j = (hash >>> segmentShift) & segmentMask;
    if ((s = (Segment<K,V>)UNSAFE.getObject          // nonvolatile; recheck
            (segments, (j << SSHIFT) + SBASE)) == null) //  in ensureSegment
        s = ensureSegment(j);
    return s.put(key, hash, value, false);
}

其核心邏輯實現(xiàn)在下面的內(nèi)部方法中:

final V put(K key, int hash, V value, boolean onlyIfAbsent) {
    // scanAndLockForPut會去查找是否有key相同Node
    // 無論如何励背,確保獲取鎖
    HashEntry<K,V> node = tryLock() ? null :
        scanAndLockForPut(key, hash, value);
    V oldValue;
    try {
        HashEntry<K,V>[] tab = table;
        int index = (tab.length - 1) & hash;
        HashEntry<K,V> first = entryAt(tab, index);
        for (HashEntry<K,V> e = first;;) {
            if (e != null) {
                K k;
                // 更新已有value...
            }
            else {
                // 放置HashEntry到特定位置春霍,如果超過閾值,進行rehash
                // ...
            }
        }
    } finally {
        unlock();
    }
    return oldValue;
}

所以叶眉,從上面的源碼清晰的看出址儒,在進行并發(fā)寫操作時:

ConcurrentHashMap會獲取再入鎖,以保證數(shù)據(jù)一致性衅疙,Segment本身就是基于ReentrantLock的擴展實現(xiàn)莲趣,所以,在并發(fā)修改期間炼蛤,相應Segment是被鎖定的妖爷。

在最初階段蝶涩,進行重復性的掃描理朋,以確定相應key值是否已經(jīng)在數(shù)組里面絮识,進而決定是更新還是放置操作,你可以在代碼里看到相應的注釋嗽上。重復掃描次舌、檢測沖突是ConcurrentHashMap的常見技巧。

我在專欄上一講介紹HashMap時兽愤,提到了可能發(fā)生的擴容問題彼念,在ConcurrentHashMap中同樣存在。不過有一個明顯區(qū)別浅萧,就是它進行的不是整體的擴容逐沙,而是單獨對Segment進行擴容,細節(jié)就不介紹了洼畅。

另外一個Map的size方法同樣需要關注吩案,它的實現(xiàn)涉及分離鎖的一個副作用

試想帝簇,如果不進行同步徘郭,簡單的計算所有Segment的總值,可能會因為并發(fā)put丧肴,導致結果不準確残揉,但是直接鎖定所有Segment進行計算,就會變得非常昂貴芋浮。其實抱环,分離鎖也限制了Map的初始化等操作。

所以纸巷,ConcurrentHashMap的實現(xiàn)是通過重試機制(RETRIES_BEFORE_LOCK江醇,指定重試次數(shù)2),來試圖獲得可靠值何暇。如果沒有監(jiān)控到發(fā)生變化(通過對比Segment.modCount)陶夜,就直接返回,否則獲取鎖進行操作裆站。

每個segment都有一個modCount變量保存修改次數(shù)条辟,segment被更新時modCount會+1。所以在size()計算大小時宏胯,會判斷每個segment的modCount是否有變化羽嫡,如果有變化,如果有變化則重新計算肩袍,當然忍耐是有限度的杭棵,重試3次后就會將所有segment鎖住,計算完size后就會釋放鎖。

8.2 Java 8和之后的版本中魂爪,ConcurrentHashMap發(fā)生了哪些變化呢先舷?

  1. 整體結構上變得與HashMap的結構相似,同樣是大的桶(bucket)數(shù)組滓侍,然后內(nèi)部也是一個個所謂的鏈表結構(bin)蒋川;同步的粒度要更細致一些。
  2. 其內(nèi)部仍然有Segment定義撩笆,但僅僅是為了保證序列化時的兼容性而已捺球,不再有任何結構上的用處。
  3. 因為不再使用Segment夕冲,初始化操作大大簡化氮兵,修改為lazy-load形式,這樣可以有效避免初始開銷歹鱼,解決了老版本很多人抱怨的這一點胆剧。
  4. 數(shù)據(jù)存儲利用volatile來保證可見性
  5. 使用CAS等操作醉冤,在特定場景進行無鎖并發(fā)操作秩霍。
  6. 使用Unsafe、LongAdder之類底層手段蚁阳,進行極端情況的優(yōu)化铃绒。

先看看現(xiàn)在的數(shù)據(jù)存儲內(nèi)部實現(xiàn),我們可以發(fā)現(xiàn)Key是final的螺捐,因為在生命周期中颠悬,一個條目的Key發(fā)生變化是不可能的;與此同時val定血,則聲明為volatile赔癌,以保證可見性。

static class Node<K,V> implements Map.Entry<K,V> {
    final int hash;
    final K key;
    volatile V val;
    volatile Node<K,V> next;
    // … 
}

8.2.1 源碼中基本概念

table:默認為null澜沟,初始化發(fā)生在第一次插入操作灾票,默認大小為16的數(shù)組,用來存儲Node節(jié)點數(shù)據(jù)茫虽,擴容時大小總是2的冪次方刊苍。

nextTable:默認為null,擴容時新生成的數(shù)組濒析,其大小為原數(shù)組的兩倍正什。

sizeCtl:默認為0,用來控制table的初始化和擴容操作号杏,具體應用在后續(xù)會體現(xiàn)出來婴氮。

  • -1代表table正在初始化
  • -N表示有N-1個線程正在進行擴容操作

Node:保存key,value及key的hash值的數(shù)據(jù)結構。

ForwardingNode:一個特殊的Node節(jié)點主经,hash值為-1荣暮,其中存儲nextTable的引用。

final class ForwardingNode<K,V> extends Node<K,V> {
    final Node<K,V>[] nextTable;
    ForwardingNode(Node<K,V>[] tab) {
        super(MOVED, null, null, null);
        this.nextTable = tab;
    }
}

只有table發(fā)生擴容的時候旨怠,F(xiàn)orwardingNode才會發(fā)揮作用,作為一個占位符放在table中表示當前節(jié)點為null或則已經(jīng)被移動蜈块。

8.2.2 初始化

和HashMap一樣鉴腻,table初始化操作會延緩到第一次put行為。但是put是可以并發(fā)執(zhí)行的百揭,那么是如何實現(xiàn)table只初始化一次的爽哎?

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        // 如果一個線程發(fā)現(xiàn)sizeCtl<0,意味著另外的線程執(zhí)行CAS操作成功器一,當前線程只需要讓出cpu時間片
        if ((sc = sizeCtl) < 0) 
            Thread.yield(); // lost initialization race; just spin
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                if ((tab = table) == null || tab.length == 0) {
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

sizeCtl默認為0课锌,如果ConcurrentHashMap實例化時有傳參數(shù),sizeCtl會是一個2的冪次方的值祈秕。所以執(zhí)行第一次put操作的線程會執(zhí)行Unsafe.compareAndSwapInt方法修改sizeCtl為-1渺贤,有且只有一個線程能夠修改成功,其它線程通過Thread.yield()讓出CPU時間片等待table初始化完成请毛。

8.2.3 put操作

假設table已經(jīng)初始化完成志鞍,put操作采用CAS+synchronized實現(xiàn)并發(fā)插入或更新操作,具體實現(xiàn)如下方仿。

final V putVal(K key, V value, boolean onlyIfAbsent) { if (key == null || value == null) throw new NullPointerException();

    // 計算hash值
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh; K fk; V fv;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable();
        // 將高位數(shù)據(jù)移位到低位進行異或運算固棚,算出table中的位置
        // 然后通過cas的getObjectVolatile方法判斷這個位置是不是空的
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            // 利用CAS操作初始化這個位置,如果cas操作成功仙蚜,則結束此洲,否則進行自旋等待下一次操作。
            if (casTabAt(tab, i, null, new Node<K,V>(hash, key, value)))
                break; 
        }
        // 如果f的hash值為-1委粉,說明當前f是ForwardingNode節(jié)點呜师,意味有其它線程正在擴容,則一起進行擴容操作贾节。
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else if (onlyIfAbsent // 不加鎖匣掸,進行檢查
                 && fh == hash
                 && ((fk = f.key) == key || (fk != null && key.equals(fk)))
                 && (fv = f.val) != null)
            return fv;
        else {
            V oldVal = null;
            synchronized (f) {
                    // 在節(jié)點f上進行同步,節(jié)點插入之前氮双,再次利用tabAt(tab, i) == f判斷碰酝,防止被其它線程修改。
                    // 如果f.hash >= 0戴差,說明f是鏈表結構的頭結點送爸,遍歷鏈表,如果找到對應的node節(jié)點,則修改value袭厂,否則在鏈表尾部加入節(jié)點墨吓。
                    // 如果f是TreeBin類型節(jié)點,說明f是紅黑樹根節(jié)點纹磺,則在樹結構上遍歷元素帖烘,更新或增加節(jié)點。
                }
            }
            // 如果鏈表中節(jié)點數(shù)binCount >= TREEIFY_THRESHOLD(默認是8)橄杨,則把鏈表轉(zhuǎn)化為紅黑樹結構秘症。
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    // 檢查當前容量是否需要進行擴容。
    addCount(1L, binCount);
    return null;
}

8.2.4 size方法

final long sumCount() {
    CounterCell[] as = counterCells; CounterCell a;
    long sum = baseCount;
    if (as != null) {
        for (int i = 0; i < as.length; ++i) {
            if ((a = as[i]) != null)
                sum += a.value;
        }
    }
    return sum;
}

我們發(fā)現(xiàn)式矫,雖然思路仍然和以前類似乡摹,都是分而治之的進行計數(shù),然后求和處理采转,但實現(xiàn)卻基于一個奇怪的CounterCell聪廉。

static final class CounterCell {
    volatile long value;
    CounterCell(long x) { value = x; }
}

對于CounterCell的操作,是基于java.util.concurrent.atomic.LongAdder進行的故慈,是一種JVM利用空間換取更高效率的方法板熊,利用了Striped64內(nèi)部的復雜邏輯。這個東西非常小眾察绷,大多數(shù)情況下邻邮,建議還是使用AtomicLong,足以滿足絕大部分應用的性能需求克婶。

部分參考于:http://www.reibang.com/p/c0642afe03e0

9筒严、Java提供了哪些IO方式? NIO如何實現(xiàn)多路復用情萤?

Java IO方式有很多種鸭蛙,基于不同的IO抽象模型和交互方式,可以進行簡單區(qū)分筋岛。

BIO:同步阻塞I/O模式娶视,數(shù)據(jù)的讀取寫入必須阻塞在一個線程內(nèi)等待其完成。

它基于流模型實現(xiàn)睁宰,提供了我們最熟知的一些IO功能肪获,比如File抽象、輸入輸出流等柒傻。交互方式是同步孝赫、阻塞的方式,也就是說红符,在讀取輸入流或者寫入輸出流時青柄,在讀伐债、寫動作完成之前,線程會一直阻塞在那里致开,它們之間的調(diào)用是可靠的線性順序峰锁。

java.io包的好處是代碼比較簡單、直觀双戳,缺點則是IO效率和擴展性存在局限性虹蒋,容易成為應用性能的瓶頸。

很多時候飒货,人們也把java.net下面提供的部分網(wǎng)絡API魄衅,比如Socket、ServerSocket膏斤、> HttpURLConnection也歸類到同步阻塞IO類庫徐绑,因為網(wǎng)絡通信同樣是IO行為邪驮。

NIO:同時支持阻塞與非阻塞模式莫辨,但主要是使用同步非阻塞IO

在Java 1.4中引入了NIO框架(java.nio包)毅访,提供了Channel沮榜、Selector、Buffer等新的抽象喻粹,可以構建多路復用的蟆融、同步非阻塞IO程序,同時提供了更接近操作系統(tǒng)底層的高性能數(shù)據(jù)操作方式守呜。

NIO2:異步非阻塞I/O模型型酥。

在Java 7中,NIO有了進一步的改進查乒,也就是NIO 2弥喉,引入了異步非阻塞IO方式,也有很多人叫它AIO(Asynchronous IO)玛迄。異步IO操作基于事件和回調(diào)機制由境,可以簡單理解為,應用操作直接返回蓖议,而不會阻塞在那里虏杰,當后臺處理完成,操作系統(tǒng)會通知相應線程進行后續(xù)工作勒虾。

9.1 序列化與IO操作纺阔?

序列化(Serialization):將對象的狀態(tài)信息轉(zhuǎn)換為可以存儲或傳輸?shù)男问降倪^程。

IO操作:從文件修然、socket中讀寫數(shù)據(jù)的操作州弟。

java中的對象需要進行序列化之后钧栖,才能進行IO操作。

9.2 阻塞婆翔、非阻塞和同步拯杠、異步的區(qū)別?

同步是一種可靠的有序運行機制啃奴,當我們進行同步操作時潭陪,后續(xù)的任務是等待當前調(diào)用返回,才會進行下一步最蕾;

異步則相反依溯,其他任務不需要等待當前調(diào)用返回,通常依靠事件瘟则、回調(diào)等機制來實現(xiàn)任務間次序關系剥汤。

在進行阻塞操作時故硅,當前線程會處于阻塞狀態(tài),無法從事其他任務,只有當條件就緒才能繼續(xù)砾莱,比如ServerSocket新連接建立完畢债鸡,或數(shù)據(jù)讀取箩兽、寫入操作完成沃疮。

非阻塞則是不管IO操作是否結束,直接返回菌赖,相應操作在后臺繼續(xù)處理缭乘。

阻塞、非阻塞說的是調(diào)用者琉用,同步堕绩、異步說的是被調(diào)用者。

0708fix:

同步:在哪等著事情做完

異步:去干別的事情了邑时,事情做完了通知我們

不知道是不是對的

// 異步阻塞
Future f = xxx;
f.get();

// 異步非阻塞
Future f = xxx;
f.addListener(yyy);

// 同步非阻塞
Future f = xxx;
while (f.isdone()) {
}

9.3 InputStream/OutputStream和Reader/Writer的關系和區(qū)別奴紧?

輸入流、輸出流(InputStream/OutputStream)是用于讀取或?qū)懭胱止?jié)的刁愿,例如操作圖片文件绰寞。

Reader/Writer則是用于操作字符,增加了字符編解碼等功能铣口,適用于類似從文件中讀取或者寫入文本信息滤钱。本質(zhì)上計算機操作的都是字節(jié),不管是網(wǎng)絡通信還是文件讀取脑题,Reader/Writer相當于構建了應用邏輯和原始數(shù)據(jù)之間的橋梁件缸。

BufferedOutputStream等帶緩沖區(qū)的實現(xiàn),可以避免頻繁的磁盤讀寫叔遂,進而提高IO處理效率他炊。這種設計利用了緩沖區(qū)争剿,將批量數(shù)據(jù)進行一次操作,但在使用中千萬別忘了flush痊末。

9.4 為什么大多數(shù)IO工具類都實現(xiàn)了Closeable接口蚕苇?

image

因為需要進行資源的釋放。比如凿叠,打開FileInputStream涩笤,它就會獲取相應的文件描述符(FileDescriptor),需要利用try-with-resources盒件、 try-finally等機制保證FileInputStream被明確關閉蹬碧,進而相應文件描述符也會失效,否則將導致資源無法被釋放炒刁。利用專欄前面的內(nèi)容提到的Cleaner或finalize機制作為資源釋放的最后把關恩沽,也是必要的。

文件描述符(File descriptor)是計算機科學中的一個術語翔始,是一個用于表述指向文件的引用的抽象化概念罗心。 —— wiki

9.5 Java NIO中的概念?

Buffer绽昏,高效的數(shù)據(jù)容器协屡,除了布爾類型俏脊,所有原始數(shù)據(jù)類型都有相應的Buffer實現(xiàn)全谤。

Channel,類似在Linux之類操作系統(tǒng)上看到的文件描述符爷贫,是NIO中被用來支持批量式IO操作的一種抽象认然。

File或者Socket,通常被認為是比較高層次的抽象漫萄,而Channel則是更加操作系統(tǒng)底層的一種抽象卷员,這也使得NIO得以充分利用現(xiàn)代操作系統(tǒng)底層機制,獲得特定場景的性能優(yōu)化腾务,例如毕骡,DMA(Direct Memory Access)等。不同層次的抽象是相互關聯(lián)的岩瘦,我們可以通過Socket獲取Channel未巫,反之亦然。

Selector启昧,是NIO實現(xiàn)多路復用的基礎叙凡,它提供了一種高效的機制,可以檢測到注冊在Selector上的多個Channel中密末,是否有Channel處于就緒狀態(tài)握爷,進而實現(xiàn)了單線程對多Channel的高效管理跛璧。

在Linux上依賴于epoll,windows上依賴于epoll新啼。

Chartset追城,提供Unicode字符串定義,NIO也提供了相應的編解碼器等燥撞,例如漓柑,通過下面的方式進行字符串到ByteBuffer的轉(zhuǎn)換:

Charset.defaultCharset().encode("Hello world!"));

9.6 NIO能解決什么問題?

以聊天室為例:

如果采用每來一個用戶就新建一個線程的方式:

由于Java語言目前的線程實現(xiàn)是比較重量級的叨吮,啟動或者銷毀一個線程是有明顯開銷的辆布,每個線程都有單獨的線程棧等結構,需要占用非常明顯的內(nèi)存茶鉴,所以锋玲,每一個Client啟動一個線程似乎都有些浪費。

那么我們引入線程池:

如果連接數(shù)并不是非常多涵叮,只有最多幾百個連接的普通應用惭蹂,這種模式往往可以工作的很好。但是割粮,如果連接數(shù)量急劇上升盾碗,這種實現(xiàn)方式就無法很好地工作了,因為線程上下文切換開銷會在高并發(fā)時變得很明顯舀瓢,這是同步阻塞方式的低擴展性劣勢廷雅。

引入IO多路復用:

在前面兩個樣例中,IO都是同步阻塞模式京髓,所以需要多線程以實現(xiàn)多任務處理航缀。而NIO則是利用了單線程輪詢事件的機制,通過高效地定位就緒的Channel堰怨,來決定做什么芥玉,僅僅select階段是阻塞的,可以有效避免大量客戶端連接時备图,頻繁線程切換帶來的問題灿巧,應用的擴展能力有了非常大的提高。

IO多路復用:通過一種機制揽涮,可以監(jiān)視多個描述符抠藕,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作绞吁。

9.7 NIO多路復用的局限性是什么呢幢痘?

1、如果并發(fā)并不高家破,一直在輪詢的等待請求颜说,非常浪費CPU資源

2购岗、處理請求的操作不能太長

10、Java有幾種文件拷貝方式门粪?哪一種最高效喊积?

利用java.io類庫,直接為源文件構建一個FileInputStream讀取玄妈,然后再為目標文件構建一個FileOutputStream乾吻,完成寫入工作。

public static void copyFileByStream(File source, File dest) throws IOException {
    try (InputStream is = new FileInputStream(source);
        OutputStream os = new FileOutputStream(dest);){
        byte[] buffer = new byte[1024];
        int length;
        while ((length = is.read(buffer)) > 0) {
            os.write(buffer, 0, length);
        }
    }
}

利用java.nio類庫提供的transferTo或transferFrom方法實現(xiàn)拟蜻。

public static void copyFileByChannel(File source, File dest) throws IOException {
    try (FileChannel sourceChannel = new FileInputStream(source).getChannel();
         FileChannel targetChannel = new FileOutputStream(dest).getChannel()) {

        for (long count = sourceChannel.size(); count > 0; ) {
            long transferred = sourceChannel.transferTo(sourceChannel.position(), count, targetChannel);
            sourceChannel.position(sourceChannel.position() + transferred);
            count -= transferred;
        }
    }
}

對于Copy的效率绎签,這個其實與操作系統(tǒng)和配置等情況相關,總體上來說酝锅,NIO transferTo/From的方式可能更快诡必,因為它更能利用現(xiàn)代操作系統(tǒng)底層機制,避免不必要拷貝和上下文切換搔扁。

10.1 2種拷貝實現(xiàn)機制爸舒?

計算機操作系統(tǒng)通常將虛擬內(nèi)存分離為內(nèi)核空間和用戶空間。這種分離主要用于提供內(nèi)存保護和硬件保護稿蹲,以防止惡意或錯誤的軟件行為扭勉。操作系統(tǒng)內(nèi)核、硬件驅(qū)動等運行在內(nèi)核態(tài)空間苛聘,具有相對高的特權涂炎;而用戶態(tài)空間,則是給普通應用和服務使用焰盗。

10.1.1 使用BIO的方式

當我們使用輸入輸出流進行讀寫時璧尸,實際上是進行了多次上下文切換咒林,比如應用讀取數(shù)據(jù)時熬拒,先在內(nèi)核態(tài)將數(shù)據(jù)從磁盤讀取到內(nèi)核緩存,再切換到用戶態(tài)將數(shù)據(jù)從內(nèi)核緩存讀取到用戶緩存垫竞。寫入操作也是類似澎粟,僅僅是步驟相反。

image

這種方式會帶來一定的額外開銷欢瞪,可能會降低IO效率活烙。

10.1.2 使用NIO的方式

NIO transferTo的實現(xiàn)方式,在Linux和Unix上遣鼓,則會使用到零拷貝技術啸盏,數(shù)據(jù)傳輸并不需要用戶態(tài)參與,省去了上下文切換的開銷和不必要的內(nèi)存拷貝骑祟,進而可能提高應用拷貝性能气笙。注意怯晕,transferTo不僅僅是可以用在文件拷貝中舟茶,與其類似的,例如讀取磁盤文件隧出,然后進行Socket發(fā)送阀捅,同樣可以享受這種機制帶來的性能和擴展性提高鸳劳。

image

10.2 。也搓。赏廓。

[其它](file:///Volumes/%E9%91%AB%E5%93%A5%E6%A3%92%E6%A3%92%E5%93%92%E7%9A%84%E7%A7%BB%E5%8A%A8%E7%A1%AC%E7%9B%98/%E6%95%99%E7%A8%8B/Java%E6%A0%B8%E5%BF%83%E6%8A%80%E6%9C%AF36%E8%AE%B2/%E7%AC%AC12%E8%AE%B2.Java%E6%9C%89%E5%87%A0%E7%A7%8D%E6%96%87%E4%BB%B6%E6%8B%B7%E8%B4%9D%E6%96%B9%E5%BC%8F%EF%BC%9F%E5%93%AA%E4%B8%80%E7%A7%8D%E6%9C%80%E9%AB%98%E6%95%88%EF%BC%9F.html)

11、接口和抽象類有什么區(qū)別傍妒?

接口是對行為的抽象幔摸,它是抽象方法的集合,利用接口可以達到API定義和實現(xiàn)分離的目的颤练。接口既忆,不能實例化;不能包含任何非常量成員嗦玖,任何field都是隱含著public static final的意義患雇;同時,沒有非靜態(tài)方法實現(xiàn)宇挫,也就是說要么是抽象方法苛吱,要么是靜態(tài)方法。Java標準類庫中器瘪,定義了非常多的接口援所,比如java.util.List。

接口的職責也不僅僅限于抽象方法的集合瘟檩,其實有各種不同的實踐。有一類沒有任何方法的接口睹簇,通常叫作Marker Interface,顧名思義,它的目的就是為了聲明某些東西埃脏,比如我們熟知的Cloneable、Serializable等堵幽。這種用法,也存在于業(yè)界其他的Java產(chǎn)品代碼中。

抽象類是不能實例化的類溃肪,用abstract關鍵字修飾class躺涝,其目的主要是代碼重用夯膀。除了不能實例化,形式上和一般的Java類并沒有太大區(qū)別俺猿,可以有一個或者多個抽象方法凯肋,也可以沒有抽象方法圈盔。抽象類大多用于抽取相關Java類的共用方法實現(xiàn)或者是共同成員變量,然后通過繼承的方式達到代碼復用的目的。Java標準庫中围辙,比如collection框架,很多通用部分就被抽取成為抽象類掸冤,例如java.util.AbstractList。

11.1 面向?qū)ο笤O計

封裝的目的是隱藏事務內(nèi)部的實現(xiàn)細節(jié),以便提高安全性和簡化編程罗丰。封裝提供了合理的邊界,避免外部調(diào)用者接觸到內(nèi)部的細節(jié)绍填。我們在日常開發(fā)中,因為無意間暴露了細節(jié)導致的難纏bug太多了住闯,比如在多線程環(huán)境暴露內(nèi)部狀態(tài)杠巡,導致的并發(fā)修改問題蚌铜。從另外一個角度看,封裝這種隱藏,也提供了簡化的界面涣觉,避免太多無意義的細節(jié)浪費調(diào)用者的精力。

繼承代碼復用的基礎機制攀隔,類似于我們對于馬昆汹、白馬、黑馬的歸納總結。但要注意捅彻,繼承可以看作是非常緊耦合的一種關系,父類代碼修改缭裆,子類行為也會變動。在實踐中,過度濫用繼承内边,可能會起到反效果。

多態(tài),你可能立即會想到重寫(override)和重載(overload)眶俩、向上轉(zhuǎn)型纲岭。簡單說,重寫是父子類中相同名字和參數(shù)的方法,不同的實現(xiàn)燃乍;重載則是相同名字的方法,但是不同的參數(shù)舆瘪,本質(zhì)上這些方法簽名(由方法名稱和一個參數(shù)列表組成)是不一樣的介陶,為了更好說明,請參考下面的樣例代碼:

public int doSomething() {
    return 0;
}
// 輸入?yún)?shù)不同,意味著方法簽名不同玻墅,重載的體現(xiàn)
public int doSomething(List<String> strs) {
    return 0;
}
// return類型不一樣,編譯不能通過
public short doSomething() {
    return 0;
}

11.2 SOLID設計原則

首字母 指代 概念
S 單一功能原則 認為對象應該僅具有一種單一功能的概念剩拢,在程序設計中如果發(fā)現(xiàn)某個類承擔著多種義務,可以考慮進行拆分角雷。
O 開閉原則 認為“軟件體應該是對于擴展開放的,但是對于修改封閉的”的概念檩咱。
L 里氏替換原則 認為“程序中的對象應該是可以在不改變程序正確性的前提下被它的子類所替換的”的概念绊含。參考契約式設計逃顶。
I 接口隔離原則 認為“多個特定客戶端接口要好于一個寬泛用途的接口”[5] 的概念。
D 依賴反轉(zhuǎn)原則 認為一個方法應該遵從“依賴于抽象而不是一個實例”[5] 的概念。 依賴注入是該原則的一種實現(xiàn)方式抖誉。

12、談談你知道的設計模式?

創(chuàng)建型模式夺艰,是對對象創(chuàng)建過程的各種問題和解決方案的總結,包括各種工廠模式(Factory烹植、Abstract Factory)、單例模式(Singleton)墩虹、構建器模式(Builder)、原型模式(ProtoType)菌湃。

結構型模式,是針對軟件設計結構的總結,關注于類势似、對象繼承霹抛、組合方式的實踐經(jīng)驗霞篡。常見的結構型模式顶滩,包括橋接模式(Bridge)赁豆、適配器模式(Adapter)、裝飾者模式(Decorator)节预、代理模式(Proxy)挫剑、組合模式(Composite)、外觀模式(Facade)、享元模式(Flyweight)等。

行為型模式,是從類或?qū)ο笾g交互秒裕、職責劃分等角度總結的模式梭稚。比較常見的行為型模式有策略模式(Strategy)、解釋器模式(Interpreter)暇昂、命令模式(Command)、觀察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Template Method)、訪問者模式(Visitor)忙迁。

12.1 如何識別裝飾器模式梅誓?

識別類設計特征來進行判斷恰梢,也就是其類構造函數(shù)以相同的抽象類或者接口為輸入?yún)?shù)

因為裝飾器模式本質(zhì)上是包裝同類型實例梗掰,我們對目標對象的調(diào)用嵌言,往往會通過包裝類覆蓋過的方法,迂回調(diào)用被包裝的實例及穗,這就可以很自然地實現(xiàn)增加額外邏輯的目的,也就是所謂的“裝飾”。

例如,BufferedInputStream經(jīng)過包裝逼争,為輸入流過程增加緩存稿壁,類似這種裝飾器還可以多次嵌套,不斷地增加不同層次的功能牌借。

public BufferedInputStream(InputStream in)

我在下面的類圖里够吩,簡單總結了InputStream的裝飾模式實踐硕淑。

image

12.2 建造者模式

使用構建器模式传黄,可以比較優(yōu)雅地解決構建復雜對象的麻煩相恃,這里的“復雜”是指類似需要輸入的參數(shù)組合較多羹呵,如果用構造函數(shù)邻吭,我們往往需要為每一種可能的輸入?yún)?shù)組合實現(xiàn)相應的構造函數(shù),一系列復雜的構造函數(shù)會讓代碼閱讀性和可維護性變得很差。

HttpRequest request = HttpRequest.newBuilder(new URI(uri))
                     .header(headerAlice, valueAlice)
                     .headers(headerBob, value1Bob,
                      headerCarl, valueCarl,
                      headerBob, value2Bob)
                     .GET()
                     .build();

12.3 Spring等框架中使用了哪些模式忆家?

BeanFactory和ApplicationContext應用了工廠模式。

在Bean的創(chuàng)建中篓足,Spring也為不同scope定義的對象暴心,提供了單例和原型等模式實現(xiàn)。

原型模式是創(chuàng)建型模式的一種毅贮,其特點在于通過「復制」一個已經(jīng)存在的實例來返回新的實例,而不是新建實例盛卡。 被復制的實例就是我們所稱的「原型」嘱吗,這個原型是可定制的。 原型模式多用于創(chuàng)建復雜的或者耗時的實例顿涣,因為這種情況下揉阎,復制一個已經(jīng)存在的實例使程序運行更高效螟加;或者創(chuàng)建值相等哄芜,只是命名不一樣的同類數(shù)據(jù)貌亭。 —— wiki

AOP領域則是使用了代理模式、裝飾器模式认臊、適配器模式等圃庭。

各種事件監(jiān)聽器,是觀察者模式的典型應用失晴。

類似JdbcTemplate等則是應用了模板模式剧腻。

13、一個線程兩次調(diào)用start()方法會出現(xiàn)什么情況涂屁?談談線程的生命周期和狀態(tài)轉(zhuǎn)移书在?

Java的線程是不允許啟動兩次的,第二次調(diào)用必然會拋出IllegalThreadStateException胯陋,這是一種運行時異常蕊温,多次調(diào)用start被認為是編程錯誤。

關于線程生命周期的不同狀態(tài)遏乔,在Java 5以后义矛,線程狀態(tài)被明確定義在其公共內(nèi)部枚舉類型java.lang.Thread.State中,分別是:

  • 新建(NEW)盟萨,表示線程被創(chuàng)建出來還沒真正啟動的狀態(tài)凉翻,可以認為它是個Java內(nèi)部狀態(tài)。

  • 就緒(RUNNABLE)捻激,表示該線程已經(jīng)在JVM中執(zhí)行制轰,當然由于執(zhí)行需要計算資源前计,它可能是正在運行,也可能還在等待系統(tǒng)分配給它CPU片段垃杖,在就緒隊列里面排隊男杈。

    • 在其他一些分析中,會額外區(qū)分一種狀態(tài)RUNNING调俘,但是從Java API的角度伶棒,并不能表示出來。
  • 阻塞(BLOCKED)彩库,這個狀態(tài)和我們前面兩講介紹的同步非常相關肤无,阻塞表示線程在等待Monitor lock。比如骇钦,線程試圖通過synchronized去獲取某個鎖凛澎,但是其他線程已經(jīng)獨占了慷彤,那么當前線程就會處于阻塞狀態(tài)岩榆。

  • 等待(WAITING)各薇,表示正在等待其他線程采取某些操作。一個常見的場景是類似生產(chǎn)者消費者模式坦仍,發(fā)現(xiàn)任務條件尚未滿足鳍烁,就讓當前消費者線程等待(wait),另外的生產(chǎn)者線程去準備任務數(shù)據(jù)繁扎,然后通過類似notify等動作幔荒,通知消費線程可以繼續(xù)工作了。Thread.join()也會令線程進入等待狀態(tài)梳玫。

  • 計時等待(TIMED_WAIT)爹梁,其進入條件和等待狀態(tài)類似,但是調(diào)用的是存在超時條件的方法提澎,比如wait或join等方法的指定超時版本姚垃,如下面示例:

public final native void wait(long timeout) throws InterruptedException;
  • 終止(TERMINATED),不管是意外退出還是正常執(zhí)行結束盼忌,線程已經(jīng)完成使命积糯,終止運行,也有人把這個狀態(tài)叫作死亡谦纱。

在第二次調(diào)用start()方法的時候看成,線程可能處于終止或者其他(非NEW)狀態(tài),但是不論如何跨嘉,都是不可以再次啟動的川慌。

image

14、Java并發(fā)包提供了哪些并發(fā)工具類?

我們通常所說的并發(fā)包也就是java.util.concurrent及其子包梦重,集中了Java并發(fā)的各種基礎工具類兑燥,具體主要包括幾個方面:

  • 提供了比synchronized更加高級的各種同步結構,包括CountDownLatch琴拧、CyclicBarrier降瞳、Semaphore等,可以實現(xiàn)更加豐富的多線程操作蚓胸,比如利用Semaphore作為資源控制器力崇,限制同時進行工作的線程數(shù)量
  • 各種線程安全的容器赢织,比如最常見的ConcurrentHashMap、有序的ConcunrrentSkipListMap馍盟,或者通過類似快照機制于置,實現(xiàn)線程安全的動態(tài)數(shù)組CopyOnWriteArrayList等。
  • 各種并發(fā)隊列實現(xiàn)贞岭,如各種BlockedQueue實現(xiàn)八毯,比較典型的ArrayBlockingQueue、 SynchorousQueue或針對特定場景的PriorityBlockingQueue等瞄桨。
  • 強大的Executor框架话速,可以創(chuàng)建各種不同類型的線程池,調(diào)度任務運行等芯侥,絕大部分情況下泊交,不再需要自己從頭實現(xiàn)線程池和任務調(diào)度器。

14.1 Map的并發(fā)容器

如果我們的應用側(cè)重于Map放入或者獲取的速度柱查,而不在乎順序廓俭,大多推薦使用ConcurrentHashMap,反之則使用ConcurrentSkipListMap唉工;如果我們需要對大量數(shù)據(jù)進行非常頻繁地修改研乒,ConcurrentSkipListMap也可能表現(xiàn)出優(yōu)勢。

在傳統(tǒng)的Map中淋硝,普通無順序場景選擇HashMap雹熬,有順序場景則可以選擇類似TreeMap等,但是為什么并發(fā)容器里面沒有ConcurrentTreeMap呢谣膳?

這是因為TreeMap要實現(xiàn)高效的線程安全是非常困難的竿报,它的實現(xiàn)基于復雜的紅黑樹。為保證訪問效率参歹,當我們插入或刪除節(jié)點時仰楚,會移動節(jié)點進行平衡操作,這導致在并發(fā)場景中難以進行合理粒度的同步。而SkipList結構則要相對簡單很多僧界,通過層次結構提高訪問速度侨嘀,雖然不夠緊湊,空間使用有一定提高(O(nlogn))捂襟,但是在增刪元素時線程安全的開銷要好很多咬腕。

15、并發(fā)包中的ConcurrentLinkedQueue和LinkedBlockingQueue有什么區(qū)別葬荷?

涨共。。宠漩。

LinkedList是Deque

java.util.concurrent包提供的容器(Queue举反、List、Set)扒吁、Map火鼻,從命名上可以大概區(qū)分為Concurrent*、CopyOnWrite和Blocking等三類雕崩。

  • Concurrent*:lock-free機制(無鎖機制魁索,一般采用CAS)
  • CopyOnWrite*:采用空間換取
  • Blocking*:基于鎖(讀鎖、寫鎖)

16盼铁、Executor框架設計粗蔚?

image

Executor是一個基礎的接口,其初衷是將任務提交和任務執(zhí)行細節(jié)解耦饶火,它提供了唯一方法鹏控。

void execute(Runnable command);

[。趁窃。牧挣。]
(
file:///Volumes/%E9%91%AB%E5%93%A5%E6%A3%92%E6%A3%92%E5%93%92%E7%9A%84%E7%A7%BB%E5%8A%A8%E7%A1%AC%E7%9B%98/%E6%95%99%E7%A8%8B/Java%E6%A0%B8%E5%BF%83%E6%8A%80%E6%9C%AF36%E8%AE%B2/%E7%AC%AC21%E8%AE%B2.Java%E5%B9%B6%E5%8F%91%E7%B1%BB%E5%BA%93%E6%8F%90%E4%BE%9B%E7%9A%84%E7%BA%BF%E7%A8%8B%E6%B1%A0%E6%9C%89%E5%93%AA%E5%87%A0%E7%A7%8D%EF%BC%9F%20%E5%88%86%E5%88%AB%E6%9C%89%E4%BB%80%E4%B9%88%E7%89%B9%E7%82%B9%EF%BC%9F.html)

17、AQS

[醒陆。瀑构。。](

file:///Volumes/%E9%91%AB%E5%93%A5%E6%A3%92%E6%A3%92%E5%93%92%E7%9A%84%E7%A7%BB%E5%8A%A8%E7%A1%AC%E7%9B%98/%E6%95%99%E7%A8%8B/Java%E6%A0%B8%E5%BF%83%E6%8A%80%E6%9C%AF36%E8%AE%B2/%E7%AC%AC22%E8%AE%B2.AtomicInteger%E5%BA%95%E5%B1%82%E5%AE%9E%E7%8E%B0%E5%8E%9F%E7%90%86%E6%98%AF%E4%BB%80%E4%B9%88%EF%BC%9F%E5%A6%82%E4%BD%95%E5%9C%A8%E8%87%AA%E5%B7%B1%E7%9A%84%E4%BA%A7%E5%93%81%E4%BB%A3%E7%A0%81%E4%B8%AD%E5%BA%94%E7%94%A8CAS%E6%93%8D%E4%BD%9C%EF%BC%9F.html)

23~29刨摩、33~35寺晌、38日后再補

18、Java程序運行在Docker等容器環(huán)境有哪些新問題澡刹?

Docker中的內(nèi)存呻征、CPU等資源限制是通過CGroup(Control Group)實現(xiàn)的,早期的JDK版本(8u131之前)并不能識別這些限制罢浇,進而會導致一些基礎問題:

  • 如果未配置合適的JVM堆和元數(shù)據(jù)區(qū)陆赋、直接內(nèi)存等參數(shù)沐祷,Java就有可能試圖使用超過容器限制的內(nèi)存,最終被容器OOM kill攒岛,或者自身發(fā)生OOM赖临。
  • 錯誤判斷了可獲取的CPU資源,例如灾锯,Docker限制了CPU的核數(shù)兢榨,JVM就可能設置不合適的GC并行線程數(shù)等。

從應用打包顺饮、發(fā)布等角度出發(fā)吵聪,JDK自身就比較大,生成的鏡像就更為臃腫兼雄,當我們的鏡像非常多的時候吟逝,鏡像的存儲等開銷就比較明顯了。

18.1 Docker與虛擬機的區(qū)別赦肋?

Docker并不是一種完全的虛擬化技術澎办,而更是一種輕量級的隔離技術。

Docker與虛擬機的區(qū)別

從技術角度金砍,基于namespace,Docker為每個容器提供了單獨的命名空間麦锯,對網(wǎng)絡恕稠、PID、用戶扶欣、IPC通信鹅巍、文件系統(tǒng)掛載點等實現(xiàn)了隔離。對于CPU料祠、內(nèi)存骆捧、磁盤IO等計算資源,則是通過CGroup進行管理髓绽。

Docker僅在類似Linux內(nèi)核之上實現(xiàn)了有限的隔離和虛擬化敛苇,并不是像傳統(tǒng)虛擬化軟件那樣,獨立運行一個新的操作系統(tǒng)顺呕。

容器雖然省略了虛擬操作系統(tǒng)的開銷枫攀,實現(xiàn)了輕量級的目標,但也帶來了額外復雜性株茶,它的限制對于應用不是透明的来涨,需要用戶理解Docker的新行為。所以启盛,有專家曾經(jīng)說過蹦掐,“幸運的是Docker沒有完全隱藏底層信息技羔,但是不幸的也是Docker沒有隱藏底層信息!”

對于Java平臺來說卧抗,這些未隱藏的底層信息帶來了很多意外的困難藤滥,主要體現(xiàn)在幾個方面:

  1. 容器環(huán)境對于計算資源的管理方式是全新的,CGroup作為相對比較新的技術颗味,歷史版本的Java顯然并不能自然地理解相應的資源限制超陆。
  2. namespace對于容器內(nèi)的應用細節(jié)增加了一些微妙的差異,比如jcmd浦马、jstack等工具會依賴于“/proc//”下面提供的部分信息,但是Docker的設計改變了這部分信息的原有結構磺陡,我們需要對原有工具進行修改以適應這種變化。

18.2 從JVM運行機制的角度庆杜,為什么這些“溝通障礙”會導致OOM等問題呢?

JVM會在啟動時設置默認參數(shù)厢洞;JVM會大概根據(jù)檢測到的內(nèi)存大小,設置最初啟動時的堆大小為系統(tǒng)內(nèi)存的1/64琴许;并將堆最大值疑枯,設置為系統(tǒng)內(nèi)存的1/4。而JVM檢測到系統(tǒng)的CPU核數(shù),則直接影響到了Parallel GC的并行線程數(shù)目和JIT complier線程數(shù)目,甚至是我們應用中ForkJoinPool等機制的并行等級苦囱。

這些默認參數(shù)嗅绸,是根據(jù)通用場景選擇的初始值。但是由于容器環(huán)境的差異撕彤,Java的判斷很可能是基于錯誤信息而做出的鱼鸠。這就類似,我以為我住的是整棟別墅羹铅,實際上卻只有一個房間是給我住的蚀狰。

19、你了解Java應用開發(fā)中的注入攻擊嗎睦裳?

1造锅、SQL注入攻擊

Select * from use_info where username = “input_usr_name” and password = “input_pwd”

-- 但是,用戶如果輸入的是【"or ""="】廉邑,就GG了
-- 或者【;delete xxx】就更完了

2哥蔚、操作系統(tǒng)命令注入

// 用戶輸入【123;rm -rf /*】也gg了
Runtime.exec("ls -la" + input_file_name);

3、XSS攻擊

19.1 MITM攻擊

在密碼學和計算機安全領域中是指攻擊者與通訊的兩端分別創(chuàng)建獨立的聯(lián)系蛛蒙,并交換其所收到的數(shù)據(jù)糙箍,使通訊的兩端認為他們正在通過一個私密的連接與對方直接對話,但事實上整個會話都被攻擊者完全控制牵祟。在中間人攻擊中深夯,攻擊者可以攔截通訊雙方的通話并插入新的內(nèi)容。 —— wiki

20诺苹、如何寫出安全的Java代碼咕晋?

以拒絕服務(DoS)攻擊為例,有人也稱其為“洪水攻擊”收奔。最常見的表現(xiàn)是掌呜,利用大量機器發(fā)送請求,將目標網(wǎng)站的帶寬或者其他資源耗盡坪哄,導致其無法響應正常用戶的請求质蕉。

  • 哈希碰撞攻擊,就是個典型的例子翩肌,對方可以輕易消耗系統(tǒng)有限的CPU和線程資源模暗。從這個角度思考,類似加密念祭、解密兑宇、圖形處理等計算密集型任務,都要防范被惡意濫用粱坤,以免攻擊者通過直接調(diào)用或者間接觸發(fā)方式顾孽,消耗系統(tǒng)資源祝钢。

  • 利用Java構建類似上傳文件或者其他接受輸入的服務,需要對消耗系統(tǒng)內(nèi)存或存儲的上限有所控制若厚,因為我們不能將系統(tǒng)安全依賴于用戶的合理使用拦英。其中特別注意的是涉及解壓縮功能時,就需要防范Zip bomb等特定攻擊测秸。

  • Java程序中需要明確釋放的資源有很多種疤估,比如文件描述符、數(shù)據(jù)庫連接霎冯,甚至是再入鎖铃拇,任何情況下都應該保證資源釋放成功,否則即使平時能夠正常運行沈撞,也可能被攻擊者利用而耗盡某類資源慷荔,這也算是可能的DoS攻擊來源。

// a, b, c都是int類型的數(shù)值
// 這段代碼的結果可能是錯誤的缠俺,當a超大的話显晶,結果就是錯誤的。
if (a + b < c) {
    // …
}

// 應該這樣寫
if (a < c - b) {

}

21壹士、

file:///Volumes/%E9%91%AB%E5%93%A5%E6%A3%92%E6%A3%92%E5%93%92%E7%9A%84%E7%A7%BB%E5%8A%A8%E7%A1%AC%E7%9B%98/%E6%95%99%E7%A8%8B/Java%E6%A0%B8%E5%BF%83%E6%8A%80%E6%9C%AF36%E8%AE%B2/%E7%AC%AC39%E8%AE%B2.%E8%B0%88%E8%B0%88%E5%B8%B8%E7%94%A8%E7%9A%84%E5%88%86%E5%B8%83%E5%BC%8FID%E7%9A%84%E8%AE%BE%E8%AE%A1%E6%96%B9%E6%A1%88%EF%BC%9FSnowflake%E6%98%AF%E5%90%A6%E5%8F%97%E5%86%AC%E4%BB%A4%E6%97%B6%E5%88%87%E6%8D%A2%E5%BD%B1%E5%93%8D%EF%BC%9F.html

22磷雇、Spring Bean的生命周期和作用域?

Spring Bean生命周期比較復雜躏救,可以分為創(chuàng)建和銷毀兩個過程唯笙。

創(chuàng)建:

  1. BDRP、BDP..對bd進行處理
  2. 實例化Bean對象盒使。
  3. 設置bean的屬性
  4. 如果我們通過各種Aware接口聲明了依賴關系崩掘,則會注入Bean對容器基礎設施層面的依賴。具體包括BeanNameAware少办、BeanFactoryAware和ApplicationContextAware苞慢,分別會注入Bean ID、Bean Factory或者ApplicationContext凡泣。
  5. 調(diào)用BeanPostProcessor的前置初始化方法postProcessBeforeInitialization。
  6. 如果實現(xiàn)了InitializingBean接口皮假,則會調(diào)用afterPropertiesSet方法鞋拟。
  7. 調(diào)用Bean自身定義的init方法。
  8. 調(diào)用BeanPostProcessor的后置初始化方法postProcessAfterInitialization惹资。
  9. 創(chuàng)建過程完畢贺纲。
image

銷毀:

Spring Bean的銷毀過程會依次調(diào)用DisposableBean的destroy方法和Bean自身定制的destroy方法(@PreDestroy)。

作用域:

Spring Bean有五個作用域褪测,其中最基礎的有下面兩種:

  • Singleton猴誊,這是Spring的默認作用域潦刃,也就是為每個IOC容器創(chuàng)建唯一的一個Bean實例。
  • Prototype懈叹,針對每個getBean請求乖杠,容器都會單獨創(chuàng)建一個Bean實例。

從Bean的特點來看澄成,Prototype適合有狀態(tài)的Bean(有實例變量的對象胧洒,可以保存數(shù)據(jù),是非線程安全的)墨状,而Singleton則更適合無狀態(tài)的情況卫漫。另外,使用Prototype作用域需要經(jīng)過仔細思考肾砂,畢竟頻繁創(chuàng)建和銷毀Bean是有明顯開銷的列赎。

如果是Web容器,則支持另外三種作用域:

  • Request镐确,為每個HTTP請求創(chuàng)建單獨的Bean實例包吝。
  • Session,很顯然Bean實例的作用域是Session范圍辫塌。
  • GlobalSession漏策,用于Portlet容器,因為每個Portlet有單獨的Session臼氨,GlobalSession提供一個全局性的HTTP Session掺喻。

22.1 IOC、AOP

IOC:控制反轉(zhuǎn)

DI:依賴注入

AOP:面向切面編程

22.2 Spring AOP自身設計和實現(xiàn)的細節(jié)储矩?

為啥需要AOP感耙?

切面編程落實到軟件工程其實是為了更好地模塊化,而不僅僅是為了減少重復代碼持隧。通過AOP等機制即硼,我們可以把橫跨多個不同模塊的代碼抽離出來,讓模塊本身變得更加內(nèi)聚(模塊內(nèi)的代碼關系更加緊密)屡拨,進而業(yè)務開發(fā)者可以更加專注于業(yè)務邏輯本身只酥。從迭代能力上來看,我們可以通過切面的方式進行修改或者新增功能呀狼,這種能力不管是在問題診斷還是產(chǎn)品能力擴展中裂允,都非常有用。

Spring AOP的切入點和切入行為如何定義哥艇?

  • Aspect(聲明當前類包含AOP的切入點和切入行為)绝编,通常叫作方面,它是跨不同Java類層面的橫切性邏輯。在實現(xiàn)形式上十饥,既可以是XML文件中配置的普通類窟勃,也可以在類代碼中用“@Aspect”注解去聲明。在運行時逗堵,Spring框架會創(chuàng)建類似Advisor來指代它秉氧,其內(nèi)部會包括切入的時機(Pointcut)和切入的動作(Advice)。

  • Join Point砸捏,它是Aspect可以切入的特定點谬运,在Spring里面只有方法可以作為Join Point。

  • Advice垦藏,它定義了切面中能夠采取的動作梆暖。一般有:前置通知、后置通知掂骏、環(huán)繞通知轰驳、異常通知,發(fā)生的順序如下圖:

image
  • Pointcut弟灼,它負責具體定義Aspect被應用在哪些Join Point级解,可以通過指定具體的類名和方法名來實現(xiàn),或者也可以使用正則表達式來定義條件田绑。
4者關系

22.3 為啥要使用單例而不是直接使用靜態(tài)方法

https://www.cnblogs.com/seesea125/archive/2012/04/05/2433463.html

為了讓開發(fā)更加模式化勤哗、面向?qū)ο蠡?/p>

靜態(tài)方法:基于對象

單例模式的方法:面向?qū)ο?/p>

23、對比Java標準NIO類庫掩驱,你知道Netty是如何實現(xiàn)更高性能的嗎芒划?

單獨從性能角度,Netty在基礎的NIO等類庫之上進行了很多改進欧穴,例如:

  • 更加優(yōu)雅的Reactor模式實現(xiàn)民逼、靈活的線程模型、利用EventLoop等創(chuàng)新性的機制涮帘,可以非常高效地管理成百上千的Channel拼苍。

  • 充分利用了Java的Zero-Copy機制,并且從多種角度调缨,“斤斤計較”般的降低內(nèi)存分配和回收的開銷疮鲫。例如,使用池化的Direct Buffer(一塊在Java堆外分配的弦叶,可以在Java程序中訪問的內(nèi)存)等技術俊犯,在提高IO性能的同時,減少了對象的創(chuàng)建和銷毀湾蔓;利用反射等技術直接操縱SelectionKey瘫析,使用數(shù)組而不是Java容器等砌梆。

  • 使用更多本地代碼默责。例如贬循,直接利用JNI調(diào)用Open SSL等方式,獲得比Java內(nèi)建SSL引擎更好的性能桃序。

  • 通信協(xié)議杖虾、序列化等其他角度的優(yōu)化

總的來說媒熊,Netty并沒有Java核心類庫那些強烈的通用性奇适、跨平臺等各種負擔,針對性能等特定目標以及Linux等特定環(huán)境芦鳍,采取了一些極致的優(yōu)化手段嚷往。

23.1 Netty與Java自身的NIO框架相比有哪些不同呢?

對象的創(chuàng)建過程

假設有個名為Dog的類:

  1. 即使沒有顯式地使用static關鍵字柠衅,構造器實際上也是靜態(tài)方法皮仁。因此,當首次創(chuàng)建類型為Dog的對象時(構造器可以看成靜態(tài)方法)菲宴,或者Dog類的靜態(tài)方法/靜態(tài)域首次被訪問時贷祈,Java解釋器必須查找類路徑,以定位Dog.class文件喝峦。
  2. 然后載入Dog.class (這將創(chuàng)建一個Class對象)势誊,有關靜態(tài)初始化的所有動作都會執(zhí)行。因此谣蠢,靜態(tài)初始化只在Class對象首次加載的時候進行一次粟耻。
  3. 當用new Dog()創(chuàng)建對象的時候,首先將在堆上為Dog對象分配足夠的存儲空間漩怎。
  4. 這塊存儲空間會被清零勋颖,這就自動地將Dog對象中的所有基本類型數(shù)據(jù)都設置成了默認值(對數(shù)字來說就是0,對布爾型和字符型也相同)勋锤,而引用則被設置成了null饭玲。
  5. 執(zhí)行所有出現(xiàn)于字段定義處的初始化動作
  6. 執(zhí)行構造器叁执。這可能會牽涉到很多動作茄厘,尤其是涉及繼承的時候。
最后編輯于
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末谈宛,一起剝皮案震驚了整個濱河市次哈,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌吆录,老刑警劉巖窑滞,帶你破解...
    沈念sama閱讀 217,509評論 6 504
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異,居然都是意外死亡哀卫,警方通過查閱死者的電腦和手機巨坊,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,806評論 3 394
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來此改,“玉大人趾撵,你說我怎么就攤上這事」部校” “怎么了占调?”我有些...
    開封第一講書人閱讀 163,875評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長移剪。 經(jīng)常有香客問我究珊,道長,這世上最難降的妖魔是什么纵苛? 我笑而不...
    開封第一講書人閱讀 58,441評論 1 293
  • 正文 為了忘掉前任苦银,我火速辦了婚禮,結果婚禮上赶站,老公的妹妹穿的比我還像新娘陈瘦。我一直安慰自己努释,他們只是感情好羡鸥,可當我...
    茶點故事閱讀 67,488評論 6 392
  • 文/花漫 我一把揭開白布曾沈。 她就那樣靜靜地躺著,像睡著了一般烙博。 火紅的嫁衣襯著肌膚如雪瑟蜈。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,365評論 1 302
  • 那天渣窜,我揣著相機與錄音铺根,去河邊找鬼。 笑死乔宿,一個胖子當著我的面吹牛位迂,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播详瑞,決...
    沈念sama閱讀 40,190評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼掂林,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了坝橡?” 一聲冷哼從身側(cè)響起泻帮,我...
    開封第一講書人閱讀 39,062評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎计寇,沒想到半個月后锣杂,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體脂倦,經(jīng)...
    沈念sama閱讀 45,500評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,706評論 3 335
  • 正文 我和宋清朗相戀三年元莫,在試婚紗的時候發(fā)現(xiàn)自己被綠了狼讨。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 39,834評論 1 347
  • 序言:一個原本活蹦亂跳的男人離奇死亡柒竞,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出播聪,到底是詐尸還是另有隱情朽基,我是刑警寧澤,帶...
    沈念sama閱讀 35,559評論 5 345
  • 正文 年R本政府宣布离陶,位于F島的核電站稼虎,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏招刨。R本人自食惡果不足惜霎俩,卻給世界環(huán)境...
    茶點故事閱讀 41,167評論 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望沉眶。 院中可真熱鬧打却,春花似錦、人聲如沸谎倔。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,779評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽片习。三九已至捌肴,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間藕咏,已是汗流浹背状知。 一陣腳步聲響...
    開封第一講書人閱讀 32,912評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留孽查,地道東北人饥悴。 一個月前我還...
    沈念sama閱讀 47,958評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像盲再,于是被迫代替她去往敵國和親铺坞。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,779評論 2 354