Java 自己是一種面向對象的語言,最顯著的特性有兩個方面,一是所謂的 「一次編寫,處處運行」(Write once, run anywhere),可以很是容易地得到跨平臺能力;另外就是 垃圾收集(GC, Garbage Collection),Java 經過垃圾收集器(Garbage Collector)回收分配內存,大部分狀況下,程序員不須要本身操心內存的分配和回收。java
咱們平常會接觸到 JRE(Java Runtime Environment)或者 JDK(Java Development Kit)。 JRE,也就是 Java 運行環境,包含了 JVM 和 Java 類庫,以及一些模塊等。而 JDK 能夠看做是 JRE 的一個超集,提供了更多工具,好比編譯器、各類診斷工具等。程序員
對於「Java 是解釋執行」這句話,這個說法不太準確。咱們開發的 Java 的源代碼,首先經過 Javac 編譯成爲字節碼(bytecode),而後,在運行時,經過 Java 虛擬機(JVM)內嵌的解釋器將字節碼轉換成爲最終的機器碼。可是常見的 JVM,好比咱們大多數狀況使用的 Oracle JDK 提供的 Hotspot JVM,都提供了 JIT(Just-In-Time)編譯器,也就是一般所說的動態編譯器,JIT 可以在運行時將熱點代碼編譯成機器碼,這種狀況下部分熱點代碼就屬於編譯執行,而不是解釋執行了。算法
Exception 和 Error 都是繼承了 Throwable 類,在 Java 中只有 Throwable 類型的實例才能夠被拋出(throw)或者捕獲(catch),它是異常處理機制的基本組成類型。數據庫
Exception 和 Error 體現了 Java 平臺設計者對不一樣異常狀況的分類。Exception 是程序正常運行中,能夠預料的意外狀況,可能而且應該被捕獲,進行相應處理。編程
Error 是指在正常狀況下,不大可能出現的狀況,絕大部分的 Error 都會致使程序(好比 JVM 自身)處於非正常的、不可恢復狀態。既然是非正常狀況,因此不便於也不須要捕獲,常見的好比 OutOfMemoryError 之類,都是 Error 的子類。後端
Exception 又分爲可檢查(checked)異常和不檢查(unchecked)異常,設計模式
可檢查異常在源代碼裏必須顯式地進行捕獲處理,這是編譯期檢查的一部分。數組
不可檢查異常就是所謂的運行時異常,相似 NullPointerException、ArrayIndexOutOfBoundsException 之類,一般是能夠編碼避免的邏輯錯誤,具體根據須要來判斷是否須要捕獲,並不會在編譯期強制要求。緩存
final 能夠用來修飾類、方法、變量,分別有不一樣的意義,final 修飾的 class 表明不能夠繼承擴展,final 的變量是不能夠修改的,而 final 的方法也是不能夠重寫的(override)。安全
finally 則是 Java 保證重點代碼必定要被執行的一種機制。咱們可使用 try-finally 或者 try-catch-finally 來進行相似關閉 JDBC 鏈接、保證 unlock 鎖等動做。
finalize 是基礎類 java.lang.Object 的一個方法,它的設計目的是保證對象在被垃圾收集前完成特定資源的回收。finalize 機制如今已經不推薦使用,而且在 JDK 9 開始被標記爲 deprecated(棄用)。
不一樣的引用類型,主要體現的是對象不一樣的可達性(reachable)狀態和對垃圾收集的影響。
所謂強引用(「Strong」 Reference),就是咱們最多見的普通對象引用,只要還有強引用指向一個對象,就能代表對象還「活着」,垃圾收集器不會碰這種對象。對於一個普通的對象,若是沒有其餘的引用關係,只要超過了引用的做用域或者顯式地將相應(強)引用賦值爲 null,就是能夠被垃圾收集的了,固然具體回收時機仍是要看垃圾收集策略。
軟引用(SoftReference),是一種相對強引用弱化一些的引用,可讓對象豁免一些垃圾收集,只有當 JVM 認爲內存不足時,纔會去試圖回收軟引用指向的對象。JVM 會確保在拋出 OutOfMemoryError 以前,清理軟引用指向的對象。軟引用一般用來實現內存敏感的緩存,若是還有空閒內存,就能夠暫時保留緩存,當內存不足時清理掉,這樣就保證了使用緩存的同時,不會耗盡內存。
弱引用(WeakReference)並不能使對象豁免垃圾收集,僅僅是提供一種訪問在弱引用狀態下對象的途徑。這就能夠用來構建一種沒有特定約束的關係,好比,維護一種非強制性的映射關係,若是試圖獲取時對象還在,就使用它,不然重現實例化。它一樣是不少緩存實現的選擇。
對於幻象引用,有時候也翻譯成虛引用,你不能經過它訪問對象。幻象引用僅僅是提供了一種確保對象被 finalize 之後,作某些事情的機制,好比,一般用來作所謂的 Post-Mortem 清理機制,我在專欄上一講中介紹的 Java 平臺自身 Cleaner 機制等,也有人利用幻象引用監控對象的建立和銷燬。
強可達(Strongly Reachable),就是當一個對象能夠有一個或多個線程能夠不經過各類引用訪問到的狀況。好比,咱們新建立一個對象,那麼建立它的線程對它就是強可達。
軟可達(Softly Reachable),就是當咱們只能經過軟引用才能訪問到對象的狀態。
弱可達(Weakly Reachable),相似前面提到的,就是沒法經過強引用或者軟引用訪問,只能經過弱引用訪問時的狀態。這是十分臨近 finalize 狀態的時機,當弱引用被清除的時候,就符合 finalize 的條件了。
幻象可達(Phantom Reachable),上面流程圖已經很直觀了,就是沒有強、軟、弱引用關聯,而且 finalize 過了,只有幻象引用指向這個對象的時候。
固然,還有一個最後的狀態,就是不可達(unreachable),意味着對象能夠被清除了。
String 是 Java 語言很是基礎和重要的類,提供了構造和管理字符串的各類基本邏輯。它是典型的 Immutable(不可變的) 類,被聲明成爲 final class,全部屬性也都是 final 的。也因爲它的不可變性,相似拼接、裁剪字符串等動做,都會產生新的 String 對象。因爲字符串操做的廣泛性,因此相關操做的效率每每對應用性能有明顯影響。
StringBuffer 是爲解決上面提到拼接產生太多中間對象的問題而提供的一個類,咱們能夠用 append 或者 add 方法,把字符串添加到已有序列的末尾或者指定位置。StringBuffer 本質是一個線程安全的可修改字符序列,它保證了線程安全,也隨之帶來了額外的性能開銷,因此除非有線程安全的須要,否則仍是推薦使用它的後繼者,也就是 StringBuilder。
StringBuilder 是 Java 1.5 中新增的,在能力上和 StringBuffer 沒有本質區別,可是它去掉了線程安全的部分,有效減少了開銷,是絕大部分狀況下進行字符串拼接的首選。
StringBuffer和StringBuilder的區別:
反射機制是 Java 語言提供的一種基礎功能,賦予程序在運行時自省(introspect,官方用語)的能力。經過反射咱們能夠直接操做類或者對象,好比獲取某個對象的類定義,獲取類聲明的屬性和方法,調用方法或者構造對象,甚至能夠運行時修改類定義。
動態代理是一種方便運行時動態構建代理、動態處理代理方法調用的機制,不少場景都是利用相似機制作到的,好比用來包裝 RPC 調用、面向切面的編程(AOP)。
實現動態代理的方式不少,好比 JDK 自身提供的動態代理,就是主要利用了上面提到的反射機制。還有其餘的實現方式,好比利用傳說中更高性能的字節碼操做機制,相似 ASM、cglib(基於 ASM)、Javassist 等。
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 之間。
這三者都是實現集合框架中的 List,也就是所謂的有序集合,所以具體功能也比較近似,好比都提供按照位置進行定位、添加或者刪除的操做,都提供迭代器以遍歷其內容等。但由於具體的設計區別,在行爲、性能、線程安全等方面,表現又有很大不一樣。
Vector 是 Java 早期提供的線程安全的動態數組,若是不須要線程安全,並不建議選擇,畢竟同步是有額外開銷的。Vector 內部是使用對象數組來保存數據,能夠根據須要自動的增長容量,當數組已滿時,會建立新的數組,並拷貝原有數組數據。
ArrayList 是應用更加普遍的動態數組實現,它自己不是線程安全的,因此性能要好不少。與 Vector 近似,ArrayList 也是能夠根據須要調整容量,不過二者的調整邏輯有所區別,Vector 在擴容時會提升 1 倍,而 ArrayList 則是增長 50%。
LinkedList 顧名思義是 Java 提供的雙向鏈表,因此它不須要像上面兩種那樣調整容量,它也不是線程安全的。
Hashtable、HashMap、TreeMap 都是最多見的一些 Map 實現,是以鍵值對的形式存儲和操做數據的容器類型。
Hashtable 是早期 Java 類庫提供的一個哈希表實現,自己是同步的,不支持 null 鍵和值,因爲同步致使的性能開銷,因此已經不多被推薦使用。
HashMap 是應用更加普遍的哈希表實現,行爲上大體上與 HashTable 一致,主要區別在於 HashMap 不是同步的,支持 null 鍵和值等。一般狀況下,HashMap 進行 put 或者 get 操做,能夠達到常數時間的性能,因此它是絕大部分利用鍵值對存取場景的首選,好比,實現一個用戶 ID 和用戶信息對應的運行時存儲結構。
TreeMap 則是基於紅黑樹的一種提供順序訪問的 Map,和 HashMap 不一樣,它的 get、put、remove 之類操做都是 O(log(n))的時間複雜度,具體順序能夠由指定的 Comparator 來決定,或者根據鍵的天然順序來判斷。
Java 提供了不一樣層面的線程安全支持。在傳統集合框架內部,除了 Hashtable 等同步容器,還提供了所謂的同步包裝器(Synchronized Wrapper),咱們能夠調用 Collections 工具類提供的包裝方法,來獲取一個同步的包裝容器(如 Collections.synchronizedMap),可是它們都是利用很是粗粒度的同步方式,在高併發狀況下,性能比較低下。
另外,更加廣泛的選擇是利用併發包提供的線程安全容器類,它提供了:
各類併發容器,好比 ConcurrentHashMap、CopyOnWriteArrayList。
各類線程安全隊列(Queue/Deque),如 ArrayBlockingQueue、SynchronousQueue。
各類有序容器的線程安全版本等。
具體保證線程安全的方式,包括有從簡單的 synchronize 方式,到基於更加精細化的,好比基於分離鎖實現的 ConcurrentHashMap 等併發實現等。具體選擇要看開發的場景需求,整體來講,併發包內提供的容器通用場景,遠優於早期的簡單同步實現。
Java IO 方式有不少種,基於不一樣的 IO 抽象模型和交互方式,能夠進行簡單區分。
首先,傳統的 java.io 包,它基於流模型實現,提供了咱們最熟知的一些 IO 功能,好比 File 抽象、輸入輸出流等。交互方式是同步、阻塞的方式,也就是說,在讀取輸入流或者寫入輸出流時,在讀、寫動做完成以前,線程會一直阻塞在那裏,它們之間的調用是可靠的線性順序。
java.io 包的好處是代碼比較簡單、直觀,缺點則是 IO 效率和擴展性存在侷限性,容易成爲應用性能的瓶頸。
不少時候,人們也把 java.net 下面提供的部分網絡 API,好比 Socket、ServerSocket、HttpURLConnection 也歸類到同步阻塞 IO 類庫,由於網絡通訊一樣是 IO 行爲。
第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,能夠構建多路複用的、同步非阻塞 IO 程序,同時提供了更接近操做系統底層的高性能數據操做方式。
第三,在 Java 7 中,NIO 有了進一步的改進,也就是 NIO 2,引入了異步非阻塞 IO 方式,也有不少人叫它 AIO(Asynchronous IO)。異步 IO 操做基於事件和回調機制,能夠簡單理解爲,應用操做直接返回,而不會阻塞在那裏,當後臺處理完成,操做系統會通知相應線程進行後續工做。
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 方法實現。
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;
}
}
}
複製代碼
固然,Java 標準類庫自己已經提供了幾種 Files.copy 的實現。
對於 Copy 的效率,這個其實與操做系統和配置等狀況相關,整體上來講,NIO transferTo/From 的方式可能更快,由於它更能利用現代操做系統底層機制,避免沒必要要拷貝和上下文切換。
接口和抽象類是 Java 面向對象設計的兩個基礎機制。
接口是對行爲的抽象,它是抽象方法的集合,利用接口能夠達到 API 定義和實現分離的目的。接口,不能實例化;不能包含任何很是量成員,任何 field 都是隱含着 public static final 的意義;同時,沒有非靜態方法實現,也就是說要麼是抽象方法,要麼是靜態方法。Java 標準類庫中,定義了很是多的接口,好比 java.util.List。
抽象類是不能實例化的類,用 abstract 關鍵字修飾 class,其目的主要是代碼重用。除了不能實例化,形式上和通常的 Java 類並無太大區別,能夠有一個或者多個抽象方法,也能夠沒有抽象方法。抽象類大多用於抽取相關 Java 類的共用方法實現或者是共同成員變量,而後經過繼承的方式達到代碼複用的目的。Java 標準庫中,好比 collection 框架,不少通用部分就被抽取成爲抽象類,例如 java.util.AbstractList。
Java 類實現 interface 使用 implements 關鍵詞,繼承 abstract class 則是使用 extends 關鍵詞,咱們能夠參考 Java 標準庫中的 ArrayList。
public class ArrayList<E> extends AbstractList<E>
implements List<E>, RandomAccess, Cloneable, java.io.Serializable
{
//...
}
複製代碼
大體按照模式的應用目標分類,設計模式能夠分爲建立型模式、結構型模式和行爲型模式。
建立型模式,是對對象建立過程的各類問題和解決方案的總結,包括各類工廠模式(Factory、Abstract Factory)、單例模式(Singleton)、構建器模式(Builder)、原型模式(ProtoType)。
結構型模式,是針對軟件設計結構的總結,關注於類、對象繼承、組合方式的實踐經驗。常見的結構型模式,包括橋接模式(Bridge)、適配器模式(Adapter)、裝飾者模式(Decorator)、代理模式(Proxy)、組合模式(Composite)、外觀模式(Facade)、享元模式(Flyweight)等。
行爲型模式,是從類或對象之間交互、職責劃分等角度總結的模式。比較常見的行爲型模式有策略模式(Strategy)、解釋器模式(Interpreter)、命令模式(Command)、觀察者模式(Observer)、迭代器模式(Iterator)、模板方法模式(Template Method)、訪問者模式(Visitor)。
synchronized 是 Java 內建的同步機制,因此也有人稱其爲 Intrinsic Locking,它提供了互斥的語義和可見性,當一個線程已經獲取當前鎖時,其餘試圖獲取的線程只能等待或者阻塞在那裏。
在 Java 5 之前,synchronized 是僅有的同步手段,在代碼中, synchronized 能夠用來修飾方法,也可使用在特定的代碼塊兒上,本質上 synchronized 方法等同於把方法所有語句用 synchronized 塊包起來。
ReentrantLock,一般翻譯爲再入鎖,是 Java 5 提供的鎖實現,它的語義和 synchronized 基本相同。再入鎖經過代碼直接調用 lock() 方法獲取,代碼書寫也更加靈活。與此同時,ReentrantLock 提供了不少實用的方法,可以實現不少 synchronized 沒法作到的細節控制,好比能夠控制 fairness,也就是公平性,或者利用定義條件等。可是,編碼中也須要注意,必需要明確調用 unlock() 方法釋放,否則就會一直持有該鎖。
synchronized 和 ReentrantLock 的性能不能一律而論,早期版本 synchronized 在不少場景下性能相差較大,在後續版本進行了較多改進,在低競爭場景中表現可能優於 ReentrantLock。
synchronized 代碼塊是由一對兒 monitorenter/monitorexit 指令實現的,Monitor 對象是同步的基本實現單元。
在 Java 6 以前,Monitor 的實現徹底是依靠操做系統內部的互斥鎖,由於須要進行用戶態到內核態的切換,因此同步操做是一個無差異的重量級操做。
現代的(Oracle)JDK 中,JVM 對此進行了大刀闊斧地改進,提供了三種不一樣的 Monitor 實現,也就是常說的三種不一樣的鎖:偏斜鎖(Biased Locking)、輕量級鎖和重量級鎖,大大改進了其性能。
所謂鎖的升級、降級,就是 JVM 優化 synchronized 運行的機制,當 JVM 檢測到不一樣的競爭情況時,會自動切換到適合的鎖實現,這種切換就是鎖的升級、降級。
當沒有競爭出現時,默認會使用偏斜鎖。JVM 會利用 CAS 操做(compare and swap),在對象頭上的 Mark Word 部分設置線程 ID,以表示這個對象偏向於當前線程,因此並不涉及真正的互斥鎖。這樣作的假設是基於在不少應用場景中,大部分對象生命週期中最多會被一個線程鎖定,使用偏斜鎖能夠下降無競爭開銷。
若是有另外的線程試圖鎖定某個已經被偏斜過的對象,JVM 就須要撤銷(revoke)偏斜鎖,並切換到輕量級鎖實現。輕量級鎖依賴 CAS 操做 Mark Word 來試圖獲取鎖,若是重試成功,就使用普通的輕量級鎖;不然,進一步升級爲重量級鎖。
我注意到有的觀點認爲 Java 不會進行鎖降級。實際上據我所知,鎖降級確實是會發生的,當 JVM 進入安全點(SafePoint)的時候,會檢查是否有閒置的 Monitor,而後試圖進行降級。
Java 的線程是不容許啓動兩次的,第二次調用必然會拋出 IllegalThreadStateException,這是一種運行時異常,屢次調用 start 被認爲是編程錯誤。
關於線程生命週期的不一樣狀態,在 Java 5 之後,線程狀態被明肯定義在其公共內部枚舉類型 java.lang.Thread.State 中,分別是:
新建(NEW),表示線程被建立出來還沒真正啓動的狀態,能夠認爲它是個 Java 內部狀態。
就緒(RUNNABLE),表示該線程已經在 JVM 中執行,固然因爲執行須要計算資源,它多是正在運行,也可能還在等待系統分配給它 CPU 片斷,在就緒隊列裏面排隊。
在其餘一些分析中,會額外區分一種狀態 RUNNING,可是從 Java API 的角度,並不能表示出來。
阻塞(BLOCKED),這個狀態和咱們前面兩講介紹的同步很是相關,阻塞表示線程在等待 Monitor lock。好比,線程試圖經過 synchronized 去獲取某個鎖,可是其餘線程已經獨佔了,那麼當前線程就會處於阻塞狀態。
等待(WAITING),表示正在等待其餘線程採起某些操做。一個常見的場景是相似生產者消費者模式,發現任務條件還沒有知足,就讓當前消費者線程等待(wait),另外的生產者線程去準備任務數據,而後經過相似 notify 等動做,通知消費線程能夠繼續工做了。Thread.join() 也會令線程進入等待狀態。
計時等待(TIMED_WAIT),其進入條件和等待狀態相似,可是調用的是存在超時條件的方法,好比 wait 或 join 等方法的指定超時版本,以下面示例:
public final native void wait(long timeout) throws InterruptedException;
複製代碼
終止(TERMINATED),無論是意外退出仍是正常執行結束,線程已經完成使命,終止運行,也有人把這個狀態叫做死亡。 在第二次調用 start() 方法的時候,線程可能處於終止或者其餘(非 NEW)狀態,可是不論如何,都是不能夠再次啓動的。
死鎖是一種特定的程序狀態,在實體之間,因爲循環依賴致使彼此一直處於等待之中,沒有任何個體能夠繼續前進。死鎖不只僅是在線程之間會發生,存在資源獨佔的進程之間一樣也可能出現死鎖。一般來講,咱們大可能是聚焦在多線程場景中的死鎖,指兩個或多個線程之間,因爲互相持有對方須要的鎖,而永久處於阻塞的狀態。
你能夠利用下面的示例圖理解基本的死鎖問題:
定位死鎖最多見的方式就是利用 jstack 等工具獲取線程棧,而後定位互相之間的依賴關係,進而找到死鎖。若是是比較明顯的死鎖,每每 jstack 等就能直接定位,相似 JConsole 甚至能夠在圖形界面進行有限的死鎖檢測。
若是程序運行時發生了死鎖,絕大多數狀況下都是沒法在線解決的,只能重啓、修正程序自己問題。因此,代碼開發階段互相審查,或者利用工具進行預防性排查,每每也是很重要的。
咱們一般所說的併發包也就是 java.util.concurrent 及其子包,集中了 Java 併發的各類基礎工具類,具體主要包括幾個方面:
提供了比 synchronized 更加高級的各類同步結構,包括 CountDownLatch、CyclicBarrier、Semaphore 等,能夠實現更加豐富的多線程操做,好比利用 Semaphore 做爲資源控制器,限制同時進行工做的線程數量。
各類線程安全的容器,好比最多見的 ConcurrentHashMap、有序的 ConcunrrentSkipListMap,或者經過相似快照機制,實現線程安全的動態數組 CopyOnWriteArrayList 等。
各類併發隊列實現,如各類 BlockedQueue 實現,比較典型的 ArrayBlockingQueue、 SynchorousQueue 或針對特定場景的 PriorityBlockingQueue 等。
強大的 Executor 框架,能夠建立各類不一樣類型的線程池,調度任務運行等,絕大部分狀況下,再也不須要本身從頭實現線程池和任務調度器。
有時候咱們把併發包下面的全部容器都習慣叫做併發容器,可是嚴格來說,相似 ConcurrentLinkedQueue 這種「Concurrent*」容器,纔是真正表明併發。
關於問題中它們的區別:
Concurrent 類型基於 lock-free,在常見的多線程訪問場景,通常能夠提供較高吞吐量。
而 LinkedBlockingQueue 內部則是基於鎖,並提供了 BlockingQueue 的等待性方法。
不知道你有沒有注意到,java.util.concurrent 包提供的容器(Queue、List、Set)、Map,從命名上能夠大概區分爲 Concurrent*、CopyOnWrite和 Blocking等三類,一樣是線程安全容器,能夠簡單認爲:
Concurrent 類型沒有相似 CopyOnWrite 之類容器相對較重的修改開銷。
可是,凡事都是有代價的,Concurrent 每每提供了較低的遍歷一致性。你能夠這樣理解所謂的弱一致性,例如,當利用迭代器遍歷時,若是容器發生修改,迭代器仍然能夠繼續進行遍歷。
與弱一致性對應的,就是我介紹過的同步容器常見的行爲「fail-fast」,也就是檢測到容器在遍歷過程當中發生了修改,則拋出 ConcurrentModificationException,再也不繼續遍歷。
弱一致性的另一個體現是,size 等操做準確性是有限的,未必是 100% 準確。
與此同時,讀取的性能具備必定的不肯定性。
一般開發者都是利用 Executors 提供的通用線程池建立方法,去建立不一樣配置的線程池,主要區別在於不一樣的 ExecutorService 類型或者不一樣的初始參數。
Executors 目前提供了 5 種不一樣的線程池建立配置:
newCachedThreadPool(),它是一種用來處理大量短期工做任務的線程池,具備幾個鮮明特色:它會試圖緩存線程並重用,當無緩存線程可用時,就會建立新的工做線程;若是線程閒置的時間超過 60 秒,則被終止並移出緩存;長時間閒置時,這種線程池,不會消耗什麼資源。其內部使用 SynchronousQueue 做爲工做隊列。
newFixedThreadPool(int nThreads),重用指定數目(nThreads)的線程,其背後使用的是無界的工做隊列,任什麼時候候最多有 nThreads 個工做線程是活動的。這意味着,若是任務數量超過了活動隊列數目,將在工做隊列中等待空閒線程出現;若是有工做線程退出,將會有新的工做線程被建立,以補足指定的數目 nThreads。
newSingleThreadExecutor(),它的特色在於工做線程數目被限制爲 1,操做一個無界的工做隊列,因此它保證了全部任務的都是被順序執行,最多會有一個任務處於活動狀態,而且不容許使用者改動線程池實例,所以能夠避免其改變線程數目。
newSingleThreadScheduledExecutor() 和 newScheduledThreadPool(int corePoolSize),建立的是個 ScheduledExecutorService,能夠進行定時或週期性的工做調度,區別在於單一工做線程仍是多個工做線程。
newWorkStealingPool(int parallelism),這是一個常常被人忽略的線程池,Java 8 才加入這個建立方法,其內部會構建ForkJoinPool,利用Work-Stealing算法,並行地處理任務,不保證處理順序。
AtomicIntger 是對 int 類型的一個封裝,提供原子性的訪問和更新操做,其原子性操做的實現是基於 CAS(compare-and-swap)技術。
所謂 CAS,表徵的是一些列操做的集合,獲取當前數值,進行一些運算,利用 CAS 指令試圖進行更新。若是當前數值未變,表明沒有其餘線程進行併發修改,則成功更新。不然,可能出現不一樣的選擇,要麼進行重試,要麼就返回一個成功或者失敗的結果。
從 AtomicInteger 的內部屬性能夠看出,它依賴於 Unsafe 提供的一些底層能力,進行底層操做;以 volatile 的 value 字段,記錄數值,以保證可見性。
private static final jdk.internal.misc.Unsafe U = jdk.internal.misc.Unsafe.getUnsafe();
private static final long VALUE = U.objectFieldOffset(AtomicInteger.class, "value");
private volatile int value;
複製代碼
具體的原子操做細節,能夠參考任意一個原子更新方法,好比下面的 getAndIncrement。
Unsafe 會利用 value 字段的內存地址偏移,直接完成操做。
public final int getAndIncrement() {
return U.getAndAddInt(this, VALUE, 1);
}
複製代碼
由於 getAndIncrement 須要返歸數值,因此須要添加失敗重試邏輯。
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!weakCompareAndSetInt(o, offset, v, v + delta));
return v;
}
複製代碼
而相似 compareAndSet 這種返回 boolean 類型的函數,由於其返回值表現的就是成功與否,因此不須要重試。
public final boolean compareAndSet(int expectedValue, int newValue)
複製代碼
CAS 是 Java 併發中所謂 lock-free 機制的基礎。
通常來講,咱們把 Java 的類加載過程分爲三個主要步驟:加載、連接、初始化,具體行爲在Java 虛擬機規範裏有很是詳細的定義。
首先是加載階段(Loading),它是 Java 將字節碼數據從不一樣的數據源讀取到 JVM 中,並映射爲 JVM 承認的數據結構(Class 對象),這裏的數據源多是各類各樣的形態,如 jar 文件、class 文件,甚至是網絡數據源等;若是輸入數據不是 ClassFile 的結構,則會拋出 ClassFormatError。
加載階段是用戶參與的階段,咱們能夠自定義類加載器,去實現本身的類加載過程。
第二階段是連接(Linking),這是核心的步驟,簡單說是把原始的類定義信息平滑地轉化入 JVM 運行的過程當中。這裏可進一步細分爲三個步驟:
驗證(Verification),這是虛擬機安全的重要保障,JVM 須要覈驗字節信息是符合 Java 虛擬機規範的,不然就被認爲是 VerifyError,這樣就防止了惡意信息或者不合規的信息危害 JVM 的運行,驗證階段有可能觸發更多 class 的加載。
準備(Preparation),建立類或接口中的靜態變量,並初始化靜態變量的初始值。但這裏的「初始化」和下面的顯式初始化階段是有區別的,側重點在於分配所須要的內存空間,不會去執行更進一步的 JVM 指令。
解析(Resolution),在這一步會將常量池中的符號引用(symbolic reference)替換爲直接引用。在Java 虛擬機規範中,詳細介紹了類、接口、方法和字段等各個方面的解析。
最後是初始化階段(initialization),這一步真正去執行類初始化的代碼邏輯,包括靜態字段賦值的動做,以及執行類定義中的靜態初始化塊內的邏輯,編譯器在編譯階段就會把這部分邏輯整理好,父類型的初始化邏輯優先於當前類型的邏輯。
再來談談雙親委派模型,簡單說就是當類加載器(Class-Loader)試圖加載某個類型的時候,除非父加載器找不到相應類型,不然儘可能將這個任務代理給當前加載器的父加載器去作。使用委派模型的目的是避免重複加載 Java 類型。
咱們能夠從常見的 Java 類來源分析,一般的開發過程是,開發者編寫 Java 代碼,調用 javac 編譯成 class 文件,而後經過類加載機制載入 JVM,就成爲應用運行時可使用的 Java 類了。
從上面過程獲得啓發,其中一個直接的方式是從源碼入手,能夠利用 Java 程序生成一段源碼,而後保存到文件等,下面就只須要解決編譯問題了。
有一種笨辦法,直接用 ProcessBuilder 之類啓動 javac 進程,並指定上面生成的文件做爲輸入,進行編譯。最後,再利用類加載器,在運行時加載便可。
前面的方法,本質上仍是在當前程序進程以外編譯的,那麼還有沒有不這麼 low 的辦法呢?
你能夠考慮使用 Java Compiler API,這是 JDK 提供的標準 API,裏面提供了與 javac 對等的編譯器功能,具體請參考java.compiler相關文檔。
進一步思考,咱們一直圍繞 Java 源碼編譯成爲 JVM 能夠理解的字節碼,換句話說,只要是符合 JVM 規範的字節碼,無論它是如何生成的,是否是均可以被 JVM 加載呢?咱們能不能直接生成相應的字節碼,而後交給類加載器去加載呢?
固然也能夠,不過直接去寫字節碼難度太大,一般咱們能夠利用 Java 字節碼操縱工具和類庫來實現,好比在專欄第 6 講中提到的ASM、Javassist、cglib 等。
一般能夠把 JVM 內存區域分爲下面幾個方面,其中,有的區域是以線程爲單位,而有的區域則是整個 JVM 進程惟一的。
首先,程序計數器(PC,Program Counter Register)。在 JVM 規範中,每一個線程都有它本身的程序計數器,而且任什麼時候間一個線程都只有一個方法在執行,也就是所謂的當前方法。程序計數器會存儲當前線程正在執行的 Java 方法的 JVM 指令地址;或者,若是是在執行本地方法,則是未指定值(undefined)。
第二,Java 虛擬機棧(Java Virtual Machine Stack),早期也叫 Java 棧。每一個線程在建立時都會建立一個虛擬機棧,其內部保存一個個的棧幀(Stack Frame),對應着一次次的 Java 方法調用。
前面談程序計數器時,提到了當前方法;同理,在一個時間點,對應的只會有一個活動的棧幀,一般叫做當前幀,方法所在的類叫做當前類。若是在該方法中調用了其餘方法,對應的新的棧幀會被建立出來,成爲新的當前幀,一直到它返回結果或者執行結束。JVM 直接對 Java 棧的操做只有兩個,就是對棧幀的壓棧和出棧。
棧幀中存儲着局部變量表、操做數(operand)棧、動態連接、方法正常退出或者異常退出的定義等。
第三,堆(Heap),它是 Java 內存管理的核心區域,用來放置 Java 對象實例,幾乎全部建立的 Java 對象實例都是被直接分配在堆上。堆被全部的線程共享,在虛擬機啓動時,咱們指定的「Xmx」之類參數就是用來指定最大堆空間等指標。
理所固然,堆也是垃圾收集器重點照顧的區域,因此堆內空間還會被不一樣的垃圾收集器進行進一步的細分,最有名的就是新生代、老年代的劃分。
第四,方法區(Method Area)。這也是全部線程共享的一塊內存區域,用於存儲所謂的元(Meta)數據,例如類結構信息,以及對應的運行時常量池、字段、方法代碼等。
因爲早期的 Hotspot JVM 實現,不少人習慣於將方法區稱爲永久代(Permanent Generation)。Oracle JDK 8 中將永久代移除,同時增長了元數據區(Metaspace)。
第五,運行時常量池(Run-Time Constant Pool),這是方法區的一部分。若是仔細分析過反編譯的類文件結構,你能看到版本號、字段、方法、超類、接口等各類信息,還有一項信息就是常量池。Java 的常量池能夠存放各類常量信息,無論是編譯期生成的各類字面量,仍是須要在運行時決定的符號引用,因此它比通常語言的符號表存儲的信息更加寬泛。
第六,本地方法棧(Native Method Stack)。它和 Java 虛擬機棧是很是類似的,支持對本地方法的調用,也是每一個線程都會建立一個。在 Oracle Hotspot JVM 中,本地方法棧和 Java 虛擬機棧是在同一起區域,這徹底取決於技術實現的決定,並未在規範中強制。
瞭解 JVM 內存的方法有不少,具體能力範圍也有區別,簡單總結以下:
可使用綜合性的圖形化工具,如 JConsole、VisualVM(注意,從 Oracle JDK 9 開始,VisualVM 已經再也不包含在 JDK 安裝包中)等。這些工具具體使用起來相對比較直觀,直接鏈接到 Java 進程,而後就能夠在圖形化界面裏掌握內存使用狀況。 以 JConsole 爲例,其內存頁面能夠顯示常見的堆內存和各類堆外部分使用狀態。
也可使用命令行工具進行運行時查詢,如 jstat 和 jmap 等工具都提供了一些選項,能夠查看堆、方法區等使用數據。
或者,也可使用 jmap 等提供的命令,生成堆轉儲(Heap Dump)文件,而後利用 jhat 或 Eclipse MAT 等堆轉儲分析工具進行詳細分析。
若是你使用的是 Tomcat、Weblogic 等 Java EE 服務器,這些服務器一樣提供了內存管理相關的功能。
另外,從某種程度上來講,GC 日誌等輸出,一樣包含着豐富的信息。
這裏有一個相對特殊的部分,就是是堆外內存中的直接內存,前面的工具基本不適用,可使用 JDK 自帶的 Native Memory Tracking(NMT)特性,它會從 JVM 本地內存分配的角度進行解讀。
實際上,垃圾收集器(GC,Garbage Collector)是和具體 JVM 實現緊密相關的,不一樣廠商(IBM、Oracle),不一樣版本的 JVM,提供的選擇也不一樣。接下來,我來談談最主流的 Oracle JDK。
Serial GC,它是最古老的垃圾收集器,「Serial」體如今其收集工做是單線程的,而且在進行垃圾收集過程當中,會進入臭名昭著的「Stop-The-World」狀態。固然,其單線程設計也意味着精簡的 GC 實現,無需維護複雜的數據結構,初始化也簡單,因此一直是 Client 模式下 JVM 的默認選項。 從年代的角度,一般將其老年代實現單獨稱做 Serial Old,它採用了標記 - 整理(Mark-Compact)算法,區別於新生代的複製算法。 Serial GC 的對應 JVM 參數是:
-XX:+UseSerialGC
複製代碼
ParNew GC,很明顯是個新生代 GC 實現,它實際是 Serial GC 的多線程版本,最多見的應用場景是配合老年代的 CMS GC 工做,下面是對應參數
-XX:+UseConcMarkSweepGC -XX:+UseParNewGC
複製代碼
CMS(Concurrent Mark Sweep) GC,基於標記 - 清除(Mark-Sweep)算法,設計目標是儘可能減小停頓時間,這一點對於 Web 等反應時間敏感的應用很是重要,一直到今天,仍然有不少系統使用 CMS GC。可是,CMS 採用的標記 - 清除算法,存在着內存碎片化問題,因此難以免在長時間運行等狀況下發生 full GC,致使惡劣的停頓。另外,既然強調了併發(Concurrent),CMS 會佔用更多 CPU 資源,並和用戶線程爭搶。
Parrallel GC,在早期 JDK 8 等版本中,它是 server 模式 JVM 的默認 GC 選擇,也被稱做是吞吐量優先的 GC。它的算法和 Serial GC 比較類似,儘管實現要複雜的多,其特色是新生代和老年代 GC 都是並行進行的,在常見的服務器環境中更加高效。 開啓選項是:
-XX:+UseParallelGC
複製代碼
另外,Parallel GC 引入了開發者友好的配置項,咱們能夠直接設置暫停時間或吞吐量等目標,JVM 會自動進行適應性調整,例以下面參數:
-XX:MaxGCPauseMillis=value
-XX:GCTimeRatio=N // GC 時間和用戶時間比例 = 1 / (N+1)
複製代碼
G1 GC 這是一種兼顧吞吐量和停頓時間的 GC 實現,是 Oracle JDK 9 之後的默認 GC 選項。G1 能夠直觀的設定停頓時間的目標,相比於 CMS GC,G1 未必能作到 CMS 在最好狀況下的延時停頓,可是最差狀況要好不少。 G1 GC 仍然存在着年代的概念,可是其內存結構並非簡單的條帶式劃分,而是相似棋盤的一個個 region。Region 之間是複製算法,但總體上實際可看做是標記 - 整理(Mark-Compact)算法,能夠有效地避免內存碎片,尤爲是當 Java 堆很是大的時候,G1 的優點更加明顯。 G1 吞吐量和停頓表現都很是不錯,而且仍然在不斷地完善,與此同時 CMS 已經在 JDK 9 中被標記爲廢棄(deprecated),因此 G1 GC 值得你深刻掌握。
談到調優,這必定是針對特定場景、特定目的的事情, 對於 GC 調優來講,首先就須要清楚調優的目標是什麼?從性能的角度看,一般關注三個方面,內存佔用(footprint)、延時(latency)和吞吐量(throughput),大多數狀況下調優會側重於其中一個或者兩個方面的目標,不多有狀況能夠兼顧三個不一樣的角度。固然,除了上面一般的三個方面,也可能須要考慮其餘 GC 相關的場景,例如,OOM 也可能與不合理的 GC 相關參數有關;或者,應用啓動速度方面的需求,GC 也會是個考慮的方面。
基本的調優思路能夠總結爲:
理解應用需求和問題,肯定調優目標。假設,咱們開發了一個應用服務,但發現偶爾會出現性能抖動,出現較長的服務停頓。評估用戶可接受的響應時間和業務量,將目標簡化爲,但願 GC 暫停儘可能控制在 200ms 之內,而且保證必定標準的吞吐量。
掌握 JVM 和 GC 的狀態,定位具體的問題,肯定真的有 GC 調優的必要。具體有不少方法,好比,經過 jstat 等工具查看 GC 等相關狀態,能夠開啓 GC 日誌,或者是利用操做系統提供的診斷工具等。例如,經過追蹤 GC 日誌,就能夠查找是否是 GC 在特定時間發生了長時間的暫停,進而致使了應用響應不及時。
這裏須要思考,選擇的 GC 類型是否符合咱們的應用特徵,若是是,具體問題表如今哪裏,是 Minor GC 過長,仍是 Mixed GC 等出現異常停頓狀況;若是不是,考慮切換到什麼類型,如 CMS 和 G1 都是更側重於低延遲的 GC 選項。
經過分析肯定具體調整的參數或者軟硬件配置。
驗證是否達到調優目標,若是達到目標,便可以考慮結束調優;不然,重複完成分析、調整、驗證這個過程。
Happen-before 關係,是 Java 內存模型中保證多線程操做可見性的機制,也是對早期語言規範中含糊的可見性概念的一個精肯定義。
它的具體表現形式,包括但遠不止是咱們直覺中的 synchronized、volatile、lock 操做順序等方面,例如:
線程內執行的每一個操做,都保證 happen-before 後面的操做,這就保證了基本的程序順序規則,這是開發者在書寫程序時的基本約定。
對於 volatile 變量,對它的寫操做,保證 happen-before 在隨後對該變量的讀取操做。
對於一個鎖的解鎖操做,保證 happen-before 加鎖操做。
對象構建完成,保證 happen-before 於 finalizer 的開始動做。
甚至是相似線程內部操做的完成,保證 happen-before 其餘 Thread.join() 的線程等。
這些 happen-before 關係是存在着傳遞性的,若是知足 a happen-before b 和 b happen-before c,那麼 a happen-before c 也成立。
前面我一直用 happen-before,而不是簡單說先後,是由於它不只僅是對執行時間的保證,也包括對內存讀、寫操做順序的保證。僅僅是時鐘順序上的前後,並不能保證線程交互的可見性。
對於 Java 來講,Docker 畢竟是一個較新的環境,例如,其內存、CPU 等資源限制是經過 CGroup(Control Group)實現的,早期的 JDK 版本(8u131 以前)並不能識別這些限制,進而會致使一些基礎問題:
若是未配置合適的 JVM 堆和元數據區、直接內存等參數,Java 就有可能試圖使用超過容器限制的內存,最終被容器 OOM kill,或者自身發生 OOM。
錯誤判斷了可獲取的 CPU 資源,例如,Docker 限制了 CPU 的核數,JVM 就可能設置不合適的 GC 並行線程數等。
從應用打包、發佈等角度出發,JDK 自身就比較大,生成的鏡像就更爲臃腫,當咱們的鏡像很是多的時候,鏡像的存儲等開銷就比較明顯了。
若是考慮到微服務、Serverless 等新的架構和場景,Java 自身的大小、內存佔用、啓動速度,都存在必定侷限性,由於 Java 早期的優化大可能是針對長時間運行的大型服務器端應用。
注入式(Inject)攻擊是一類很是常見的攻擊方式,其基本特徵是程序容許攻擊者將不可信的動態內容注入到程序中,並將其執行,這就可能徹底改變最初預計的執行過程,產生惡意效果。
下面是幾種主要的注入式攻擊途徑,原則上提供動態執行能力的語言特性,都須要提防發生注入攻擊的可能。
首先,就是最多見的 SQL 注入攻擊。一個典型的場景就是 Web 系統的用戶登陸功能,根據用戶輸入的用戶名和密碼,咱們須要去後端數據庫覈實信息。
假設應用邏輯是,後端程序利用界面輸入動態生成相似下面的 SQL,而後讓 JDBC 執行。
Select * from use_info where username = 「input_usr_name」 and password = 「input_pwd」
複製代碼
可是,若是我輸入的 input_pwd 是相似下面的文本,
「 or 「」=」
複製代碼
那麼,拼接出的 SQL 字符串就變成了下面的條件,OR 的存在致使輸入什麼名字都是複合條件的。
Select * from use_info where username = 「input_usr_name」 and password = 「」 or 「」 = 「」
複製代碼
這裏只是舉個簡單的例子,它是利用了指望輸入和可能輸入之間的誤差。上面例子中,指望用戶輸入一個數值,但實際輸入的則是 SQL 語句片斷。相似場景能夠利用注入的不一樣 SQL 語句,進行各類不一樣目的的攻擊,甚至還能夠加上「;delete xxx」之類語句,若是數據庫權限控制不合理,攻擊效果就多是災難性的。
第二,操做系統命令注入。Java 語言提供了相似 Runtime.exec(…) 的 API,能夠用來執行特定命令,假設咱們構建了一個應用,以輸入文本做爲參數,執行下面的命令:
ls –la input_file_name
複製代碼
可是若是用戶輸入是 「input_file_name;rm –rf /*」,這就有可能出現問題了。固然,這只是個舉例,Java 標準類庫自己進行了很是多的改進,因此相似這種編程錯誤,未必能夠真的完成攻擊,但其反映的一類場景是真實存在的。
第三,XML 注入攻擊。Java 核心類庫提供了全面的 XML 處理、轉換等各類 API,而 XML 自身是能夠包含動態內容的,例如 XPATH,若是使用不當,可能致使訪問惡意內容。
還有相似 LDAP 等容許動態內容的協議,都是可能利用特定命令,構造注入式攻擊的,包括 XSS(Cross-site Scripting)攻擊,雖然並不和 Java 直接相關,但也可能在 JSP 等動態頁面中發生。
這個問題可能有點寬泛,咱們能夠用特定類型的安全風險爲例,如拒絕服務(DoS)攻擊,分析 Java 開發者須要重點考慮的點。
DoS 是一種常見的網絡攻擊,有人也稱其爲「洪水攻擊」。最多見的表現是,利用大量機器發送請求,將目標網站的帶寬或者其餘資源耗盡,致使其沒法響應正經常使用戶的請求。
我認爲,從 Java 語言的角度,更加須要重視的是程序級別的攻擊,也就是利用 Java、JVM 或應用程序的瑕疵,進行低成本的 DoS 攻擊,這也是想要寫出安全的 Java 代碼所必須考慮的。例如:
若是使用的是早期的 JDK 和 Applet 等技術,攻擊者構建合法但惡劣的程序就相對容易,例如,將其線程優先級設置爲最高,作一些看起來無害但空耗資源的事情。幸運的是相似技術已經逐步退出歷史舞臺,在 JDK 9 之後,相關模塊就已經被移除。
上一講中提到的哈希碰撞攻擊,就是個典型的例子,對方能夠輕易消耗系統有限的 CPU 和線程資源。從這個角度思考,相似加密、解密、圖形處理等計算密集型任務,都要防範被惡意濫用,以避免攻擊者經過直接調用或者間接觸發方式,消耗系統資源。
利用 Java 構建相似上傳文件或者其餘接受輸入的服務,須要對消耗系統內存或存儲的上限有所控制,由於咱們不能將系統安全依賴於用戶的合理使用。其中特別注意的是涉及解壓縮功能時,就須要防範Zip bomb等特定攻擊。
另外,Java 程序中須要明確釋放的資源有不少種,好比文件描述符、數據庫鏈接,甚至是再入鎖,任何狀況下都應該保證資源釋放成功,不然即便平時可以正常運行,也可能被攻擊者利用而耗盡某類資源,這也算是可能的 DoS 攻擊來源。
因此能夠看出,實現安全的 Java 代碼,須要從功能設計到實現細節,都充分考慮可能的安全影響。
首先,須要對這個問題進行更加清晰的定義:
服務是忽然變慢仍是長時間運行後觀察到變慢?相似問題是否重複出現?
「慢」的定義是什麼,我可以理解是系統對其餘方面的請求的反應延時變長嗎?
第二,理清問題的症狀,這更便於定位具體的緣由,有如下一些思路:
問題可能來自於 Java 服務自身,也可能僅僅是受系統裏其餘服務的影響。初始判斷能夠先確認是否出現了意外的程序錯誤,例如檢查應用自己的錯誤日誌。 對於分佈式系統,不少公司都會實現更加系統的日誌、性能等監控系統。一些 Java 診斷工具也能夠用於這個診斷,例如經過 JFR(Java Flight Recordera>),監控應用是否大量出現了某種類型的異常。 若是有,那麼異常可能就是個突破點。 若是沒有,能夠先檢查系統級別的資源等狀況,監控 CPU、內存等資源是否被其餘進程大量佔用,而且這種佔用是否不符合系統正常運行情況。
監控 Java 服務自身,例如 GC 日誌裏面是否觀察到 Full GC 等惡劣狀況出現,或者是否 Minor GC 在變長等;利用 jstat 等工具,獲取內存使用的統計信息也是個經常使用手段;利用 jstack 等工具檢查是否出現死鎖等。
若是還不能肯定具體問題,對應用進行 Profiling 也是個辦法,但由於它會對系統產生侵入性,若是不是很是必要,大多數狀況下並不建議在生產系統進行。
定位了程序錯誤或者 JVM 配置的問題後,就能夠採起相應的補救措施,而後驗證是否解決,不然還須要重複上面部分過程。
我認爲,「Lambda 能讓 Java 程序慢 30 倍」這個爭論實際反映了幾個方面:
第一,基準測試是一個很是有效的通用手段,讓咱們以直觀、量化的方式,判斷程序在特定條件下的性能表現。
第二,基準測試必須明肯定義自身的範圍和目標,不然頗有可能產生誤導的結果。前面代碼片斷自己的邏輯就有瑕疵,更多的開銷是源於自動裝箱、拆箱(auto-boxing/unboxing),而不是源自 Lambda 和 Stream,因此得出的初始結論是沒有說服力的。
第三,雖然 Lambda/Stream 爲 Java 提供了強大的函數式編程能力,可是也須要正視其侷限性:
通常來講,咱們能夠認爲 Lambda/Stream 提供了與傳統方式接近對等的性能,可是若是對於性能很是敏感,就不能徹底忽視它在特定場景的性能差別了,例如:初始化的開銷。 Lambda 並不算是語法糖,而是一種新的工做機制,在首次調用時,JVM 須要爲其構建CallSite實例。這意味着,若是 Java 應用啓動過程引入了不少 Lambda 語句,會致使啓動過程變慢。其實現特色決定了 JVM 對它的優化可能與傳統方式存在差別。
增長了程序診斷等方面的複雜性,程序棧要複雜不少,Fluent 風格自己也不算是對於調試很是友好的結構,而且在可檢查異常的處理方面也存在着侷限性等。
JVM 在對代碼執行的優化可分爲運行時(runtime)優化和即時編譯器(JIT)優化。運行時優化主要是解釋執行和動態編譯通用的一些機制,好比說鎖機制(如偏斜鎖)、內存分配機制(如 TLAB)等。除此以外,還有一些專門用於優化解釋執行效率的,好比說模版解釋器、內聯緩存(inline cache,用於優化虛方法調用的動態綁定)。
JVM 的即時編譯器優化是指將熱點代碼以方法爲單位轉換成機器碼,直接運行在底層硬件之上。它採用了多種優化方式,包括靜態編譯器可使用的如方法內聯、逃逸分析,也包括基於程序運行 profile 的投機性優化(speculative/optimistic optimization)。這個怎麼理解呢?好比我有一條 instanceof 指令,在編譯以前的執行過程當中,測試對象的類一直是同一個,那麼即時編譯器能夠假設編譯以後的執行過程當中還會是這一個類,而且根據這個類直接返回 instanceof 的結果。若是出現了其餘類,那麼就拋棄這段編譯後的機器碼,而且切換回解釋執行。
固然,JVM 的優化方式僅僅做用在運行應用代碼的時候。若是應用代碼自己阻塞了,好比說併發時等待另外一線程的結果,這就不在 JVM 的優化範疇啦。
所謂隔離級別(Isolation Level),就是在數據庫事務中,爲保證併發數據讀寫的正確性而提出的定義,它並非 MySQL 專有的概念,而是源於ANSI/ISO制定的SQL-92標準。
每種關係型數據庫都提供了各自特點的隔離級別實現,雖然在一般的定義中是以鎖爲實現單元,但實際的實現千差萬別。以最多見的 MySQL InnoDB 引擎爲例,它是基於 MVCC(Multi-Versioning Concurrency Control)和鎖的複合實現,按照隔離程度從低到高,MySQL 事務隔離級別分爲四個不一樣層次:
讀未提交(Read uncommitted),就是一個事務可以看到其餘事務還沒有提交的修改,這是最低的隔離水平,容許髒讀出現。
讀已提交(Read committed),事務可以看到的數據都是其餘事務已經提交的修改,也就是保證不會看到任何中間性狀態,固然髒讀也不會出現。讀已提交仍然是比較低級別的隔離,並不保證再次讀取時可以獲取一樣的數據,也就是容許其餘事務併發修改數據,容許不可重複讀和幻象讀(Phantom Read)出現。
可重複讀(Repeatable reads),保證同一個事務中屢次讀取的數據是一致的,這是 MySQL InnoDB 引擎的默認隔離級別,可是和一些其餘數據庫實現不一樣的是,能夠簡單認爲 MySQL 在可重複讀級別不會出現幻象讀。
串行化(Serializable),併發事務之間是串行化的,一般意味着讀取須要獲取共享讀鎖,更新須要獲取排他寫鎖,若是 SQL 使用 WHERE 語句,還會獲取區間鎖(MySQL 以 GAP 鎖形式實現,可重複讀級別中默認也會使用),這是最高的隔離級別。
至於悲觀鎖和樂觀鎖,也並非 MySQL 或者數據庫中獨有的概念,而是併發編程的基本概念。主要區別在於,操做共享數據時,「悲觀鎖」即認爲數據出現衝突的可能性更大,而「樂觀鎖」則是認爲大部分狀況不會出現衝突,進而決定是否採起排他性措施。
反映到 MySQL 數據庫應用開發中,悲觀鎖通常就是利用相似 SELECT … FOR UPDATE 這樣的語句,對數據加鎖,避免其餘事務意外修改數據。樂觀鎖則與 Java 併發包中的 AtomicFieldUpdater 相似,也是利用 CAS 機制,並不會對數據加鎖,而是經過對比數據的時間戳或者版本號,來實現樂觀鎖須要的版本判斷。
我認爲前面提到的 MVCC,其本質就能夠看做是種樂觀鎖機制,而排他性的讀寫鎖、雙階段鎖等則是悲觀鎖的實現。
有關它們的應用場景,你能夠構建一下簡化的火車餘票查詢和購票系統。同時查詢的人可能不少,雖然具體座位票只能是賣給一我的,但餘票可能不少,並且也並不能預測哪一個查詢者會購票,這個時候就更適合用樂觀鎖。
Spring Bean 生命週期比較複雜,能夠分爲建立和銷燬兩個過程。
首先,建立 Bean 會通過一系列的步驟,主要包括:
實例化 Bean 對象。
設置 Bean 屬性。
若是咱們經過各類 Aware 接口聲明瞭依賴關係,則會注入 Bean 對容器基礎設施層面的依賴。具體包括 BeanNameAware、BeanFactoryAware 和 ApplicationContextAware,分別會注入 Bean ID、Bean Factory 或者 ApplicationContext。
調用 BeanPostProcessor 的前置初始化方法 postProcessBeforeInitialization。
若是實現了 InitializingBean 接口,則會調用 afterPropertiesSet 方法。
調用 Bean 自身定義的 init 方法。
調用 BeanPostProcessor 的後置初始化方法 postProcessAfterInitialization。
建立過程完畢。
你能夠參考下面示意圖理解這個具體過程和前後順序。
第二,Spring Bean 的銷燬過程會依次調用 DisposableBean 的 destroy 方法和 Bean 自身定製的 destroy 方法。
Spring Bean 有五個做用域,其中最基礎的有下面兩種:
Singleton,這是 Spring 的默認做用域,也就是爲每一個 IOC 容器建立惟一的一個 Bean 實例。
Prototype,針對每一個 getBean 請求,容器都會單首創建一個 Bean 實例。
從 Bean 的特色來看,Prototype 適合有狀態的 Bean,而 Singleton 則更適合無狀態的狀況。另外,使用 Prototype 做用域須要通過仔細思考,畢竟頻繁建立和銷燬 Bean 是有明顯開銷的。
若是是 Web 容器,則支持另外三種做用域:
Request,爲每一個 HTTP 請求建立單獨的 Bean 實例。
Session,很顯然 Bean 實例的做用域是 Session 範圍。
GlobalSession,用於 Portlet 容器,由於每一個 Portlet 有單獨的 Session,GlobalSession 提供一個全局性的 HTTP Session。
單獨從性能角度,Netty 在基礎的 NIO 等類庫之上進行了不少改進,例如:
更加優雅的 Reactor 模式實現、靈活的線程模型、利用 EventLoop 等創新性的機制,能夠很是高效地管理成百上千的 Channel。
充分利用了 Java 的 Zero-Copy 機制,而且從多種角度,「斤斤計較」般的下降內存分配和回收的開銷。例如,使用池化的 Direct Buffer 等技術,在提升 IO 性能的同時,減小了對象的建立和銷燬;利用反射等技術直接操縱 SelectionKey,使用數組而不是 Java 容器等。
使用更多本地代碼。例如,直接利用 JNI 調用 Open SSL 等方式,得到比 Java 內建 SSL 引擎更好的性能。
在通訊協議、序列化等其餘角度的優化。
總的來講,Netty 並無 Java 核心類庫那些強烈的通用性、跨平臺等各類負擔,針對性能等特定目標以及 Linux 等特定環境,採起了一些極致的優化手段。
首先,咱們須要明確一般的分佈式 ID 定義,基本的要求包括:
全局惟一,區別於單點系統的惟一,全局是要求分佈式系統內惟一。
有序性,一般都須要保證生成的 ID 是有序遞增的。例如,在數據庫存儲等場景中,有序 ID 便於肯定數據位置,每每更加高效。
目前業界的方案不少,典型方案包括:
基於數據庫自增序列的實現。這種方式優缺點都很是明顯,好處是簡單易用,可是在擴展性和可靠性等方面存在侷限性。
基於 Twitter 早期開源的Snowflake的實現,以及相關改動方案。這是目前應用相對比較普遍的一種方式,其結構定義你能夠參考下面的示意圖。
總體長度一般是 64 (1 + 41 + 10+ 12 = 64)位,適合使用 Java 語言中的 long 類型來存儲。
頭部是 1 位的正負標識位。
緊跟着的高位部分包含 41 位時間戳,一般使用 System.currentTimeMillis()。
後面是 10 位的 WorkerID,標準定義是 5 位數據中心 + 5 位機器 ID,組成了機器編號,以區分不一樣的集羣節點。
最後的 12 位就是單位毫秒內可生成的序列號數目的理論極限。
Snowflake 的官方版本是基於 Scala 語言,Java 等其餘語言的參考實現有不少,是一種很是簡單實用的方式,具體位數的定義是能夠根據分佈式系統的真實場景進行修改的,並不必定要嚴格按照示意圖中的設計。
Redis、Zookeeper、MongoDB 等中間件,也都有各類惟一 ID 解決方案。其中一些設計也能夠算做是 Snowflake 方案的變種。例如,MongoDB 的ObjectId提供了一個 12 byte(96 位)的 ID 定義,其中 32 位用於記錄以秒爲單位的時間,機器 ID 則爲 24 位,16 位用做進程 ID,24 位隨機起始的計數序列。
國內的一些大廠開源了其自身的部分分佈式 ID 實現,InfoQ 就曾經介紹過微信的seqsvr,它採起了相對複雜的兩層架構,並根據社交應用的數據特色進行了針對性設計,具體請參考相關代碼實現。另外,百度、美團等也都有開源或者分享了不一樣的分佈式 ID 實現,均可以進行參考。
關於第二個問題,Snowflake 是否受冬令時切換影響?
我認爲沒有影響,你能夠從 Snowflake 的具體算法實現尋找答案。咱們知道 Snowflake 算法的 Java 實現,大都是依賴於 System.currentTimeMillis(),這個數值表明什麼呢?從 Javadoc 能夠看出,它是返回當前時間和 1970 年 1 月 1 號 UTC 時間相差的毫秒數,這個數值與夏 / 冬令時並無關係,因此並不受其影響。