用Java這麼多年,這些祕密你知道嗎?

摘要:java

若是您是Java開發人員,那麼這些問題可能會讓您在某個時刻頭痛不已。繼續閱讀以瞭解如何處理這5個棘手的祕密。編程

Java是一個擁有悠久歷史的大型語言。在二十多年的時間裏,語言中蘊含着許多功能,其中一些功能對其改進有很大貢獻,另外一些功能卻極大地簡化了它。在後一種狀況下,這些功能中的不少功能都滯留在這裏,而且爲了向後兼容而留在這裏。在本系列的前一篇文章中,咱們探討了該語言的一些奇怪特徵,這些特徵在平常實踐中可能不會使用。在這篇文章中,咱們將介紹一些有用但經常被忽略的語言功能,以及一些有趣的特性,若是忽略,可能會引發嚴重的騷動。緩存

對於這些祕密中的每個,重要的是要注意它們中的一些,例如數字下劃線和緩存自動裝箱在應用程序中多是有用的,可是其餘的(如單個Java文件中的多個類)已被降級到backburner一個緣由。所以,僅僅由於語言中存在的功能並不 意味着它應該被使用(即便它不被棄用)。相反,判斷應該用於什麼時候應用這些隱藏功能。在研究好的,壞的和醜陋的以前,咱們先從語言的特性開始,若是忽略這些語言會致使一些嚴重的錯誤:指令從新排序。安全

1.指令能夠從新排序性能優化

因爲多處理芯片數十年前進入計算環境,多線程已成爲大多數不平凡的Java應用程序中不可或缺的部分。不管是在多線程超文本傳輸協議(HTTP)服務器仍是大數據應用程序中,線程都容許使用強大的中央處理單元(CPU)提供的處理預算同時執行工做。儘管線程是CPU使用率的重要組成部分,但它們可能會很是棘手,並且它們的錯誤使用可能會在應用程序中注入一些不合適且難以調試的錯誤。服務器

例如,若是咱們建立一個打印變量值的單線程應用程序,咱們能夠假設咱們在源代碼中提供的代碼行(更準確地說,每條指令)都是按順序執行的,從第一行開始並以最後一行結束。遵循這個假設,下面的代碼片斷將致使4 打印到標準輸出中應該不使人驚訝 :多線程

雖然它可能會出現的值 1 被分配給變量 x 第一和而後的值 3 被分配到 y,這可能不老是這樣的狀況。仔細檢查後,前兩行的順序不會影響應用程序的輸出:若是 y 首先分配哪一個位置,那麼 x系統的行爲不會改變。咱們仍然會看到 4 打印到標準輸出。使用這種觀察,編譯器能夠 根據須要安全地從新排序這兩個指令,由於它們的從新排序不會改變系統的總體行爲。當咱們編譯咱們的代碼時,編譯器會這樣作:只要它不改變系統的可觀察行爲,就能夠自由地對上述指令進行從新排序。架構

儘管對上述命令從新排序彷佛是徒勞無功,但在許多狀況下,從新排序可能容許編譯器進行一些很是明顯的性能優化。例如,假設咱們有下面的代碼片斷,其中咱們 x 和 y 變量在交錯的方式遞增兩次:併發

8不管編譯器執行的任何優化如何,都應打印此片斷 ,但請留下上述說明以便進行有效的優化。若是編譯器將增量從新排列爲非交錯方式,則能夠徹底刪除它們:編程語言

實際上,編譯器可能會更進一步,並簡單地內聯 print語句中的值, x 並 y刪除將每一個值存儲在變量中的開銷,但爲了演示的目的,只需說從新排序指令就能夠了編譯器會對性能作出一些重大改進。在單線程環境中,這種從新排序對應用程序的可觀察行爲沒有任何影響,由於當前線程是惟一一個能夠看到x 和 y,但在多線程環境中,這是否是 這種狀況。因爲有一個連貫的內存視圖,一般是不須要的,所以須要CPU的大量開銷(請參閱緩存一致性),CPU一般會放棄一致性,除非被指示這樣作。一樣,Java編譯器能夠自由地優化代碼,以便重排序能夠發生,即便多線程讀取或寫入相同的數據,除非另有指示。

在Java中,這強加了指令的偏序排序,由發生前 關係表示,其中hb(x,y) 表示指令x在y以前發生。在這種狀況下,發生 -事實上並不意味着沒有發生指令的從新排序,而是說,x在y以前達到了一致狀態 (即 在執行y以前執行了全部對x的修改而且可見)。例如,在上面的代碼片斷中,變量 而且 必須達到它們的終端值(在 和上執行的全部計算的結果) xy x y)在執行印刷聲明以前。在單線程和多線程環境中,每一個線程 中的全部指令都是以先發生的 方式執行的,所以當數據不是 從一個線程發佈到另外一個線程時,咱們從不會遇到從新排序問題。在發佈發佈(例如在兩個線程之間共享數據)時,可能會出現很是潛在的問題。

例如,若是咱們執行如下代碼(來自Java Concurrency in Practice,第340頁),那麼對具備併發經驗的開發人員來講,線程交織可能會致使(0,1),(1,0)或(1,1)打印到標準輸出; 可是,(0,0)因爲從新排序也不是不可能的。

因爲每一個線程中的指令就不會有以前發生 彼此之間的關係,他們能夠自由地從新排序。例如,線程 one 可能在執行x = b 以前 a = 1 和以後實際執行 ,線程 other 可能會在y = a 以前 執行 b = 1 (由於在每一個線程的上下文中,這些指令的執行順序可有可無)。若是這兩種再排序發生,結果多是(0,0)。請注意,這不一樣於交錯,其中線程搶佔和線程執行順序會影響應用程序的輸出。交錯只能致使(0,1),(1,0)或(1,1)被打印:(0,0)是從新排序的惟一結果。

爲了強制 兩個線程之間發生以前發生的關係,咱們須要強制同步。例如,下面的代碼片斷刪除了致使(0,0)結果的從新排序的可能性,由於它 在兩個線程之間施加了一個before-before關係。但請注意,(0,1)和(1,0)是此代碼段中惟一可能的兩種結果,具體取決於每一個線程的運行順序。例如,若是線程 one 首先啓動,結果將爲(0,1),但若是線程 other 先運行,結果將爲(1,0)。

通常來講,有幾種明確的方式來強加一種先發生的 關係,包括(從 包文檔中引用): java.util.concurrent

線程中的每一個動做都發生在該線程中的每一個動做以前,該動做稍後會按程序的順序進行。

監視器的解鎖(同步塊或方法退出)發生在相同監視器的每一個後續鎖定(同步塊或方法輸入)以前。而且由於發生以前的關係是可傳遞的,因此在解鎖以前的線程的全部動做發生 - 在任何監視的線程鎖定以後的全部動做以前。

在相同字段的每次後續讀取以前發生對volatile 字段的寫入 。寫入和讀取 字段具備與進入和退出監視器相似的內存一致性效果,但不須要互斥鎖定。volatile

在啓動線程中的任何操做以前,都會發生在線程上啓動的調用。

在 任何其餘線程從該線程上的鏈接成功返回以前,線程中的全部操做都會發生。

在以前發生 偏序關係是一個複雜的話題,但我只想說,交織不是能夠在併發程序致使錯誤偷偷摸摸惟一的困境。在任何狀況下,在兩個或多個線程之間共享數據或資源的狀況下,volatile必須使用某些同步機制(不管是synchronized,鎖定,原子變量等)來確保數據正確共享。有關更多信息,請參見Java語言規範(JLS)的第17.4.5節和實踐中的Java併發。

下劃線可用於數字

不管是在計算機仍是在紙筆數學中,大量的數字都很難讀懂。例如,試圖辨別1183548876845其實是「1萬億1,853億548萬876萬845」多是很是乏味的。值得慶幸的是,英語數學包括逗號分隔符,它容許一次將三位數字分組在一塊兒。例如,如今更明顯的是,1,183,548,876,845表明超過一萬億美圓的數字(經過計算逗號的數量)。

不幸的是,在Java中表示這麼大的數字每每是件麻煩事。例如,在程序中將這些大數字表示爲常量並不罕見,以下面的代碼片斷所示,該代碼片斷顯示了上面的數字:

雖然這足以實現咱們打印大量數據的目標,但不言而喻,咱們創造的常量缺少美感。值得慶幸的是,自從Java開發工具包(Java Development Kit,JDK)7以來,Java已經引入了一個與逗號分隔符相同的內容:下劃線。能夠按照與逗號徹底相同的方式使用下劃線,將數字組分開以提升大數值的可讀性。例如,咱們能夠重寫上面的程序,以下所示:

一樣,因爲逗號使得咱們的原始數學值更易於閱讀,如今咱們能夠更容易地讀取Java程序中的大數值。下劃線也可用於浮點值,如如下常數所示:

還應該注意的是,下劃線能夠放置在一個數字中的任意點(不只僅是分隔三個數字的組),只要它不是前綴,後綴,與浮點值中的小數點相鄰,或者與x 十六進制值相鄰 。例如,如下全部內容都是Java中的無效數字:

儘管這種用於分隔數字的技術不該該被過分使用,理想狀況下,它只能以與英語數學中的逗號相同的方式使用,或者在小數位後以浮點值分隔三組數字 - 它能夠幫助辨別之前不可讀的數字。有關數字下劃線的更多信息,請參閱Oracle的數字文字文檔中的Underscore。

3. Autoboxed整數緩存

因爲原始值不能用做對象引用和做爲正式泛型類型參數,所以Java引入了原始值的盒裝對象的概念。這些裝箱值重要的是包裝原始值 - 從基元建立對象 - 容許它們用做對象引用和正式泛型類型。例如,咱們能夠按如下方式填充一個整數值:

實際上,原始int 500 被轉換爲一個類型的對象 Integer 並存儲在其中 myInt。該處理稱爲自動裝箱,由於自動執行轉換以將原始整數值 500 轉換爲類型的對象Integer。實際上,這種轉換至關於如下內容(請參閱自動裝箱和取消裝箱以獲取更多關於原始代碼段和下面代碼段等效性的信息):

既然 myInt 是一個類型的對象Integer,咱們會指望將它的相等性與Integer 包含500 使用 == 操做符的另外一個對象 進行比較 應該會致使false,由於兩個對象不是同一個對象(== 操做符的標準含義 ); 可是調用 equals 這兩個對象應該會致使true,由於這兩個 Integer 對象是表明相同整數(即, )的值對象500:

此時,自動裝箱操做徹底按照咱們預期任何價值對象的行爲。若是咱們用較小的數字嘗試這種狀況會發生什麼?例如,25:

使人驚訝的是,若是咱們嘗試這樣作 25,咱們Integer 能夠經過identity(==)和value來看到這兩個 對象是相等的。這意味着這兩個 Integer 對象其實是同一個 對象。這種奇怪的行爲實際上不是一個疏忽或錯誤,而是一個有意識的決定,如第5.1.7節所述。的JLS。因爲許多自動裝箱操做都是在小數字(下127)下執行的,所以JLS指定 緩存了 和 Integer 之間的值 (包括)。這反映在包含此緩存的JDK 9源代碼中 :-128127Integer.valueOf

若是咱們檢查源代碼IntegerCache,咱們收集一些很是有趣的信息:

雖然這段代碼看起來很複雜,但實際上很簡單。根據JLS的第5.1.7節,緩存Integer 值的包含下限 始終設置爲-128,但包含的上限可由Java虛擬機(JVM)配置。默認狀況下,上限被設定爲 (根據JLS 5.1.7),但它能夠被配置成任何數量的更大的 比 大於最大整數值或更小。理論上,咱們能夠設置上限。127 127500

在實踐中,咱們可使用java.lang.Integer.IntegerCache.high VM屬性完成此操做 。例如,若是咱們基於500 with 的自動裝箱從新運行原始程序 -Djava.lang.Integer.IntegerCache.high=500,則程序行爲會發生變化:

通常狀況下,除非有嚴重的性能需求,不然不該將VM調整爲更高的 Integer 緩存值。此外,的盒裝形式 boolean,char,short,和 long (即Boolean,Char, Short和 Long)也被緩存,但一般都不會 有VM設置來改變它們的緩存上限。相反,這些界限一般是固定的。例如, Short.valueOf 在JDK 9中定義以下:

有關自動裝箱和緩存對話的更多信息,請參閱JLS的5.1.7節,以及 爲何128 == 128返回false但127 == 127在轉換爲Integer包裝時返回true?

4. Java文件能夠包含多個非嵌套類

一個廣泛接受的規則是 .java 文件必須只包含一個非嵌套類,而且該類的名稱必須與該文件的名稱相匹配。例如, Foo.java 只能包含一個名爲的非嵌套類Foo。雖然這是一項重要的作法和公認的慣例,但這並不是徹底正確。特別是,它實際上做爲Java編譯器的實現決策留下,無論是否強制限制文件的公共類必須與文件名相匹配。根據JLS第7.6節的規定:

在實踐中,大多數編譯器實現強制執行此限制,但在此定義中也有一個限定條件:該類型被聲明爲public。所以,經過嚴格的定義,這容許Java文件包含多個類,只要最多一個類是公共的。換句話說,實際上,全部的Java編譯器都強制限制頂級公共類必須匹配文件的名稱(不考慮 .java 擴展名),這限制了Java文件擁有多個公共頂級類(由於只有一個這些類能夠匹配文件的名稱)。因爲此語句僅限於公共類,因此只要最多隻有一個類是公共的,就能夠將多個類放置在Java源代碼文件中。

例如,Foo.java即便它包含多個類(但只有一個類是public類,而且公共類與該文件的名稱相匹配),如下文件(named )仍然有效:

若是咱們執行這個文件,咱們會看到10 打印到標準輸出的值 ,這代表咱們能夠實例化並與Bar 類(第二個但在咱們的Foo.java 文件中非公共類 )進行交互, 就像咱們任何其餘類同樣。咱們也能夠Bar 從另外一個Java文件(Baz.java)中與類進行交互, 只要它包含在同一個包中,由於 Bar 類是包私有的。所以,如下 Baz.java 文件打印 20 到標準輸出:

儘管Java文件中可能有多個類,但這不是 一個好主意。在每一個Java文件中只有一個類是常見的約定,違反這個約定會給其餘開發人員讀取文件帶來一些困難和挫折。若是文件中須要多個類,則應使用嵌套類。例如,咱們能夠Foo.java 經過嵌套Bar 類輕鬆地將文件減小 到單個頂級 類(由於Bar 類不依賴於特定的實例,所以使用靜態嵌套 Foo):

通常而言,應避免單個Java文件中的多個非嵌套類。有關更多信息,請參閱 Java文件是否能夠有多個類?

5. StringBuilder用於字符串鏈接

字符串鏈接是幾乎全部編程語言的常見部分,容許將多個字符串(或對象和不一樣類型的基元)合併爲一個字符串。Java中字符串鏈接複雜化的一個警告是 不可變性Strings。這意味着咱們不能簡單地建立一個 String 實例並不斷追加到它。相反,每一個附加產生一個新的 String 對象。舉例來講,若是咱們看看 concat(String) 米的ethod String,咱們看到一個新的 String 實例製做:

若是將這種技術用於字符串鏈接,String 則會產生大量中間 實例。例如,如下兩行在功能上是等同的-第二行生成兩個 String 實例只是爲了執行級聯:

雖然這可能看起來像是一個很小的代價來支付字符串鏈接,但這種技術在大規模使用時會變得難以維繫。例如,如下內容會浪費地建立1,000個 String 永遠不會使用的String 實例(建立1,001個 實例並僅使用最後一個實例):

爲了減小String 爲鏈接建立的浪費實例 的數量, JLS的第15.18.1節提供了有關如何實現字符串鏈接的強烈建議:

a不是建立中間String 實例,而是將 a StringBuilder 用做緩衝區,從而容許添加無數個 String 值直到String 須要結果爲止 。這確保只String 建立一個 實例:結果。此外,StringBuilder 建立了一個 實例,可是這種組合開銷要比String 建立的個別實例要少得多 。例如,咱們上面寫的循環串聯在語義上等同於如下內容:

而不是建立1,000個浪費的 String 實例,只 建立一個 StringBuilder 和一個 String實例。儘管咱們能夠手動編寫上面的代碼片斷而不是使用串聯(經過 += 運算符),但二者都是等價的,而且沒有性能增益。在許多狀況下,使用字符串鏈接而不是明確的 StringBuilder,在語法上更容易,所以是首選。無論選擇如何,重要的是要知道Java編譯器會盡量提升性能,所以,試圖過早地優化代碼(例如經過使用顯式 StringBuilder 實例)可能會以犧牲可讀性爲代價提供不多的收益。

結論

Java是具備歷史傳奇的大型語言。多年來,這門語言有數不清的增長,可能還有不少錯誤的增長。這兩個因素的結合致使了語言的一些很是奇特的特徵:一些好,一些壞。其中一些方面(如數字下劃線,緩存自動裝箱和字符串級聯優化)對於任何Java開發人員來講都是很重要的,而單個Java文件中諸如類的多樣性等功能已被降級到已棄用的架構。其餘的,好比不一樣步指令的從新排序,若是處理不當,可能會致使一些很是繁瑣的調試。

在此我向你們推薦一個架構學習交流羣。交流學習羣號: 744642380, 裏面會分享一些資深架構師錄製的視頻錄像:有Spring,MyBatis,Netty源碼分析,高併發、高性能、分佈式、微服務架構的原理,JVM性能優化、分佈式架構等這些成爲架構師必備的知識體系。還能領取免費的學習資源

相關文章
相關標籤/搜索