Java基礎面試知識點整理

Exception和Error有什么區(qū)別?

  • Exception 和 Error 都是繼承了 Throwable 類,在 Java 中只有 Throwable 類型的實例才可以被拋出(throw)或者捕獲(catch),它是異常處理機制的基本組成類型。
  • Exception 和 Error 體現(xiàn)了 Java 平臺設計者對不同異常情況的分類。Exception 是程序正常運行中,可以預料的意外情況,可能并且應該被捕獲,進行相應處理。
  • Error 是指在正常情況下,不大可能出現(xiàn)的情況,絕大部分的 Error 都會導致程序(比如 JVM 自身)處于非正常的、不可恢復狀態(tài)。既然是非正常情況,所以不便于也不需要捕獲,常見的比如 OutOfMemoryError 之類,都是 Error 的子類。
  • Exception 又分為可檢查(checked)異常和不檢查(unchecked)異常,可檢查異常在源代碼里必須顯式地進行捕獲處理,這是編譯期檢查的一部分。前面我介紹的不可查的 Error,是 Throwable 不是 Exception。
    不檢查異常就是所謂的運行時異常,類似 NullPointerException、ArrayIndexOutOfBoundsException 之類,通常是可以編碼避免的邏輯錯誤,具體根據(jù)需要來判斷是否需要捕獲,并不會在編譯期強制要求。

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。

強引用、軟引用、弱引用、幻象引用

不同的引用類型,主要體現(xiàn)的是對象不同的可達性(reachable)狀態(tài)和對垃圾收集的影響。

  • 所謂強引用(“Strong” Reference),就是我們最常見的普通對象引用,只要還有強引用指向一個對象,就能表明對象還“活著”,垃圾收集器不會碰這種對象。對于一個普通的對象,如果沒有其他的引用關系,只要超過了引用的作用域或者顯式地將相應(強)引用賦值為 null,就是可以被垃圾收集的了,當然具體回收時機還是要看垃圾收集策略。
  • 軟引用(SoftReference),是一種相對強引用弱化一些的引用,可以讓對象豁免一些垃圾收集,只有當 JVM 認為內存不足時,才會去試圖回收軟引用指向的對象。JVM 會確保在拋出 OutOfMemoryError 之前,清理軟引用指向的對象。軟引用通常用來實現(xiàn)內存敏感的緩存,如果還有空閑內存,就可以暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。
  • 弱引用(WeakReference)并不能使對象豁免垃圾收集,僅僅是提供一種訪問在弱引用狀態(tài)下對象的途徑。這就可以用來構建一種沒有特定約束的關系,比如,維護一種非強制性的映射關系,如果試圖獲取時對象還在,就使用它,否則重現(xiàn)實例化。它同樣是很多緩存實現(xiàn)的選擇。
  • 對于幻象引用,有時候也翻譯成虛引用,你不能通過它訪問對象?;孟笠脙H僅是提供了一種確保對象被 finalize 以后,做某些事情的機制,比如,通常用來做所謂的 Post-Mortem 清理機制,我在專欄上一講中介紹的 Java 平臺自身 Cleaner 機制等,也有人利用幻象引用監(jiān)控對象的創(chuàng)建和銷毀。

String、StringBuffer、StringBuilder

  • String 是 Java 語言非?;A和重要的類,提供了構造和管理字符串的各種基本邏輯。它是典型的 Immutable 類,被聲明成為 final class,所有屬性也都是 final 的。也由于它的不可變性,類似拼接、裁剪字符串等動作,都會產生新的 String 對象。由于字符串操作的普遍性,所以相關操作的效率往往對應用性能有明顯影響。

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

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

  • 補充
    String 是 Immutable 類的典型實現(xiàn),原生的保證了基礎線程安全,因為你無法對它內部數(shù)據(jù)進行任何修改,這種便利甚至體現(xiàn)在拷貝構造函數(shù)中,由于不可變,Immutable 對象在拷貝時不需要額外復制數(shù)據(jù)。
    我們再來看看 StringBuffer 實現(xiàn)的一些細節(jié),它的線程安全是通過把各種修改數(shù)據(jù)的方法都加上 synchronized 關鍵字實現(xiàn)的
    為了實現(xiàn)修改字符序列的目的,StringBuffer 和 StringBuilder 底層都是利用可修改的(char,JDK 9 以后是 byte)數(shù)組,二者都繼承了 AbstractStringBuilder,里面包含了基本操作,區(qū)別僅在于最終的方法是否加了 synchronized。
    構建時初始字符串長度加 16(這意味著,如果沒有構建對象時輸入最初的字符串,那么初始值就是 16)。

動態(tài)代理和反射

  • 反射機制是 Java 語言提供的一種基礎功能,賦予程序在運行時自?。╥ntrospect,官方用語)的能力。通過反射我們可以直接操作類或者對象,比如獲取某個對象的類定義,獲取類聲明的屬性和方法,調用方法或者構造對象,甚至可以運行時修改類定義。
  • 動態(tài)代理是一種方便運行時動態(tài)構建代理、動態(tài)處理代理方法調用的機制,很多場景都是利用類似機制做到的,比如用來包裝 RPC 調用、面向切面的編程(AOP)。
  • 反射提供的 AccessibleObject.setAccessible?(boolean flag)。它的子類也大都重寫了這個方法,這里的所謂 accessible 可以理解成修飾成員的 public、protected、private,這意味著我們可以在運行時修改成員訪問限制!
  • Spring AOP 支持兩種模式的動態(tài)代理,JDK Proxy 或者 cglib
  • JDK Proxy 的優(yōu)勢:
    1.最小化依賴關系,減少依賴意味著簡化開發(fā)和維護,JDK 本身的支持,可能比 cglib 更加可靠。
    2.平滑進行 JDK 版本升級,而字節(jié)碼類庫通常需要進行更新以保證在新版 Java 上能夠使用。
    3.代碼實現(xiàn)簡單。
  • 基于類似 cglib 框架的優(yōu)勢:
    1.有的時候調用目標可能不便實現(xiàn)額外接口,從某種角度看,限定調用者實現(xiàn)接口是有些侵入性的實踐,類似 cglib 動態(tài)代理就沒有這種限制。
    2.只操作我們關心的類,而不必為其他相關類增加工作量。
    3.高性能。

int和Integer有什么區(qū)別

  • int 是我們常說的整形數(shù)字,是 Java 的 8 個原始數(shù)據(jù)類型(Primitive Types,boolean、byte 、short、char、int、float、double、long)之一。Java 語言雖然號稱一切都是對象,但原始數(shù)據(jù)類型是例外。
  • Integer 是 int 對應的包裝類,它有一個 int 類型的字段存儲數(shù)據(jù),并且提供了基本操作,比如數(shù)學運算、int 和字符串之間轉換等。在 Java 5 中,引入了自動裝箱和自動拆箱功能(boxing/unboxing),Java 可以根據(jù)上下文,自動進行轉換,極大地簡化了相關編程。
  • 關于 Integer 的值緩存,這涉及 Java 5 中另一個改進。構建 Integer 對象的傳統(tǒng)方式是直接調用構造器,直接 new 一個對象。但是根據(jù)實踐,我們發(fā)現(xiàn)大部分數(shù)據(jù)操作都是集中在有限的、較小的數(shù)值范圍,因而,在 Java 5 中新增了靜態(tài)工廠方法 valueOf,在調用它的時候會利用一個緩存機制,帶來了明顯的性能改進。按照 Javadoc,這個值默認緩存是 -128 到 127 之間。
  • 原始類型線程安全
    前面提到了線程安全設計,你有沒有想過,原始數(shù)據(jù)類型操作是不是線程安全的呢?這里可能存在著不同層面的問題:
    1.原始數(shù)據(jù)類型的變量,顯然要使用并發(fā)相關手段,才能保證線程安全,這些我會在專欄后面的并發(fā)主題詳細介紹。如果有線程安全的計算需要,建議考慮使用類似 AtomicInteger、AtomicLong 這樣的線程安全類。
    2.特別的是,部分比較寬的數(shù)據(jù)類型,比如 float、double,甚至不能保證更新操作的原子性,可能出現(xiàn)程序讀取到只更新了一半數(shù)據(jù)位的數(shù)值!

Vector、ArrayList、LinkedList

1.Vector 是 線程安全的動態(tài)數(shù)組,內部是使用對象數(shù)組來保存數(shù)據(jù),可以根據(jù)需要自動的增加容量,當數(shù)組已滿時,會創(chuàng)建新的數(shù)組,并拷貝原有數(shù)組數(shù)據(jù)。
2.ArrayList 是動態(tài)數(shù)組實現(xiàn),不是線程安全的,所以性能要好很多。Vector 在擴容時會提高 1 倍,而 ArrayList 則是增加 50%。
3.LinkedList 顧名思義是 Java 提供的雙向鏈表,所以它不需要像上面兩種那樣調整容量,它也不是線程安全的。

  • TreeSet 支持自然順序訪問,但是添加、刪除、包含等操作要相對低效(log(n) 時間)。

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

  • LinkedHashSet,內部構建了一個記錄插入順序的雙向鏈表,因此提供了按照插入順序遍歷的能力,與此同時,也保證了常數(shù)時間的添加、刪除、包含等操作,這些操作性能略低于 HashSet,因為需要維護鏈表的開銷。
    在遍歷元素時,HashSet 性能受自身容量影響,所以初始化時,除非有必要,不然不要將其背后的 HashMap 容量設置過大。而對于 LinkedHashSet,由于其內部鏈表提供的方便,遍歷性能只和元素多少有關系。
    以上3個集合類,都不是線程安全的,可用Collections.synchronized() 方法在操作方法上都加上synchronized關鍵字保證線程安全。

  • Java 提供的默認排序算法,具體是什么排序方式以及設計思路

  1. 對于原始數(shù)據(jù)類型,目前使用的是所謂雙軸快速排序(Dual-Pivot QuickSort),是一種改進的快速排序算法,早期版本是相對傳統(tǒng)的快速排序,你可以閱讀源碼。

  2. 而對于對象數(shù)據(jù)類型,目前則是使用TimSort,思想上也是一種歸并和二分插入排序(binarySort)結合的優(yōu)化排序算法。TimSort 并不是 Java 的獨創(chuàng),簡單說它的思路是查找數(shù)據(jù)集中已經(jīng)排好序的分區(qū)(這里叫 run),然后合并這些分區(qū)來達到排序的目的。

Hashtable、HashMap、TreeMap

  • 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ù)鍵的自然順序來判斷。

  • HashMap 的性能表現(xiàn)非常依賴于哈希碼的有效性,請務必掌握 hashCode 和 equals 的一些基本約定,比如:
    1.equals 相等,hashCode 一定要相等。
    2.重寫了 hashCode 也要重寫 equals。
    3.hashCode 需要保持一致性,狀態(tài)改變返回的哈希值仍然要一致。
    4.equals 的對稱、反射、傳遞等特性。

  • LinkedHashMap 和 TreeMap 都可以保證某種順序,但二者是不同的。1.LinkedHashMap 通常提供的是遍歷順序符合插入順序,它的實現(xiàn)是通過為條目(鍵值對)維護一個雙向鏈表。
    2.對于 TreeMap,它的整體順序是由鍵的順序關系決定的,通過 Comparator 或 Comparable(自然順序)來決定。compareTo 的返回值需要和 equals 一致,否則就會出現(xiàn)模棱兩可情況。

HashMap 源碼分析
  1. 底層數(shù)據(jù)結構
    是數(shù)組(Node<K,V>[] table)和鏈表結合組成的復合結構,數(shù)組被分為一個個桶(bucket),通過哈希值決定了鍵值對在這個數(shù)組的尋址;哈希值相同的鍵值對,則以鏈表形式存儲,如果鏈表大小超過閾值(TREEIFY_THRESHOLD, 8),圖中的鏈表就會被改造為樹形結構。

  2. 代碼邏輯

  • resize 方法兼顧兩個職責,創(chuàng)建初始存儲表格,或者在容量不滿足需求的時候,進行擴容(resize)。
    依據(jù) resize 源碼,不考慮極端情況(容量理論最大極限由 MAXIMUM_CAPACITY 指定,數(shù)值為 1<<30,也就是 2 的 30 次方),我們可以歸納為:
  1. 門限值等于(負載因子 0.75)x(容量 16),如果構建 HashMap 的時候沒有指定它們,那么就是依據(jù)相應的默認常量值。指定了cap,先求大于cap最近的2次冪,初始容量為2次冪 * 0.75,不指定就為12。
  2. 門限通常是以倍數(shù)進行調整 (newThr = oldThr << 1),我前面提到,根據(jù) putVal 中的邏輯,當元素個數(shù)超過門限大小時,則調整 Map 大小。
  3. 擴容后,需要將老的數(shù)組中的元素重新放置到新的數(shù)組,這是擴容的一個主要開銷來源。
  • 在哈希表中的位置(數(shù)組 index)取決于下面的位運算:
    i = (n - 1) & hash
    因為有些數(shù)據(jù)計算出的哈希值差異主要在高位,而 HashMap 里的哈希尋址是忽略容量以上的高位的,那么這種處理就可以有效避免類似情況下的哈希碰撞。
static final int hash(Object kye) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>>16);
}
  • put:如果桶數(shù)組為空,先初始化桶數(shù)組;通過hash找到HashTable桶位置,鏈表為空就插為頭結點;不為空就遍歷桶鏈表,桶中不包含key與插入節(jié)點相同節(jié)點,尾插,并檢查是否鏈表節(jié)點個數(shù)大于8需要樹化;列表中存在key與插入節(jié)點相同的節(jié)點,替換value;最后檢查包含節(jié)點個數(shù)是否超過門限需要擴容。

3.容量、負載因子和樹化

  • 負載因子 * 容量 > 元素數(shù)量
  • 樹化 桶數(shù)組容量小于 MIN_TREEIFY_CAPACITY(64),優(yōu)先進行擴容桶數(shù)組而不是樹化桶鏈表。
  • 為什么 HashMap 要樹化呢?
    本質上這是個安全問題。因為在元素放置過程中,如果一個對象哈希沖突,都被放置到同一個桶里,則會形成一個鏈表,我們知道鏈表查詢是線性的,會嚴重影響存取的性能。
    而在現(xiàn)實世界,構造哈希沖突的數(shù)據(jù)并不是非常復雜的事情,惡意代碼就可以利用這些數(shù)據(jù)大量與服務器端交互,導致服務器端 CPU 大量占用,這就構成了哈希碰撞拒絕服務攻擊,國內一線互聯(lián)網(wǎng)公司就發(fā)生過類似攻擊事件。

ConcurrentHashMap(基于分離鎖)

Hashtable 本身比較低效,因為它的實現(xiàn)基本就是將 put、get、size 等各種方法加上“synchronized”。簡單來說,這就導致了所有并發(fā)操作都要競爭同一把鎖,一個線程在進行同步操作時,其他線程只能等待,大大降低了并發(fā)操作的效率。
看看下面的代碼片段,我們發(fā)現(xiàn)同步包裝器只是利用輸入 Map 構造了另一個同步版本,所有操作雖然不再聲明成為 synchronized 方法,但是還是利用了“this”作為互斥的 mutex,沒有真正意義上的改進!

private static class SynchronizedMap<K,V>
    implements Map<K,V>, Serializable {
    private final Map<K,V> m;     // Backing Map
    final Object      mutex;        // Object on which to synchronize
    // …
    public int size() {
        synchronized (mutex) {return m.size();}
    }
 // … 
}

所以,Hashtable 或者同步包裝版本,都只是適合在非高度并發(fā)的場景下。

  • 早期 ConcurrentHashMap,其實現(xiàn)是基于:
  1. 分離鎖,也就是將內部進行分段(Segment),里面則是 HashEntry 的數(shù)組,和 HashMap 類似,哈希相同的條目也是以鏈表形式存放。
  2. HashEntry 內部使用 volatile 的 value 字段來保證可見性,也利用了不可變對象的機制以改進利用 Unsafe 提供的底層能力,比如 volatile access,去直接完成部分操作,以最優(yōu)化性能,畢竟 Unsafe 中的很多操作都是 JVM intrinsic 優(yōu)化過的。
    你可以參考下面這個早期 ConcurrentHashMap 內部結構的示意圖,其核心是利用分段設計,在進行并發(fā)操作的時候,只需要鎖定相應段,這樣就有效避免了類似 Hashtable 整體同步的問題,大大提高了性能。
image.png

在構造的時候,Segment 的數(shù)量由所謂的 concurrentcyLevel 決定,默認是 16,也可以在相應構造函數(shù)直接指定。注意,Java 需要它是 2 的冪數(shù)值,如果輸入是類似 15 這種非冪值,會被自動調整到 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 調用方式,直接獲取相應的 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)在下面的內部方法中:

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),就直接返回,否則獲取鎖進行操作。

  • 在 Java 8 和之后的版本中,ConcurrentHashMap 發(fā)生了哪些變化呢?

  1. 總體結構上,它的內部存儲變得和我在專欄上一講介紹的 HashMap 結構非常相似,同樣是大的桶(bucket)數(shù)組,然后內部也是一個個所謂的鏈表結構(bin),同步的粒度要更細致一些。
  2. 其內部仍然有 Segment 定義,但僅僅是為了保證序列化時的兼容性而已,不再有任何結構上的用處。因為不再使用 Segment,初始化操作大大簡化,修改為 lazy-load 形式,這樣可以有效避免初始開銷,解決了老版本很多人抱怨的這一點。
  3. 數(shù)據(jù)存儲利用 volatile 來保證可見性。
  4. 使用 CAS 等操作,在特定場景進行無鎖并發(fā)操作。
  5. 使用 Unsafe、LongAdder 之類底層手段,進行極端情況的優(yōu)化。

JavaIO

首先,傳統(tǒng)的 java.io 包,它基于流模型實現(xiàn),提供了我們最熟知的一些 IO 功能,比如 File 抽象、輸入輸出流等。交互方式是同步、阻塞的方式,也就是說,在讀取輸入流或者寫入輸出流時,在讀、寫動作完成之前,線程會一直阻塞在那里,它們之間的調用是可靠的線性順序。
java.io 包的好處是代碼比較簡單、直觀,缺點則是 IO 效率和擴展性存在局限性,容易成為應用性能的瓶頸。
很多時候,人們也把 java.net 下面提供的部分網(wǎng)絡 API,比如 Socket、ServerSocket、HttpURLConnection 也歸類到同步阻塞 IO 類庫,因為網(wǎng)絡通信同樣是 IO 行為。
第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,可以構建多路復用的、同步非阻塞 IO 程序,同時提供了更接近操作系統(tǒng)底層的高性能數(shù)據(jù)操作方式。
第三,在 Java 7 中,NIO 有了進一步的改進,也就是 NIO 2,引入了異步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。異步 IO 操作基于事件和回調機制,可以簡單理解為,應用操作直接返回,而不會阻塞在那里,當后臺處理完成,操作系統(tǒng)會通知相應線程進行后續(xù)工作。

  • 基本概念
  1. 區(qū)分同步或異步(synchronous/asynchronous)。簡單來說,同步是一種可靠的有序運行機制,當我們進行同步操作時,后續(xù)的任務是等待當前調用返回,才會進行下一步;而異步則相反,其他任務不需要等待當前調用返回,通常依靠事件、回調等機制來實現(xiàn)任務間次序關系。
  2. 區(qū)分阻塞與非阻塞(blocking/non-blocking)。在進行阻塞操作時,當前線程會處于阻塞狀態(tài),無法從事其他任務,只有當條件就緒才能繼續(xù),比如 ServerSocket 新連接建立完畢,或數(shù)據(jù)讀取、寫入操作完成;而非阻塞則是不管 IO 操作是否結束,直接返回,相應操作在后臺繼續(xù)處理。
  • NIO 的主要組成部分
  1. Buffer,高效的數(shù)據(jù)容器,除了布爾類型,所有原始數(shù)據(jù)類型都有相應的 Buffer 實現(xiàn)。
  2. 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,反之亦然。
  3. Selector,是 NIO 實現(xiàn)多路復用的基礎,它提供了一種高效的機制,可以檢測到注冊在 Selector 上的多個 Channel 中,是否有 Channel 處于就緒狀態(tài),進而實現(xiàn)了單線程對多 Channel 的高效管理。
  4. Chartset,提供 Unicode 字符串定義,NIO 也提供了相應的編解碼器等,例如,通過下面的方式進行字符串到 ByteBuffer 的轉換:
Charset.defaultCharset().encode("Hello world!"));

Java 語言目前的線程實現(xiàn)是比較重量級的,啟動或者銷毀一個線程是有明顯開銷的,每個線程都有單獨的線程棧等結構,需要占用非常明顯的內存,所以,每一個 Client 啟動一個線程似乎都有些浪費。使用線程池可以避免這一問題,但是在連接數(shù)量急劇升高的時候線程上下文切換開銷會在高并發(fā)時變得很明顯,這是同步阻塞方式的低擴展性劣勢。java NIO多路復用能很好解決這一問題。

  • NIO主要步驟:
  1. 首先,通過 Selector.open() 創(chuàng)建一個 Selector,作為類似調度員的角色。
  2. 然后,創(chuàng)建一個 ServerSocketChannel,并且向 Selector 注冊,通過指定 SelectionKey.OP_ACCEPT,告訴調度員,它關注的是新的連接請求。
    注意,為什么我們要明確配置非阻塞模式呢?這是因為阻塞模式下,注冊操作是不允許的,會拋出 IllegalBlockingModeException 異常。
  3. Selector 阻塞在 select 操作,當有 Channel 發(fā)生接入請求,就會被喚醒。
    IO 同步阻塞模式,需要多線程以實現(xiàn)多任務處理。而 NIO 則是利用了單線程輪詢事件的機制,通過高效地定位就緒的 Channel,來決定做什么,僅僅 select 階段是阻塞的,可以有效避免大量客戶端連接時,頻繁線程切換帶來的問題,應用的擴展能力有了非常大的提高。
    在 Java 7 引入的 NIO 2 中,又增添了一種額外的異步 IO 模式,利用事件和回調,處理 Accept、Read 等操作。

java拷貝方式

  1. java.io 類庫, FileInputStream、FileOutputStream
  2. java.nio 類庫提供的 transferTo 或 transferFrom 方法 (零拷貝技術,數(shù)據(jù)傳輸并不需要用戶態(tài)參與,在內核態(tài)直接復制,省去了上下文切換的開銷和不必要的內存拷貝)。
  3. Java 標準類庫本身已經(jīng)提供了幾種 Files.copy 的實現(xiàn)
  • 如何提高類似拷貝等 IO 操作的性能,有一些寬泛的原則:
  1. 在程序中,使用緩存等機制,合理減少 IO 次數(shù)(在網(wǎng)絡通信中,如 TCP 傳輸,window 大小也可以看作是類似思路)。
  2. 使用 transferTo 等機制,減少上下文切換和額外 IO 操作。
  3. 盡量減少不必要的轉換過程,比如編解碼;對象序列化和反序列化,比如操作文本文件或者網(wǎng)絡通信,如果不是過程中需要使用文本信息,可以考慮不要將二進制信息轉換成字符串,直接傳輸二進制信息。

面向對象

  • 基本的設計原則 S.O.L.I.D 原則
  1. 單一職責(Single Responsibility),類或者對象最好是只有單一職責,在程序設計中如果發(fā)現(xiàn)某個類承擔著多種義務,可以考慮進行拆分。
  2. 開關原則(Open-Close, Open for extension, close for modification),設計要對擴展開放,對修改關閉。換句話說,程序設計應保證平滑的擴展性,盡量避免因為新增同類功能而修改已有實現(xiàn),這樣可以少產出些回歸(regression)問題。
  3. 里氏替換(Liskov Substitution),這是面向對象的基本要素之一,進行繼承關系抽象時,凡是可以用父類或者基類的地方,都可以用子類替換。
  4. 接口分離(Interface Segregation),我們在進行類和接口設計時,如果在一個接口里定義了太多方法,其子類很可能面臨兩難,就是只有部分方法對它是有意義的,這就破壞了程序的內聚性。
    對于這種情況,可以通過拆分成功能單一的多個接口,將行為進行解耦。在未來維護中,如果某個接口設計有變,不會對使用其他接口的子類構成影響。
  5. 依賴反轉(Dependency Inversion),實體應該依賴于抽象而不是實現(xiàn)。也就是說高層次模塊,不應該依賴于低層次模塊,而是應該基于抽象。實踐這一原則是保證產品代碼之間適當耦合度的法寶。

拷貝整理極客時間內容,侵刪

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
【社區(qū)內容提示】社區(qū)部分內容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發(fā)布,文章內容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內容

  • Java SE 基礎: 封裝、繼承、多態(tài) 封裝: 概念:就是把對象的屬性和操作(或服務)結合為一個獨立的整體,并盡...
    Jayden_Cao閱讀 2,247評論 0 8
  • 九種基本數(shù)據(jù)類型的大小,以及他們的封裝類。(1)九種基本數(shù)據(jù)類型和封裝類 (2)自動裝箱和自動拆箱 什么是自動裝箱...
    關瑋琳linSir閱讀 2,064評論 0 47
  • 相關概念 面向對象的三個特征 封裝,繼承,多態(tài).這個應該是人人皆知.有時候也會加上抽象. 多態(tài)的好處 允許不同類對...
    東經(jīng)315度閱讀 2,202評論 0 8
  • java基礎 集合承繼包含圖 Collection vs Collections 首先,"Collection" ...
    onlyHalfSoul閱讀 1,434評論 0 5
  • 1、堆設置(這里是重點) -Xms:初始堆大小-Xmx:最大堆大小-XX:NewSize=n:設置年輕代初始大小-...
    jasonlu1208閱讀 340評論 0 0

友情鏈接更多精彩內容