面對上面的問題,你該怎麼回答?java
int 是咱們常說的整形數字,是 Java 的 8 個原始數據類型(Primitive Types,boolean、byte、short、char、int、float、double、long)之一。Java 語言雖然號稱一切都是對象,但原始數據類型是例外。程序員
Integer 是 int 對應的包裝類,它有一個 int 類型的字段存儲數據,而且提供了基本操做,好比數學運算、int 和字符串之間轉換等。在 Java 5 中,引入了自動裝箱和自動拆箱功能(boxing/unboxing),Java 能夠根據上下文,自動進行轉換,極大地簡化了相關編程。面試
關於 Integer 的值緩存,這涉及 Java 5 中另外一個改進。構建 Integer 對象的傳統方式是直接調用構造器,直接 new 一個對象。可是根據實踐,咱們發現大部分數據操做都是集中在有限的、較小的數值範圍,於是,在 Java 5 中新增了靜態工廠方法 valueOf,在調用它的時候會利用一個緩存機制,帶來了明顯的性能改進。按照 Javadoc,這個值默認緩存是 -128 到 127 之間。編程
這個問題涵蓋了 Java 裏的兩個基礎要素:原始數據類型、包裝類。談到這裏,就能夠很是天然地擴展到自動裝箱、自動拆箱機制,進而考察封裝類的一些設計和實踐。坦白說,理解基本原理和用法已經足夠平常工做需求了,可是要落實到具體場景,仍是有不少問題須要仔細思考才能肯定。數組
咱們能夠結合其餘方面,來考察面試者的知識掌握程度和思考邏輯,好比:緩存
彷佛有太多內容能夠探討,咱們一塊兒來分析一下。安全
什麼是語法糖?能夠簡單理解爲 Java 平臺爲咱們自動進行了一些轉換,保證不一樣的寫法在運行時等價,它們發生在編譯階段,也就是生成的字節碼是一致的。
像前面提到的整數,javac 替咱們自動把裝箱轉換爲 Integer.valueOf(),把拆箱替換爲Integer.intValue(),這彷佛這也順道回答了另外一個問題,既然調用的是 Integer.valueOf,天然可以獲得緩存的好處啊。性能優化
如何程序化的驗證上面的結論呢?併發
你能夠寫一段簡單的程序包含下面兩句代碼,而後反編譯一下。固然,這是一種從表現倒推的方法,大多數狀況下,咱們仍是直接參考規範文檔會更加可靠,畢竟軟件承諾的是遵循規範,而不是保持當前行爲。jvm
Integer integer = 1; int unboxing = integer ++; |
反編譯輸出:
1: invokestatic #2 // Method java/lang/Integer.valueOf:(I)Ljava/lang/Integer; 8: invokevirtual #3 // Method java/lang/Integer.intValue:()I |
這種緩存機制並非只有 Integer 纔有,一樣存在於其餘的一些包裝類,好比:
自動裝箱 / 自動拆箱彷佛很酷,在編程實踐中,有什麼須要注意的嗎?
原則上,建議避免無心中的裝箱、拆箱行爲,尤爲是在性能敏感的場合,建立 10 萬個 Java 對象和 10 萬個整數的開銷可不是一個數量級的,不論是內存使用仍是處理速度,光是對象頭的空間佔用就已是數量級的差距了。
咱們其實能夠把這個觀點擴展開,使用原始數據類型、數組甚至本地代碼實現等,在性能極度敏感的場景每每具備比較大的優點,用其替換掉包裝類、動態數組(如 ArrayList)等能夠做爲性能優化的備選項。一些追求極致性能的產品或者類庫,會極力避免建立過多對象。固然,在大多數產品代碼裏,並無必要這麼作,仍是以開發效率優先。以咱們常常會使用到的計數器實現爲例,下面是一個常見的線程安全計數器實現。
class Counter { private final AtomicLong counter = new AtomicLong(); public void increase() { counter.incrementAndGet(); } } |
若是利用原始數據類型,能夠將其修改成
class CompactCounter { private volatile long counter; private static final AtomicLongFieldUpdater<CompactCounter> updater = AtomicLongFieldUpdater.newUpdater(CompactCounter.class,"counter"); public void increase() { updater.incrementAndGet(this); } } |
考察是否閱讀過、是否理解 JDK 源代碼多是部分面試官的關注點,這並不徹底是一種苛刻要求,閱讀並實踐高質量代碼也是程序員成長的必經之路,下面我來分析下 Integer 的源碼。總體看一下 Integer 的職責,它主要包括各類基礎的常量,好比最大值、最小值、位數等;前面提到的各類靜態工廠方法 valueOf();獲取環境變量數值的方法;各類轉換方法,好比轉換爲不一樣進制的字符串,如 8 進制,或者反過來的解析方法等。咱們進一步來看一些有意思的地方。首先,繼續深挖緩存,Integer 的緩存範圍雖然默認是 -128 到 127,可是在特別的應用場景,好比咱們明確知道應用會頻繁使用更大的數值,這時候應該怎麼辦呢?
緩存上限值實際是能夠根據須要調整的,JVM 提供了參數設置:
- XX:AutoBoxCacheMax=N
這些實現,都體如今java.lang.Integer源碼之中,並實如今 IntegerCache 的靜態初始化塊裏。
private static class IntegerCache { static final int low = -128; static final int high; static final Integer cache[]; static { // high value may be configured by property int h = 127; String integerCacheHighPropValue = sun.misc.VM.getSavedProperty("java.lang.Integer.IntegerCache.high"); if (integerCacheHighPropValue != null) { try { int i = parseInt(integerCacheHighPropValue); i = Math.max(i, 127); // Maximum array size is Integer.MAX_VALUE h = Math.min(i, Integer.MAX_VALUE - (-low) -1); } catch( NumberFormatException nfe) { // If the property cannot be parsed into an int, ignore it. } } high = h; cache = new Integer[(high - low) + 1]; int j = low; for(int k = 0; k < cache.length; k++) cache[k] = new Integer(j++); // range [-128, 127] must be interned (JLS7 5.1.7) assert IntegerCache.high >= 127; } private IntegerCache() {} } .... } |
第二,咱們在分析字符串的設計實現時,提到過字符串是不可變的,保證了基本的信息安全和併發編程中的線程安全。若是你去看包裝類裏存儲數值的成員變量「value」,你會發現,不論是Integer 仍是Boolean 等其餘基本類型封裝類,其value都被聲明爲「private final」,因此,它們一樣是不可變類型!
前面提到了線程安全設計,你有沒有想過,原始數據類型操做是否是線程安全的呢?
這裏可能存在着不一樣層面的問題:
「深刻java虛擬機」中提到,int等不大於32位的基本類型的操做都是原子操做,可是某些jvm對long和double類型的操做並非原子操做,這樣就會形成錯誤數據的出現。
錯誤數據出現的緣由是:
在32位的虛擬機中,對於long和double變量,把它們做爲2個原子性的32位值來對待,而不是一個原子性的64位值,
這樣將一個long型的值保存到內存的時候,多是2次32位的寫操做,
2個競爭線程想寫不一樣的值到內存的時候,可能致使內存中的值是不正確的結果。
一、寫入高位32位值(線程2)
二、寫入高位32位值(線程1)
三、寫入低位32位值(線程1)
四、寫入低位32位值(線程2)
這樣內存中的值變成線程1的高32位值和線程2的低32位值的組合,是個錯誤的值。
書中還提到,上面出現問題的long和double變量是沒有聲明爲volatile的變量。
volatile自己不保證獲取和設置操做的原子性,僅僅保持修改的可見性。
可是java內存模型保證聲明爲volatile的long和double變量的get和set操做是原子的。
jvm spec引用中的最後一段說到,jvm能夠很輕易的將變量操做變成原子性的,可是卻受到了當前硬件的約束,由於流行的微處理器仍是32bit居多,所以64bit的變量須要拆分紅兩次,但若是是64bit處理器就能知足64bit變量的原子性操做了。
針對這個問題,找到了網上的一個總結: