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 提供的默認排序算法,具體是什么排序方式以及設計思路
對于原始數(shù)據(jù)類型,目前使用的是所謂雙軸快速排序(Dual-Pivot QuickSort),是一種改進的快速排序算法,早期版本是相對傳統(tǒng)的快速排序,你可以閱讀源碼。
而對于對象數(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 源碼分析
底層數(shù)據(jù)結構
是數(shù)組(Node<K,V>[] table)和鏈表結合組成的復合結構,數(shù)組被分為一個個桶(bucket),通過哈希值決定了鍵值對在這個數(shù)組的尋址;哈希值相同的鍵值對,則以鏈表形式存儲,如果鏈表大小超過閾值(TREEIFY_THRESHOLD, 8),圖中的鏈表就會被改造為樹形結構。代碼邏輯
- resize 方法兼顧兩個職責,創(chuàng)建初始存儲表格,或者在容量不滿足需求的時候,進行擴容(resize)。
依據(jù) resize 源碼,不考慮極端情況(容量理論最大極限由 MAXIMUM_CAPACITY 指定,數(shù)值為 1<<30,也就是 2 的 30 次方),我們可以歸納為:
- 門限值等于(負載因子 0.75)x(容量 16),如果構建 HashMap 的時候沒有指定它們,那么就是依據(jù)相應的默認常量值。指定了cap,先求大于cap最近的2次冪,初始容量為2次冪 * 0.75,不指定就為12。
- 門限通常是以倍數(shù)進行調整 (newThr = oldThr << 1),我前面提到,根據(jù) putVal 中的邏輯,當元素個數(shù)超過門限大小時,則調整 Map 大小。
- 擴容后,需要將老的數(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)是基于:
- 分離鎖,也就是將內部進行分段(Segment),里面則是 HashEntry 的數(shù)組,和 HashMap 類似,哈希相同的條目也是以鏈表形式存放。
- HashEntry 內部使用 volatile 的 value 字段來保證可見性,也利用了不可變對象的機制以改進利用 Unsafe 提供的底層能力,比如 volatile access,去直接完成部分操作,以最優(yōu)化性能,畢竟 Unsafe 中的很多操作都是 JVM intrinsic 優(yōu)化過的。
你可以參考下面這個早期 ConcurrentHashMap 內部結構的示意圖,其核心是利用分段設計,在進行并發(fā)操作的時候,只需要鎖定相應段,這樣就有效避免了類似 Hashtable 整體同步的問題,大大提高了性能。

在構造的時候,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ā)生了哪些變化呢?
- 總體結構上,它的內部存儲變得和我在專欄上一講介紹的 HashMap 結構非常相似,同樣是大的桶(bucket)數(shù)組,然后內部也是一個個所謂的鏈表結構(bin),同步的粒度要更細致一些。
- 其內部仍然有 Segment 定義,但僅僅是為了保證序列化時的兼容性而已,不再有任何結構上的用處。因為不再使用 Segment,初始化操作大大簡化,修改為 lazy-load 形式,這樣可以有效避免初始開銷,解決了老版本很多人抱怨的這一點。
- 數(shù)據(jù)存儲利用 volatile 來保證可見性。
- 使用 CAS 等操作,在特定場景進行無鎖并發(fā)操作。
- 使用 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ù)工作。
- 基本概念
- 區(qū)分同步或異步(synchronous/asynchronous)。簡單來說,同步是一種可靠的有序運行機制,當我們進行同步操作時,后續(xù)的任務是等待當前調用返回,才會進行下一步;而異步則相反,其他任務不需要等待當前調用返回,通常依靠事件、回調等機制來實現(xiàn)任務間次序關系。
- 區(qū)分阻塞與非阻塞(blocking/non-blocking)。在進行阻塞操作時,當前線程會處于阻塞狀態(tài),無法從事其他任務,只有當條件就緒才能繼續(xù),比如 ServerSocket 新連接建立完畢,或數(shù)據(jù)讀取、寫入操作完成;而非阻塞則是不管 IO 操作是否結束,直接返回,相應操作在后臺繼續(xù)處理。
- 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 的高效管理。
- Chartset,提供 Unicode 字符串定義,NIO 也提供了相應的編解碼器等,例如,通過下面的方式進行字符串到 ByteBuffer 的轉換:
Charset.defaultCharset().encode("Hello world!"));
Java 語言目前的線程實現(xiàn)是比較重量級的,啟動或者銷毀一個線程是有明顯開銷的,每個線程都有單獨的線程棧等結構,需要占用非常明顯的內存,所以,每一個 Client 啟動一個線程似乎都有些浪費。使用線程池可以避免這一問題,但是在連接數(shù)量急劇升高的時候線程上下文切換開銷會在高并發(fā)時變得很明顯,這是同步阻塞方式的低擴展性劣勢。java NIO多路復用能很好解決這一問題。
- NIO主要步驟:
- 首先,通過 Selector.open() 創(chuàng)建一個 Selector,作為類似調度員的角色。
- 然后,創(chuàng)建一個 ServerSocketChannel,并且向 Selector 注冊,通過指定 SelectionKey.OP_ACCEPT,告訴調度員,它關注的是新的連接請求。
注意,為什么我們要明確配置非阻塞模式呢?這是因為阻塞模式下,注冊操作是不允許的,會拋出 IllegalBlockingModeException 異常。 - Selector 阻塞在 select 操作,當有 Channel 發(fā)生接入請求,就會被喚醒。
IO 同步阻塞模式,需要多線程以實現(xiàn)多任務處理。而 NIO 則是利用了單線程輪詢事件的機制,通過高效地定位就緒的 Channel,來決定做什么,僅僅 select 階段是阻塞的,可以有效避免大量客戶端連接時,頻繁線程切換帶來的問題,應用的擴展能力有了非常大的提高。
在 Java 7 引入的 NIO 2 中,又增添了一種額外的異步 IO 模式,利用事件和回調,處理 Accept、Read 等操作。
java拷貝方式
- java.io 類庫, FileInputStream、FileOutputStream
- java.nio 類庫提供的 transferTo 或 transferFrom 方法 (零拷貝技術,數(shù)據(jù)傳輸并不需要用戶態(tài)參與,在內核態(tài)直接復制,省去了上下文切換的開銷和不必要的內存拷貝)。
- Java 標準類庫本身已經(jīng)提供了幾種 Files.copy 的實現(xiàn)
- 如何提高類似拷貝等 IO 操作的性能,有一些寬泛的原則:
- 在程序中,使用緩存等機制,合理減少 IO 次數(shù)(在網(wǎng)絡通信中,如 TCP 傳輸,window 大小也可以看作是類似思路)。
- 使用 transferTo 等機制,減少上下文切換和額外 IO 操作。
- 盡量減少不必要的轉換過程,比如編解碼;對象序列化和反序列化,比如操作文本文件或者網(wǎng)絡通信,如果不是過程中需要使用文本信息,可以考慮不要將二進制信息轉換成字符串,直接傳輸二進制信息。
面向對象
- 基本的設計原則 S.O.L.I.D 原則
- 單一職責(Single Responsibility),類或者對象最好是只有單一職責,在程序設計中如果發(fā)現(xiàn)某個類承擔著多種義務,可以考慮進行拆分。
- 開關原則(Open-Close, Open for extension, close for modification),設計要對擴展開放,對修改關閉。換句話說,程序設計應保證平滑的擴展性,盡量避免因為新增同類功能而修改已有實現(xiàn),這樣可以少產出些回歸(regression)問題。
- 里氏替換(Liskov Substitution),這是面向對象的基本要素之一,進行繼承關系抽象時,凡是可以用父類或者基類的地方,都可以用子類替換。
- 接口分離(Interface Segregation),我們在進行類和接口設計時,如果在一個接口里定義了太多方法,其子類很可能面臨兩難,就是只有部分方法對它是有意義的,這就破壞了程序的內聚性。
對于這種情況,可以通過拆分成功能單一的多個接口,將行為進行解耦。在未來維護中,如果某個接口設計有變,不會對使用其他接口的子類構成影響。 - 依賴反轉(Dependency Inversion),實體應該依賴于抽象而不是實現(xiàn)。也就是說高層次模塊,不應該依賴于低層次模塊,而是應該基于抽象。實踐這一原則是保證產品代碼之間適當耦合度的法寶。
拷貝整理極客時間內容,侵刪