Java效能問題一直困擾著廣大程式設計師,由於平臺複雜性,要定位問題,找出其根源確實很難。隨著10多年Java平臺的改進以及新出現的多核多處理器,Java軟體的效能和擴充套件性已經今非昔比了。現代JVM持續演進,內建了更為成熟的優化技術、執行時技術和垃圾收集器。與此同時,底層的硬體平臺和作業系統也在演化。
目錄:
一、Java效能優化系列之一--設計優化
二、Java效能優化系列之二--程式優化
三、Java效能優化系列之三--併發程式設計詳解
四、Java效能優化系列之四--Java記憶體管理與垃圾回收機制詳解
五、Java效能優化系列之五--JavaIO
現代大規模關鍵性系統中的Java效能調優,是一項富有挑戰的任務。你需要關注各種問題,包括演算法結構、記憶體分配模式以及磁碟和檔案I/O的使用方式。效能調優最困難的通常是找到問題所在,即便是經驗豐富的人也會被他們的直覺所誤導。效能殺手總是隱藏在最意想不到的地方。
這一次,我將在本文中著重介紹Java效能優化的一系列舉措,希望能夠對現在的你有所幫助。覺得有幫助的朋友也可以轉發、收藏以下。
一、Java效能優化系列之一--設計優化
1、善於利用 Java 中的設計模式:享元模式、代理模式、裝飾器模式等。詳見我的上一篇文章設計模式之我說
文章連結:https://www.toutiao.com/i6540181471888409102/
2、Java 中的緩衝區:
(1)緩衝最常用的場景就是提高 IO 速度:比如 BufferedWriter 可以用來裝飾 FileWriter ,為 FileWriter 加上緩衝。 BufferedOutputStream 可以用來裝飾 FileOutputStream 。使用這兩個裝飾器時候可以指定緩衝區大小,預設的 size 為 8K 。
(2)JavaNIO 中的各種 Buffer 類族,有更加強大的緩衝區控制功能。
(3)除了效能上的優化,緩衝區也可以作為上層元件和下層元件的一種通訊工具,將上層元件好下層元件進行解耦。比如生產者消費者模式中的緩衝區。
2、快取:
(1)比如 Hibernate 採用的兩級快取:一級快取和二級快取。二級快取指的是 sessionFactory層面上的快取, Hibernate 採用的是 EHCache 。一級快取指的是 session 層面上的快取。
3、物件複用技術 -- 池的使用
(1)資料庫連線池:較常使用的資料庫連線池元件是 C3P0 和 Proxool 。其中 C3P0 是伴隨 Hibernate 一起釋出的, Hibernate 預設採用的資料庫連線池。
(2)執行緒池:自定義執行緒池以及 jdk1.5 提供的執行緒池元件。
4、並行代替序列。
5、時間換空間:不引入中間變數實現兩個數字的交換。代價是增加 CPU 運算。
6、空間換時間:使用下標陣列排序。
二、Java效能優化系列之二--程式優化
常用的程式設計優化技巧:
1、字串優化處理
(1)String 類的特點:不變性、針對常量池的優化( String.intern() 方法的意義)
(2)subString 方法的記憶體洩漏 :
(3)字串分割和查詢不要使用 split 函式,效率低,而是使用 StringTokenizer 或者 indexOf結合 subString() 函式完成分割。
(4)用 charAt ()方法代替 startWith ()方法。
(5)對於靜態字串或者變數字串的連線操作, Java 在編譯的時候會進行徹底的優化,將多個連線操作的字串在編譯時合成一個單獨的字串,而不是生成大量的 String 例項。只生成一個物件。
(6)在無需考慮執行緒安全情況下儘量使用 StringBuilder 。
(7)StringBuffer 和 StringBuilder 初始化的時候都可以設定一個初始值,預設是 16B 。如果字串的長度大於 16B 的時候,則需要進行擴容。擴容策略是將原有的容量大小翻倍,以新的容量申請記憶體空間,建立 char 陣列,然後將陣列中的內容複製到這個新的陣列中,使用 Arrays.copyOf() 函式。因此,如果能預先評估 StringBuilder 的大小,則可以節省這些複製操作,從而提高系統的效能。
2、List 介面
( 1 ) ArrayList 和 Vector 的區別:它們幾乎使用了相同的演算法,它們的唯一區別是對多執行緒的支援。 ArrayList 是不安全的,而 Vector 是執行緒安全的。
( 2 ) LinkedList 和 ArrayList 的區別:
|---1 、 linkedList 採用連結串列實現,適合於資料刪除和插入非常頻繁的情況,不適合隨機訪問。
|---2 、 ArrayList 採用陣列實現,適用於隨機查詢和順序讀的情況,不適合刪除和插 入資料非常頻繁的場景。
(3)基於陣列的 List 都會有一個容量引數。當 ArrayList 所儲存的元素容量超過其已有大小的時候就會進行擴容,陣列的擴容會導致整個陣列進行一次記憶體複製。因此合理的陣列大小會減小陣列擴容的次數從而提高系統效能。
(4)遍歷列表的時候儘量使用迭代器,速度塊。
2、Map 介面:
(1)HashMap 的實現原理:簡單的說, HashMap 就是將 key 做 hash 演算法,然後將 hash 值對映到記憶體地址,直接取得 key 所對應的資料。在 HashMap 中,底層資料結構使用的是陣列,所謂的記憶體地址指的是陣列的下標索引。
(2)容量引數與擴容:預設情況下, hashmap 的初始容量為 16 ,負載因子為 0.75 ,也就是說當 hashmap 的實際容量達到了初始容量 * 負載因子( hashmap 內部維護的一個 threshold 值)的時候, hashmap 就會進行擴容。在擴容時,會遍歷整個 hashmap ,因此應該設定合理的初始大小和負載因子,可以減小 hashmap 擴容的次數。
(3)LinkedHashMap-- 有序的 HashMap : HashMap 的最大缺點是其無序性,被存入到 Hashmap 中的元素,在遍歷 HashMap 的時候,其輸出不一定按照輸入的順序,而是 HashMap 會根據 hash 演算法設定一個查詢高效的順序。如果希望儲存輸入順序,則需要使用 LinkedHashMap 。LinkedHashmap 在內部又增加了一個連結串列,用於儲存元素的順序。
(4)LinkedList 可以提供兩種型別的順序:一個是元素插入時候的順序,一個是最近訪問的順序。注意: LinkedHashMap 在迭代過程中,如果設定為按照最後訪問時間進行排序,即:每當使用 get() 方法訪問某個元素時,該元素便會移動到連結串列的尾端。但是這個時候會出現異常,因此, LinkedHashMap 工作在這種模式的時候,不能在迭代器中使用 get() 操作。
(5)關於 ConcurrentModificationException :該異常一般會在集合迭代過程中被修改時丟擲。因此,不要在迭代器模式中修改集合的結構。這個特性適合於所有的集合類,包括 HashMap 、 Vector 、 ArrayList 等。
(6)TreeMap-- 如果要對元素進行排序,則使用 TreeMap 對 key 實現自定義排序,有兩種方式:在 TreeMap 的建構函式中注入一個 Comparator 或者使用一個實現了 Comparable 的 key 。
(7)如果需要將排序功能加入 HashMap ,最好是使用 Treemap 而不是在應用程式自定義排序。
(8)HashMap 基於 Hash 表實現, TreeMap 基於紅黑樹實現。
3 、 Map 和 Set 的關係:
( 1 )所有 Set 的實現都只是對應的 Map 的一種封裝,其內部維護一個 Map 物件。即: Set只是相應的 Map 的 Value 是一種特殊的表現形式的一種特例。
( 2 ) Set 主要有三種實現類: HashSet 、 LinkedHashSet 、 TreeSet 。其中 HashSet 是基於 Hash 的快速元素插入,元素之間無序。 LinkedHashSet 同時維護著元素插入順序,遍歷集合的時候,總是按照先進先出的順序排序。 TreeSet 是基於紅黑樹的實現,有著高效的基於元素 Key 的排序演算法。
4 、優化集合訪問程式碼:
( 1 )、分離迴圈中被重複呼叫的程式碼:例如, for 迴圈中使用集合的 size() 函式,則不應該把這個函式的呼叫放到迴圈中,而是放到迴圈外邊、
( 2 )、省略相同的操作:
5 、 RandomAccess 介面:通過 RandomAccess 可知道 List 是否支援隨機快速訪問。同時,如果應用程式需要通過索引下標對 List 做隨機訪問,儘量 buyaoshiyongLinkedList , ArrayList 或者 Vector 可以。
6 、 JavaNIO 的特性:
1 、為所有的原始型別提供 Buffer 支援。
2 、使用 Java.nio.charset.Charset 作為字元編碼解碼解決方案。
3 、增加通道抽象代替原有的 IO 流抽象。
4 、支援鎖和記憶體對映檔案的檔案訪問介面。
5 、提供基於 Selector 的非同步網路 IO 。
7 、 Java 中 NIO 的使用。 Channel 是一個雙向通道,即可讀也可寫。應用程式不能直接操作 Channel ,必須藉助於 Buffer 。例如讀資料的時候,必須把資料從通道讀入到緩衝區,然後在緩衝區中進行讀取。以檔案讀取為例,首先通過檔案輸入流獲得檔案通道,然後把檔案通道的內容讀入到緩衝區中,然後就可以對緩衝區操作。
8 、 Buffer 的基本原理:
1 、 Buffer 的建立: Buffer 的靜態 allocate(int size) 方法或者 Buffer.wrap(byte[]src) 。
2 、 Buffer 的工作原理:三個變數: position ,代表當前緩衝區的位置,寫緩衝區的時候,將從 position 的下一個位置寫資料。 Capacity ,代表緩衝區的總容量上限。 Limit ,緩衝區的實際上限,也就是說,讀資料的時候,資料即是從 position 到 limit 之間的資料
3 、 flip 操作: limit=position,position=0, 一般是在讀寫切換的時候使用。寫完資料之後,需要限定下有效資料範圍,才能讀資料;
4 、 clear 操作: position-0 , limit=capacity. 。為重新寫入緩衝區做準備。
5 、 rewind 操作: position=0 ,為讀取緩衝區中有效資料做準備,一半 limit 已經被合理設定。
9 、讀寫緩衝區:
1 、 public byte get() :順序讀取緩衝區的一個位元組, position 會加一
2 、 public Buffer get(byte[]dst): 將緩衝區中的資料讀入到陣列 dst 中,並適當的移動 position
3 、 public byte get(int index) :得到第 index 個位元組,但不移動 posoiion
4 、 public ByteBuffer put(byte b) :將位元組 b 放入到緩衝區中,並移動 position
5 、 public ByteBuffer put(int index,byte b) :將位元組 b 放到緩衝區的 index 位位置
6 、 pubglic final ByteBuffer(byte[]src) :將位元組陣列 src 放到緩衝區中。
10 、標誌緩衝區:類似於一個書籤的功能,在資料的處理過程中,可隨時記錄當前位置。然後在任意時刻,回到這個位置。 Mark 用於記錄當前位置, reset 用於恢復到 mark 所在的位置、
11 、複製緩衝區:使用 Buffer 的 duplicate 方法可以複製一個緩衝區,副本緩衝區和原緩衝區共享一份空間但是有有著獨立的 position 、 capacity 和 limit 值。
20 、緩衝區分片:緩衝區分片使用 slice 方法實現。它將在現有的緩衝區中,建立的子緩衝區。子緩衝區和父緩衝區共享資料。這個方法有助於將系統模組化。緩衝區切片可以將一個大緩衝區進行分割處理,得到的子緩衝區都具有緩衝的緩衝區模型結構;因此。這個操作有助於系統的模組化。
12 、只讀緩衝區:只讀緩衝區可以保證核心資料的安全,如果不希望資料被隨意篡改,返回一個只讀緩衝區是很有幫助的。
13 、檔案對映到記憶體: NIO 提供了一種將檔案對映到記憶體的方法進行 IO 操作,這種方法比基於流 IO 快很多。這個操作主要由 FileChanne.map() 操作。使用檔案記憶體的方式,將文字通過 FileChannel 對映到記憶體中。然後從記憶體中讀取資料。同時,通過修改 Buffer, 將對記憶體中資料的修改寫到對應的硬碟檔案中。
14 、處理結構化資料:散射和聚集。散射就是將資料讀入到一組 bytebuffer 中,而聚集正好相反。通過 ScatteringByteChannel 和 GatheringByteChannel 可以簡化對結構資料的操作。
15 、直接記憶體訪問: DirectBuffer 直接分配在實體記憶體中,並不佔用對空間,因此也不受對空間限制。 DirectBuffer 的讀寫操作比普通 Buffer 塊,因為 DirectBuffer 直接操縱的就是核心緩衝區。
16 、引用型別:強、軟、若、虛四種引用型別。
WeakHashMap :是弱引用的一中典型應用,它使用弱引用作為內部資料的儲存方案。可以作為簡單的快取表解決方案。
如果在系統中,需要一張很大的 Map 表, Map 中的表項作為快取之用。這也意味著即使沒能從該 Map 中取得相應地資料,系統也可以通過選項方案獲取這些資料,雖然這樣會消耗更多的時間,但是不影響系統的正常執行。這個時候,使用 WeakHashMap 是最合適的。因為 WeakHashMap 會在系統記憶體範圍內,儲存所有表項,而一旦記憶體不夠,在 GC 時,沒有被引用的又會很快被清除掉,避免系統記憶體溢位。
17 、有助於改善系統效能的技巧:
1 、慎用異常: for 迴圈中使用 try-catch 會大大降低系統效能
2 、使用區域性變數:區域性變數的訪問速度遠遠高於類的靜態變數的訪問速度,因為類的 變數是存在在堆空間中的。
3 、位運算代替乘除法:右移代表除以二、左移代表乘以二。
4 、有的時候考慮是否可以使用陣列代替位運算。
5 、一維陣列代替二維陣列。
6 、提取表示式:儘可能讓程式少做重複的計算,尤其要關注在迴圈體的程式碼,從迴圈提中提取重複的程式碼可以有效的提升系統效能。
三、Java效能優化系列之三--併發程式設計詳解
1、併發程式設計模式:
( 1 )、 Future-Callable 模式: FutureTask 類實現了 Runnable 介面,可以作為單獨的執行緒執行,其 Run 方法中通過 Sync 內部類呼叫 Callable 介面,並維護 Callable 介面的返回值。當呼叫FutureTask.get() 的時候將返回 Callable 介面的返回物件。 Callable 介面是使用者自定義的實現,通過實現 Callable 介面的 call() 方法,指定 FutureTask 的實際工作內容和返回物件。 Future 取得的結果型別和 Callable 返回的型別必須一致,這是由定義 FutureTask 的時候指定泛型保證的。 Callable 要採用 ExecutorSevice 的 submit 方法提交,返回的 future 物件可以取消任務。
( 2 )、 Master-Worker 格式:其核心思想是系統由兩類程式協作工作: Master 程式和 Worker 程式。 Master 程式負責接收和分配任務, Worker 負責處理子任務。當各個子任務處理完成後,將結果返回給 Master 程式。由 Master 程式進行歸納會彙總,從而得到系統的最終結果。
( 3 )、保護暫停模式:其核心思想是僅當服務程式準備好時,才提供服務。設想一種場景,伺服器會在很短時間內承受大量的客戶端請求,客戶端請求的數量可能超過伺服器本身的即時處理能力。為了不丟棄任意一個請求,最好的方式就是將這個客戶端進行排列,由伺服器逐個處理。
( 4 )、不變模式:為了儘可能的去除這些由於執行緒安全而引發的同步操作,提高並行程式效能 ,可以使用一種不可變的物件,依靠物件的不變性,可以確保在沒有同步操作的多執行緒環境中依然保持內部狀態的一致性和正確性。
( 5 )、 Java 實現不變模式的條件:
1) 、去除 setter 方法以及所有修改自身屬性的方法。
2 )、將所有屬性設定為私有,並用 final 標記,確保其不可修改。
3 )、確保沒有子類可以過載修改它的行為。
4 )、有一個可以建立完整物件的建構函式。
Java 中,不變模式的使用有: java.lang.String 類。以及所有的後設資料類包裝類。
(6)、生產者 - 消費者模式:生產者程式負責提交使用者請求,消費者程式負責具體處理生產者程式提交的任務。生產者和消費者之間通過共享記憶體緩衝區進行通訊。通過 Java 提供和餓 BlockingQueue 可以實現生產者消費者模式。
2、JDK 多工執行框架:
( 1 )、簡單執行緒池實現:執行緒池的基本功能就是進行執行緒的複用。當系統接受一個提交的任務,需要一個執行緒時,並不著急立即去建立程式,而是先去執行緒池查詢是否有空餘的程式,若有則直接使用執行緒池中的執行緒工作。如果沒有,則再去建立新的程式。待任務完成後,不是簡單的銷燬程式,而是將執行緒放入執行緒池的空閒佇列,等待下次使用。使用執行緒池之後,執行緒的建立和關閉通常由執行緒池維護,執行緒通常不會因為會執行晚一次任務而被關閉,執行緒池中的執行緒會被多個任務重複使用。
( 2 )、 Executor 框架: Executor 框架提供了建立一個固定執行緒數量的執行緒池、返回一個只有一個執行緒的執行緒池、建立一個可根據實際情況進行執行緒數量調整的執行緒池、可排程的單執行緒池以及可變執行緒數量的可排程的執行緒池。
( 3 )、自定義執行緒池 : 使用 ThreadPoolExecutor 介面: ThreadPoolExecutor 的建構函式引數如下:
corePoolSize :指的是保留的執行緒池大小
maximumPoolSize : 指的是執行緒池的最大大小
keepAliveTime :指的是空閒執行緒結束的超時時間
Unit : 是一個列舉,表示 keepAliveTime 的單位
workQueue : 表示存放任務的佇列。
ThreadFactory :建立執行緒的時候,使用到的執行緒工廠
handler : 當執行緒達到最大限制,並且工作佇列裡面也已近存放滿了任務的時候,決定如何處理提交到執行緒池的任務策略
上述的幾種執行緒池的內部實現均使用了 ThreadPoolExecutor 介面。我們可以自定義提交但是未被執行的任務佇列被執行的順序,常見的有直接提交的佇列、有界的任務佇列、無界的任務佇列、優先任務佇列,這樣可以在系統繁忙的時候忽略任務的提交先後次序,總是讓優先順序高的任務先執行。使用優先佇列時,必須讓 target 實現 Comparable 介面。
(4)、優化執行緒池大小: NThreads=Ncpi*Ucpu*(1+W/C) , Java 中使用: Runtime.getRuntime().availableProcesses() 獲取可用的 CPU 數量。
3、JDK 併發資料結構:
( 1 )併發 List : Vector 或者 CopyOnWriteArrayList 是兩個執行緒安全的 List 實現。
CopyOnWriteArrayList 很好的利用了物件的不變性,在沒有對物件進行寫操作之前,由於物件未發生改變,因此不需要加鎖。而在試圖改變物件的時候,總是先獲得物件的一個副本,然後對副本進行修改,最後將副本寫回。 CopyOnWriteArrayList 適合讀多寫少的高併發場合。而 Vector適合高併發寫的場合。
( 2 )併發 Set : synchronizedSet 適合高併發寫的情景、 CopyOnWriteSet 適合讀多寫少的高併發場合。
( 3 )併發 Map : ConcurrentHashMap 是專門為執行緒併發而設計的 HashMap ,它的 get 操作是無鎖的,其 put 操作的鎖粒度小於 SynchronizedHashMap ,因此其整體效能優於 SynchronizedHashMap 。
( 4 )併發 Queue :在併發佇列上, JDK 提供了兩種實現,一個是以 ConcurrentLinkedQueue 為代表的高效能佇列,一個是以 BlockingQueue 介面為代表的阻塞佇列。如果需要一個能夠在高併發時,仍能保持良好效能的佇列,可以使用 ConcurrentLinkedQueue 物件。而 BlockingQueue的主要適用場景就是生產者消費者模式中的實現資料共享。 BlockingQueue 介面主要有兩種實現: ArrayBlockingQueue 是一種基於陣列的阻塞佇列實現,也就是說其內部維護著一個定長陣列,用於快取佇列中的資料物件。 LinkedBlockingQueue 則使用一個連結串列構成的資料緩衝佇列。
4 、併發控制方法:
( 1 )、 Java 中的記憶體模型與 Volatile :在 Java 中,每一個執行緒有一塊工作記憶體區,其中存放著被所有執行緒共享的主記憶體中的變數的值的拷貝。當執行緒執行時,它在自己的記憶體中操作變數。為了存取一個共享的變數,一個執行緒通常要先獲取鎖定並且清除它的記憶體緩衝區,這保證該共享變數從所有執行緒的共享記憶體區正確地裝入到執行緒的工作記憶體區;當執行緒解鎖時保證該工作記憶體區中變數的值寫回到共享記憶體中。
( 2 )、 Volatile 關鍵字:宣告為 Volatile 的變數可以做以下保證:
1 )、其他執行緒對變數的修改,可以隨即反應在當前程式中。
2 )、確保當前執行緒對 Volatile 變數的修改,能隨即寫回到共享主記憶體中,並被其他執行緒所見
3 )、使用 Volatile 宣告的變數,編譯器會保證其有序性。
4 )、 double 和 long 型別的非原子處理:如果一個 double 型別或者 long 型別的變數沒有被宣告為 volatile 型別,則變數在進行 read 和 write 操作的時候,主記憶體會把它當成兩個 32 位的read 或者 write 操作。因此,在 32 為作業系統中,必須對 double 或者 long 進行同步
原因在於:使用 Volatile 標誌變數,將迫使所有執行緒均讀寫主記憶體中的對應變數,從而使得 Volatile 變數在多執行緒間可見。
(3)、同步關鍵字 -Synchronized ,其本質是一把鎖: Synchronized 關鍵字可以作用在方法或者程式碼塊中。當作用的是成員方法時,預設的鎖是該物件 this ,這個時候一般在共享資源上進行Synchronized 操作。該關鍵字一般和 wait ()和 notify ()方法一起使用,呼叫這兩個方法的時候一般指的是資源本身。由於所有的物件都能當成資源,因此這兩個方法是從 Object 繼承而來的,而不是 Thread 或者 Runnable 才具有的方法。
(4)、 ReentrantLock 鎖:比 Synchronized 的功能更強大,可中斷、可定時。所有使用內部鎖實現的功能,都可以使用重入鎖實現。重入鎖必須放入 finally 塊中進行釋放,而內部鎖可以自動釋放。 重入鎖有著更強大的功能,比如提供了鎖等待時間 (boolean tryLock(long time.TimeUnit unit)) 、支援鎖中斷 (lockInterruptibly()) 和快速鎖輪詢 (boolean tryLock()) 以及一套 Condition 機制,這個機制類似於內部鎖的 wait() 和 notify() 方法。
(5)、 ReadWriteLock :讀寫分列鎖。如果 系統中讀操作次數遠遠大於寫操作,而讀寫鎖就可以發揮巨大的作用。
(6)Condition 物件: await() 方法和 signal() 方法。 Condition 物件需要和重入鎖( ReentrantLock )配合工作以完成多執行緒協作的控制。
(7)Semaphore 訊號量:訊號量為多執行緒寫作提供了更為強大的控制方法。廣義上講,訊號量是對鎖的擴充套件。無論是內部鎖( Synchronized )還是重入鎖( ReentrantLock ),一次都只允許一個程式訪問一個資源。而訊號量卻可以指定多個執行緒同時訪問某一個資源。
(8)ThreadLocal 執行緒區域性變數: ThreadLocal 是一種多執行緒間併發訪問變數的解決方案。與synchronized 等加鎖方式不同, ThreadLocal 完全不提供鎖,而使用以空間換時間的手段,為每個執行緒提供變數的獨立副本,以保障執行緒安全,因此並不是一種資料共享的解決方案。
5、同步工具類:
( 1 ) CountDownLatch (閉鎖):確保一個服務不會開始,直到它依賴的其他服務都準備就緒。 CountDownLatch 作用猶如倒數計時計數器,呼叫 CountDownLatch 物件的 countDown 方法就將計數器減 1 ,當計數到達 0 時,則所有等待者或單個等待者開始執行。比如有 10 個運動員的田徑比賽 , ,有兩個裁判 A 和 B , A 在起點吹哨起跑, B 在終點記錄記錄並公佈每個運動員的成績。剛開始的時候,運動員們都趴在跑道上( A.await() )等到裁判吹哨。 A 吹哨耗費了 5 秒,此時呼叫 A.countDown() 方法將等待時間減為 4 秒。當減為 0 的時候,所有的運動員開始起跑。這個時候, B 裁判開始工作。啟動一個初始值為 10 的定時器,每當有一個運動員跑到重點的時候,就將計數器減一,代表已經有一個運動員跑到終點。當計時器為 0 的時候,代表所有的運動員都跑到了終點。此時可以根據公佈成績了。
( 2 ) CylicBarrier (關卡):
1 )、類似於閉鎖,它們能夠阻塞一組執行緒直到某些事件發生
2 )、與同步鎖的不同之處是一個可以重用,一個不可以重用
3 )、所有執行緒必須同時到達關卡點,才能繼續處理。
類似組團旅遊,導遊就是一個關卡。表示大家彼此等待,大家集合好後才開始出發,分散活動後又在指定地點集合碰面,這就好比整個公司的人員利用週末時間集體郊遊一樣,先各自從家出發到公司集合後,再同時出發到公園遊玩,在指定地點集合後再同時開始就餐。
( 3 ) Exchanger :使用在兩個夥伴執行緒之間進行資料交換,這個交換對於兩個執行緒來說都是安全的。
講解 Exchanger 的比喻:好比兩個毒販要進行交易,一手交錢、一手交貨,不管誰先來到接頭地點後,就處於等待狀態了,當另外一方也到達了接頭地點(所謂到達接頭地點,也就是到到達了準備接頭的狀態)時,兩者的資料就立即交換了,然後就又可以各忙各的了。
exchange 方法就相當於兩手高高舉著待交換物,等待人家前來交換,一旦人家到來(即人家也執行到 exchange 方法),則兩者立馬完成資料的交換。
5、關於死鎖:
(1)死鎖的四個條件:
1) 、互斥條件:一個資源只能被一個執行緒使用:
2 )、請求與保持條件:一個執行緒因請求資源而阻塞時,對已獲得則資源保持不放。
3 )、不剝奪條件:程式已經獲得的資源,在未使用完之前,不能強行剝奪。
4 )、迴圈等待條件:若干個執行緒已經形成了一種頭尾相接的迴圈等待資源關係。
(2)常見的死鎖:靜態順序死鎖、動態順序死鎖、協作物件間的死鎖、執行緒飢餓死鎖。
(3)如何儘量避免死鎖:
1 )、制定鎖的順序,來避免死鎖
2 )、嘗試使用定時鎖( lock.tryLock(timeout) )
3 )、在持有鎖的方法中進行其他方法的呼叫,儘量使用開放呼叫(當呼叫方法不需要持有鎖時,叫做開放呼叫)
4 )、減少鎖的持有時間、減小鎖程式碼塊的粒度。
5 )、不要將功能互斥的 Task 放入到同一個 Executor 中執行。
6 、 程式碼層面對鎖的優化機制:
1 、避免死鎖
2 、減少鎖持有時間,程式碼塊級別的鎖,而不是方法級別的鎖
3 、減小鎖粒度, ConcurrentHashMap 分段加鎖
4 、讀寫鎖代替獨佔鎖
5 、鎖分離,例如 LinkedBlockingQueue 的尾插頭出的特點,用兩把鎖 (putLock takeLock) 分離兩種操作。
6 、重入鎖和內部鎖
重入鎖( ReentrantLock )和內部鎖( Synchronized ):所有使用內部鎖實現的功能,都可以使用重入鎖實現。重入鎖必須放入 finally 塊中進行釋放,而內部鎖可以自動釋放。
重入鎖有著更強大的功能,比如提供了鎖等待時間 (boolean tryLock(long time.TimeUnit unit))、支援鎖中斷 (lockInterruptibly()) 和快速鎖輪詢 (boolean tryLock()) 以及一套 Condition 機制,這個機制類似於內部鎖的 wait() 和 notify() 方法。想要獲取多執行緒面試題的可以加群:650385180,面試題及答案在群的共享區。
7 、鎖粗化:虛擬機器在遇到一連串連續的對同一個鎖不斷進行請求和釋放從操作的時候,便會把所有的鎖操作整合成對鎖的一次請求,從而減少對鎖的請求同步次數。
7 、 Java 虛擬機器層面對鎖的優化機制:
1、自旋鎖:由於執行緒切換(執行緒的掛起和恢復)消耗的時間較大,則使執行緒在沒有獲得鎖時,不被掛起,而轉而執行一個空迴圈。在若干空迴圈後,執行緒如果獲得了鎖,而繼續執行,若執行緒依然不能獲得鎖,而才被掛起。
2、鎖消除: JVM 通過對上下文的掃描,去除不可能存在共享資源競爭的鎖,這樣可以節省毫無意義的請求鎖時間。比如單執行緒中或者非共享資源的常使用的 StringBuffer 和 Vector 。
3、鎖偏向:若某一個鎖被執行緒獲取後,便進入偏向模式,當執行緒再次請求這個鎖時,無需進行相關的同步操作,從而節省了操作時間。
8 、 Java 無鎖實現併發的機制:
( 1 )非阻塞的同步 / 無鎖: ThreadLocal ,讓每個程式擁有各自獨立的變數副本,因此在平行計算時候,無須相互等待而造成阻塞。 CVS 演算法的無鎖併發控制方法。
( 2 )原子操作: java.util.concurrent.atomic 包。
四、Java效能優化系列之四--Java記憶體管理與垃圾回收機制詳解
1 、 JVM 執行時資料區域。
( 1 )、程式計數器:每一個 Java 執行緒都有一個程式計數器來用於儲存程式執行到當前方法的哪一個指令。此記憶體區域是唯一一個在 JVM Spec 中沒有規定任何 OutOfMemoryError 情況的區域。
( 2 )、 Java 虛擬機器棧:該塊記憶體描述的是 Java 方法呼叫的記憶體模型,每個方法在被執行的時候,都會同時建立一個幀( Frame )用於儲存本地變數表、操作棧、動態連結、方法出入口等資訊。
( 3 )、本地方法棧。本地方法呼叫的記憶體模型。
( 4 )、 Java 堆。 Java 中的物件以及類的靜態變數的存放地方。
( 5 )、方法區:方法區中存放了每個 Class 的結構資訊,包括常量池、欄位描述、方法描述等等
( 6 )、執行時常量池: Class 檔案中除了有類的版本、欄位、方法、介面等描述等資訊外,還有一項資訊是常量表 (constant_pool table) ,用於存放編譯期已可知的常量,這部分內容將在類載入後進入方法區(永久代)存放。但是 Java 語言並不要求常量一定只有編譯期預置入 Class 的常量表的內容才能進入方法區常量池,執行期間也可將新內容放入常量池(最典型的 String.intern() 方法)。執行時常量池是方法區的一部分,自然受到方法區記憶體的限制,當常量池無法在申請到記憶體時會丟擲 OutOfMemoryError 異常。
( 7 )、本機直接記憶體( Direct Memory )
在 JDK1.4 中新加入了 NIO 類,引入一種基於渠道與緩衝區的 I/O 方式,它可以通過本機 Native 函式庫直接分配本機記憶體,然後通過一個儲存在 Java 堆裡面的 DirectByteBuffer 物件作為這塊記憶體的引用進行操作。這樣能在一些場景中顯著提高效能,因為避免了在 Java 對和本機堆中來回複製資料。
2、Java 類載入機制的特點:
(1)基於父類的委託機制:執行一個程式時,總是由 AppClass Loader (系統類載入器)開始載入指定的類,在載入類時,每個類載入器會將載入任務上交給其父,如果其父找不到,再由自己去載入, Bootstrap Loader (啟動類載入器)是最頂級的類載入器了,其父載入器為 null 。如果父類載入器找不到給定的類名,則交由子載入器去載入,如果最低一層的子載入器也無法找到,則丟擲異常。
(2)全盤負責機制:所謂全盤負責,就是當一個類載入器負責載入某個 Class 時,該 Class 鎖依賴的和引用的其他 Class 也將由該類載入器負責載入,除非顯式使用另外一個類載入器來載入。
(3)快取機制:快取機制將會保證所有載入過的 Class 物件都會被快取,當程式中需要使用某個 Class 時,類載入器會先從緩衝區中搜尋該 Class ,只有當快取區中不存在該 Class 物件時,系統才會讀取該類對應的二進位制資料,並將其轉化為 Class 物件,存入快取區中。這就是為什麼修改了 Class 後,必須重新啟動 JVM ,程式所做的修改才會生效的原因。同時,往們比較 A.getClass() 與 B.getClass() 是否相等時,直接使用 == 比較,因為快取機制保證類的位元組碼在記憶體中只可能存在一份。
(4)類載入器的三種方法以及其區別:
1)、命令列啟動應用時候由 JVM 初始化載入
2)、通過 Class.forName() 方法動態載入
3)、通過 ClassLoader.loadClass() 方法動態載入 // 使用 Class.forName() 來載入類,預設會執行初始化塊 , // 使用 Class.forName() 來載入類,並指定 ClassLoader ,初始化時不執行靜態塊。
4)區別:使用 ClassLoader.loadClass() 來載入類,不會執行初始化塊,
3 、類的主動引用
什麼情況下需要開始類載入過程的第一個階段,也即類的初始化階段。 Java 虛擬機器規定了有且只有 5 種情況下必須立即對類進行初始化:
(1)、遇到 new 、 getstatic 、 putstatic 或 invokestatic 這四條位元組碼指令時,如果類沒有進行過初始化,則需要觸發其初始化。(而且初始化的時候按照先父後子的順序)。這四條指令最常見的 Java 程式碼場景是:使用 new 關鍵字例項化物件的時候、讀取或設定一個類的靜態欄位(被 final 修飾,已在編譯時期把結果放入常量池的靜態欄位除外)、呼叫一個類的靜態方法的的時候。
(2)使用 java.lang.reflect 包的方法對類進行反射呼叫的時候,如果類沒有進行過初始化,則需要先對其進行初始化。
(3)當初始化一個類的時候,如果發現其父類還沒有進行過初始化,則需要先觸發其父類的初始化。但是一個介面在初始化時,並不要求其父類介面全部都完成了初始化,只有在真正使用到父介面的時候(如引用介面中定義的常量)才會被初始化。
(4)當虛擬機器啟動時,使用者需要指定一個要執行的主類(包含 main ()方法的那個類),虛擬機器會先初始化這個主類。
(5)當使用 jdk1.7 的動態語言支援時,如果一個 java.lang.invoke.MethodHandle 例項最後的解析結果 REF_getStatic 、 REF_putStatic 、 REF_invokeStatic 的方法控制程式碼,並且這個方法控制程式碼所對應的類沒有初始化過,則需要先觸發其初始化。
4 、類的被動引用
1、對於靜態欄位,只有直接定義這個欄位的類才會被初始化,因此通過其子類來引用父類中定義的靜態欄位,只會觸發父類的初始化而不會觸發子類的初始化。
2、通過陣列定義來引用類,不會觸發類的初始化 SuperClass[]sca=new SuperClass[10].
3、常量在編譯階段會存入呼叫類的常量池中,本質上並沒有直接引用到定義常量的類,因此不會觸發常量的類的初始化。
5 、 Java 物件的建立過程以及如何保證物件建立的多執行緒的安全性:
虛擬機器遇到一條 new 指令時,首先將去檢查這個指令的引數是否能在常量池中定位到一個類的符號引用,並且檢查這個符號引用代表的類是否已經被載入、解析和初始化過。如果沒有則進行類載入過程。
在類載入通過後,接下來虛擬機器將為新生物件分配記憶體。物件所需的記憶體的大小在類載入完成後便可完全確定。為物件分配空間的任務等價於把一塊確定大小的記憶體從 Java 堆中劃分出來。
保證多執行緒的安全性。 有兩種方案,一種是對分配記憶體的動作進行同步操作,實際上虛擬機器採用 CAS 加上失敗重試的方式保證更新操作的原子性。另一種是把記憶體分配的動作按照執行緒劃分在不同的空間中進行。即為每個執行緒在 Java 堆中預先分配一小塊記憶體,成為本地執行緒分配緩衝( TLAB )。哪個執行緒要分配記憶體,就在哪個 TLAB 上分配,只有 TLAB 用完並分配新的 TLAB時,才需要分配新的 TLAB 。
6、 什麼時候判斷一個物件可以被回收?
用可達性分析演算法。這個演算法的基本思路就是通過一系列的成為“ GC roots ”的物件作為起始點,從這些節點開始向下搜尋,如果一個物件到 GCroots 沒有任何引用鏈相連,則證明此物件是不可用的。可作為 GCroots 的物件包括虛擬機器棧中引用的物件、方法區中常量引用的物件、方法區中靜態屬性引用的物件或者本地方法棧中 JNI 引用的物件,這些物件的共同點都是生命週期與程式的生命週期一樣長,一般不會被 GC 。判斷一個物件死亡,至少經歷兩次標記過程:如果物件在進行可達性演算法後,發現沒有與 GC Roots 相連線的引用鏈,那他將會被第一次標記,並在稍後執行其 finalize ()方法。執行是有機會,並不一定執行。稍後 GC 進行第二次標記,如果第一次標記的物件在 finalize ()方法中拯救自己,比如把自己賦值到某個引用上,則第二次標記時它將被移除出“即將回收”的集合,如果這個時候物件還沒有逃脫,那基本上就會被 GC 了。
7 、 關於 finalize ()方法的作用的說明:
finalize ()方法的工作原理理論上是這樣的:一旦垃圾回收器準備好釋放佔用的儲存空間,將首先呼叫其 finalize ()方法,並且在下一次垃圾回收動作發生時,才會真正回收物件佔用的記憶體,所以使用 finalize ()的目的就是在垃圾回收時刻做一些重要的清理工作。我們知道,使用 GC 的唯一原因就是回收程式不再使用的記憶體,所以對於與垃圾回收有關的任何行為來說,包括 finalize() 方法,它們也必須同記憶體及其回收有關。個人認為 Java 物件的 finalize ()方法有兩個作用( 1 )回收通過建立物件方式以外的方式為物件分配了儲存空間。比如,比如在 Java 程式碼中採用了 JNI 操作,即在記憶體分配時,採用了類似 C 語言中的 malloc 函式來分配記憶體,而且沒有呼叫free 函式進行釋放。此時就需要在 finalize ()中用本地方法呼叫 free 函式以釋放記憶體。( 2 )物件終結條件的驗證,即用來判定物件是否符合被回收條件。比如,如果要回收一個物件,物件被清理時應該處於某種狀態,比如說是一個開啟的檔案,在回收之前應該關閉這個檔案。只要物件中存在沒有被適當清理的部分, finalize ()就可以用來最終法相這種情況。因為物件在被清理的時候肯定處於生命週期的最後一個階段,如果此時還含有一些未釋放的資源,則有能力釋放這些資源。這個不是 C/C++ 裡面的解構函式,它執行代價高昂,不確定性大,無法保證各個物件的呼叫順序。需要關閉外部資源之類的事情,基本上它能做的使用 try-finally 可以做的更好。
8 、 一個類被回收的條件。
(1)、該類所有的例項都已經為 GC ,也就是說 JVM 中不存在該 Class 的任何例項。
(2)、載入該類的 ClassLoader 已經被 GC 。
(3)該類對應的 java.lang.Class 物件沒有在任何地方被引用,如不能在任何地方通過反射訪問類的方法。
9、 垃圾回收演算法 :
(1)、標記 - 清除演算法:標記階段根據根節點標記所有從根節點開始的可達物件。則未被標記的物件就是未被引用的垃圾物件,然後在清除階段,清楚所有未被標記的物件。其最大缺點是空間碎片。
(2)、複製演算法:將原有的記憶體空間分為兩塊,每次只使用其中一塊,在垃圾回收時,將正在使用的記憶體中的存活物件複製到未使用的記憶體塊中,之後清楚正在使用的記憶體快中的所有物件,然後交換兩個記憶體的角色。完成垃圾回收。這種演算法比較適合新生代,因為在新生代,垃圾物件通常會多於存活物件,複製演算法效果較好。 Java 的新生代序列 GC 中,就使用了複製演算法的思想。新生代分為 eden 空間、 from 空間和 to 空間三個部分。 From 和 to 空間可以視為用於複製的兩塊大小相同、地位相等、且可以進行角色互換的空間塊。 From 和 to 空間也成為 survivor 空間,即倖存者空間,用於存放未被回收的物件。
(3)、標記 - 壓縮演算法:標記過程與標記清楚演算法一樣,但後續不是直接對可回收物件進行清理,而是讓所有存活的物件向一段移動,然後直接清理掉端邊界以外的記憶體。適合老年代的回收。
(4)、分代收集演算法。
10 、 垃圾回收器。
( 1 )、 Serial 收集器
單執行緒收集器,收集時會暫停所有工作執行緒(我們將這件事情稱之為 Stop The World ,下稱 STW ),使用複製收集演算法,虛擬機器執行在 Client 模式時的預設新生代收集器。
(2)、 ParNew 收集器就是 Serial 的多執行緒版本,除了使用多條收集執行緒外,其餘行為包括演算法、 STW 、物件分配規則、回收策略等都與 Serial 收集器一摸一樣。對應的這種收集器是虛擬機器執行在 Server 模式的預設新生代收集器,在單 CPU 的環境中, ParNew 收集器並不會比 Serial 收集器有更好的效果。
(3)Parallel Scavenge 收集器(下稱 PS 收集器)也是一個多執行緒收集器,也是使用複製演算法,但它的物件分配規則與回收策略都與 ParNew 收集器有所不同,它是以吞吐量最大化(即 GC 時間佔總執行時間最小)為目標的收集器實現,它允許較長時間的 STW 換取總吞吐量最大化。
(4)4.Serial Old 收集器 Serial Old 是單執行緒收集器,使用標記-整理演算法,是老年代的收集器
(5) Parallel Old 收集器
老年代版本吞吐量優先收集器,使用多執行緒和標記-整理演算法, JVM 1.6 提供,在此之前,新生代使用了 PS 收集器的話,老年代除 Serial Old 外別無選擇,因為 PS 無法與 CMS 收集器配合工作。
(6)CMS ( Concurrent Mark Sweep )收集器
CMS 是一種以最短停頓時間為目標的收集器,使用 CMS 並不能達到 GC 效率最高(總體 GC時間最小),但它能儘可能降低 GC 時服務的停頓時間,這一點對於實時或者高互動性應用(譬如證券交易)來說至關重要。
( 7 )、 G1 收集器。
11 、記憶體分配與回收策略:
( 1 )、規則一:通常情況下,物件在 eden 中分配。當 eden 無法分配時,觸發一次 Minor GC 。
( 2 )、規則二:配置了 PretenureSizeThreshold 的情況下,物件大於設定值將直接在老年代分配。
( 3 )、規則三:在 eden 經過 GC 後存活,並且 survivor 能容納的物件,將移動到 survivor 空間內,如果物件在 survivor 中繼續熬過若干次回收(預設為 15 次)將會被移動到老年代中。回收次數由 MaxTenuringThreshold 設定。
( 4 )、規則四:如果在 survivor 空間中相同年齡所有物件大小的累計值大於 survivor 空間的一半,大於或等於該年齡的物件就可以直接進入老年代,無需達到 MaxTenuringThreshold 中要求的年齡。
( 5 )、規則五:在 Minor GC 觸發時,會檢測之前每次晉升到老年代的平均大小是否大於老年代的剩餘空間,如果大於,改為直接進行一次 Full GC ,如果小於則檢視 HandlePromotionFailure 設定看看是否允許擔保失敗,如果允許,那仍然進行 Minor GC ,如果不允許,則也要改為進行一次 Full GC 。
11、 關於 Minor GC 與 Full GC
Java 堆,分配物件例項所在空間,是 GC 的主要物件。分為新生代 (Young Generation/New)和老年代 (Tenured Generation/Old) 。新生代又劃分成 Eden Space 、 From Survivor/Survivor 0 、
To Survivor/Survivor 1 。
新生代要如此劃分是因為新生代使用的 GC 演算法是複製收集演算法。新生代使用賦值收集演算法,但是為了記憶體利用率,只使用一個 Survivor 空間來作為輪轉備份(之所以把該空間分為 FromSpace 和 ToSpace 兩部分是為了在 Minor GC 的時候把一些 age 大的物件從新生代空間中複製到老年代空間中)這種演算法效率較高,而 GC 主要是發生在物件經常消亡的新生代,因此新生代適合使用這種複製收集演算法。由於有一個假設:在一次新生代的 GC(Minor GC) 後大部分的物件佔用的記憶體都會被回收,因此留存的放置 GC 後仍然活的物件的空間就比較小了。這個留存的空間就是 Survivor space : From Survivor 或 To Survivor 。這兩個 Survivor 空間是一樣大小的。例如,新生代大小是 10M(Xmn10M) ,那麼預設情況下 (-XX:SurvivorRatio=8) , Eden Space 是 8M , From 和 To 都是 1M 。
在 new 一個物件時,先在 Eden Space 上分配,如果 Eden Space 空間不夠就要做一次 Minor GC 。 Minor GC 後,要把 Eden 和 From 中仍然活著的物件們複製到 To 空間中去。如果 To 空間不能容納 Minor GC 後活著的某個物件,那麼該物件就被 promote 到老年代空間。從 Eden 空間被複制到 To 空間的物件就有了 age=1 。此 age=1 的物件如果在下一次的 Minor GC 後仍然存活,它還會被複制到另一個 Survivor 空間 ( 如果認為 From 和 To 是固定的,就是又從 To 回到了From 空間 ) ,而它的 age=2 。如此反覆,如果 age 大於某個閾值 (-XX:MaxTenuringThreshold=n),那個該物件就也可以 promote 到老年代了。
如果 Survivor 空間中相同 age( 例如, age=5) 物件的總和大於等於 Survivor 空間的一半,那麼 age>=5 的物件在下一次 Minor GC 後就可以直接 promote 到老年代,而不用等到 age 增長到閾值。
在做 Minor GC 時,只對新生代做回收,不會回收老年代。即使老年代的物件無人索引也將仍然存活,直到下一次 Full GC 。
在發生 Minor GC 之前,虛擬機器會先檢查老年代最大可用的連續空間是否大於新生代所有物件總空間,如果這個條件成立,那麼 Minor GC 可以確保是安全的。如果經過 Minor GC 後仍有大量物件存活的情況,則需要老年代進行分配擔保,把 Survior 無法容納的物件直接進入老年代。
13 、四種引用型別:
( 1 )、強引用:直接關聯,虛擬機器永遠不會回收。
( 2 )、軟引用:描述一些還有用但並非必須的物件,虛擬機器會在丟擲記憶體溢位異常之前會對 這些物件進行第二次回收。
( 3 )弱引用:虛擬機器一定會回收的物件
( 4 )虛引用:為一個物件設定虛引用關聯的唯一目的就是能在這個物件被收集器回收時收到一個系統通知。
14 、關於 Java 中生成物件的 4 種方式與區別:
( 1 )、使用 new 操作符,這是最普遍的一種(會呼叫相應的建構函式):
如: String s=new String("abc");
( 2 )使用反射動態生成( 會呼叫相應的建構函式 ):
利用 Class , ClassLoader , Constructor 中的方法可以動態的生成類例項
如: Object o=Class.forName("java.lang.String").newInstance();
Object o=String.class.getClassLoader.loadClass("java.lang.String").newInstance();
以上的方式需要目標類擁有公有無參建構函式
以下使用 Constructor 進行動態生成
class User{
public User(String user,Integer id){}
}
Constructor c=User.class.getConstructor(new Class[]{String.class,Integer.class});
User user=(User)c.newInstance(new Object[]{"zhang san",123});
( 3 )使用克隆生成物件( 不會呼叫建構函式 )
例如使用一個實現了 Cloneable 介面的物件,呼叫其 clone() 方法獲得該物件的一份拷貝,使用 Java 序列化方式實現深拷貝。
( 4 )利用反序列化從流中生成物件( 不會呼叫建構函式 ):
利用 ObjectInptuStream 的 readObject() 方法生成物件
五、Java效能優化系列之五--JavaIO
1 、關於 Java 序列化與反序列化:
(1)作用:
1、實現物件狀態的儲存到本地,以便下一次啟動虛擬機器的時候直接讀取儲存的序列化位元組生成物件,而不是初始化物件; 2 、實現物件的網路傳輸( RMI 分佈物件); 3 、實現物件的深拷貝。
一:物件序列化可以實現分散式物件。主要應用例如: RMI 要利用物件序列化執行遠端主機上的服務,就像在本地機上執行物件時一樣。
二: java 物件序列化不僅保留一個物件的資料,而且遞迴儲存物件引用的每個物件的資料。可以將整個物件層次寫入位元組流中,可以儲存在檔案中或在網路連線上傳遞。利用物件序列化可以進行物件的 " 深複製 " ,即複製物件本身及引用的物件本身。序列化一個物件可能得到整個物件序列。
(2)基本方式:
ObjectOutputStream 只能對 Serializable 介面的類的物件進行序列化。預設情況下, ObjectOutputStream 按照預設方式序列化,這種序列化方式僅僅對物件的非 transient 的例項變數進行序列化,而不會序列化物件的 transient 的例項變數,也不會序列化靜態變數。
當 ObjectOutputStream 按照預設方式反序列化時,具有如下特點:
1 ) 如果在記憶體中物件所屬的類還沒有被載入,那麼會先載入並初始化這個類。如果在 classpath 中不存在相應的類檔案,那麼會丟擲 ClassNotFoundException ;
2 ) 在反序列化時不會呼叫類的任何構造方法。
如果使用者希望控制類的序列化方式,可以在可序列化類中提供以下形式的 writeObject() 和 readObject() 方法。
private void writeObject(java.io.ObjectOutputStream out) throws IOException
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException;
當 ObjectOutputStream 對一個 Customer 物件進行序列化時,如果該物件具有 writeObject() 方法,那麼就會執行這一方法,否則就按預設方式序列化。在該物件的 writeObjectt() 方法中,可以先呼叫 ObjectOutputStream 的 defaultWriteObject() 方法,使得物件輸出流先執行預設的序列化操作。同理可得出反序列化的情況,不過這次是 defaultReadObject() 方法。
有些物件中包含一些敏感資訊,這些資訊不宜對外公開。如果按照預設方式對它們序列化,那麼它們的序列化資料在網路上傳輸時,可能會被不法份子竊取。對於這類資訊,可以對它們進行加密後再序列化,在反序列化時則需要解密,再恢復為原來的資訊。
預設的序列化方式會序列化整個物件圖,這需要遞迴遍歷物件圖。如果物件圖很複雜,遞迴遍歷操作需要消耗很多的空間和時間,它的內部資料結構為雙向列表。
在應用時,如果對某些成員變數都改為 transient 型別,將節省空間和時間,提高序列化的效能。
|-1 、實體物件實現 seriable 介面以及自定義 seriousid 。
|-2 、 ObjectOutputStream out= new ObjectOutputStream(baos);
out.writeObject(new PersonDemo("rollen", 20));
out.close();
|-3 、 ByteArrayInputStream bais=new ByteArrayInputStream(baos.toByteArray());
ObjectInputStream input=new ObjectInputStream(bais);
Object obj =input.readObject();
input.close();
(3)Java 自定義序列化反序列化:複寫實現了 seriliable 的實體類的 readObject() 和 writeObject() 的方法的原因:
有些物件中包含一些敏感資訊,這些資訊不宜對外公開。如果按照預設方式對它們序列化,那麼它們的序列化資料在網路上傳輸時,可能會被不法份子竊取。對於這類資訊,可以對它們進行加密後再序列化,在反序列化時則需要解密,再恢復為原來的資訊。此時便不能使用預設的 readObject 和 writeObject() 方法。
private void writeObject(java.io.ObjectOutputStream out) throws IOException{
out.defaultWriteObject();
out.writeUTF(name);
}
private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException{
in.defaultReadObject();
name=in.readUTF();
}
一般情況直接實現 Serializable 介面就可以實現序列化的要求,但是有些情況需要對序列化做一些特殊的要求。
(4)Transits 關鍵字的作用:遮蔽一些不想進行序列化的成員變數,解遮蔽的方法可以用( 3)
(5)Externalize 的作用:
Externalizable 介面繼承自 Serializable 介面,如果一個類實現了 Externalizable 介面,那麼將完全由這個類控制自身的序列化行為。 Externalizable 介面宣告瞭兩個方法:
public void writeExternal(ObjectOutput out) throws IOException
public void readExternal(ObjectInput in) throws IOException , ClassNotFoundException
前者負責序列化操作,後者負責反序列化操作。
在對實現了 Externalizable 介面的類的物件進行反序列化時, 會先呼叫類的不帶引數的構造方法,這是有別於預設反序列方式的。如果把類的不帶引數的構造方法刪除 ,或者把該構造方法的訪問許可權設定為 private 、預設或 protected 級別,會丟擲 java.io.InvalidException: no valid constructor 異常。
(6)與 Java 建構函式的關係:
實現了 Externalizable 介面的類的物件進行反序列化時,會先呼叫類的不帶引數的構造方法;而實現了 Serializable 介面的類的物件進行反序列化時,不會呼叫任何構造方法。僅僅是根據所儲存的物件的狀態資訊,在記憶體中重新構建物件!
(7)注意事項:
1) 、序列化執行時使用一個稱為 serialVersionUID 的版本號與每個可序列化類相關聯,該序列號在反序列化過程中用於驗證序列化物件的傳送者和接收者是否為該物件載入了與序列化相容的類。為它賦予明確的值。顯式地定義 serialVersionUID 有兩種用途:
在某些場合,希望類的不同版本對序列化相容,因此需要確保類的不同版本具有相同的 serialVersionUID ;
在某些場合,不希望類的不同版本對序列化相容,因此需要確保類的不同版本具有不同的 serialVersionUID 。
2)、 java 有很多基礎類已經實現了 serializable 介面,比如 string,vector 等。但是比如 hashtable 就沒有實現 serializable 介面。
3)、並不是所有的物件都可以被序列化。由於安全方面的原因一個物件擁有 private,public 等 field, 對於一個要傳輸的物件 , 比如寫到檔案 , 或者進行 rmi 傳輸等等 , 在序列化進行傳輸的過程中 ,這個物件的 private 等域是不受保護的;資源分配方面的原因 , 比如 socket,thread 類 , 如果可以序列化 , 進行傳輸或者儲存 , 也無法對他們進行重新的資源分配 , 而且 , 也是沒有必要這樣實現 .
4)、反序列化物件時,並不會呼叫該物件的任何構造方法,僅僅是根據所儲存的物件的狀態資訊,在記憶體中重新構建物件!
5)、當一個物件被序列化時,只儲存物件的非靜態成員變數,不能儲存任何的成員方法和靜態的成員變數
6)、如果一個物件的成員變數是一個物件,那麼這個物件的資料成員也會被儲存!這是能用序列化解決深拷貝的重要原因。
(8)序列化與單例模式的衝突解決辦法:
另外還有兩個自定義序列化方法 writeReplace 和 readResolve ,分別用來在序列化之前替換序列化物件 和 在反序列化之後的對返回物件的處理。一般可以用來避免 singleTon 物件跨 jvm 序列化和反序列化時產生多個物件例項,事實上 singleTon 的物件一旦可序列化,它就不能保證 singleTon 了。 JVM 的 Enum 實現裡就是重寫了 readResolve 方法,由 JVM 保證 Enum 的值都是 singleTon 的,所以建議多使用 Enum 代替使用 writeReplace 和 readResolve 方法。
Java 程式碼
private Object readResolve()
{
return INSTANCE;
}
private Object writeReplace(){
return INSTANCE;
}
注: writeReplace 呼叫在 writeObject 前 ;readResolve 呼叫在 readObject 之後。
(9)序列化解決深拷貝的程式碼:
public Object deepClone() throws IOException, OptionalDataException,
ClassNotFoundException {
// 將物件寫到流裡
ByteArrayOutputStream bo = new ByteArrayOutputStream();
ObjectOutputStream oo = new ObjectOutputStream(bo);
oo.writeObject(this); // 從流裡讀出來
ByteArrayInputStream bi = new ByteArrayInputStream(bo.toByteArray());
ObjectInputStream oi = new ObjectInputStream(bi);
return (oi.readObject());
}
物件所屬的類要實現 Serializable 介面。同時將該方法寫入到物件所屬的類中。
深拷貝的時候,呼叫該方法即可。
2、JavaIO 中的裝飾模式:
Java 中使用的最廣泛的裝飾器模式就是 JavaIO 類的設計。比如, OutPutStream 是輸出流的基類,其子類有 FileOutputStream 和 FilterOutputStream, 而 FilterOutputStream 的子類有 BufferedOutputStream 和 DataOutputStream 兩個子類。其中, FileOutputStream 為系統的核心類,它實現了向檔案寫資料的功能,使用 DataOutputStream 可以在 FileOutputStream 的基礎上增加多種資料型別的寫操作支援( DataOutputStream 類中有 writeUTF 、 writeInt 等函式),而 BufferdOutputStream 裝飾器可以對 FileOutputStream 增加緩衝功能,優化 I/O 效能。
3、JavaIO 流的使用場景:
(1)IO 流:用於處理裝置上的資料,這裡的裝置指的是:硬碟上的檔案、記憶體、鍵盤輸入、螢幕顯示。
(2)位元組流和字元流:位元組流好理解,因為所有格式的檔案都是以位元組形式硬碟上儲存的,包括圖片、 MP3 、 avi 等,因此位元組流可以處理所有型別的資料。字元流讀取的時候讀到一個或多個位元組時(中文對應的 位元組數是兩個,在 UTF-8 碼錶中是三個位元組)時,先去查指定的編碼表,將查到的字元返回。字元流之所以出現,就是因為有了檔案編碼的不同,而有了對字元進行高效操作的字元流物件。因此,只要是處理純文字資料,就要優先考慮使用字元流,除此之外都使用位元組流。
(3)流操作的基本規律:
1 )、明確資料來源和資料匯,目的是明確使用輸入流還是輸出流。
2 )、明確操作的資料是否是純文字資料。
3 )、是否需要進行位元組流和字元流的轉換。
4 )、是否需要使用快取。
(4)例項說明流操作的基本流程:把鍵盤上讀入的資料以指定的編碼存入到檔案中。
1 )、明白資料來源:鍵盤輸入, System.in ,可用 InputStream 和 Reader
2 )、發現 System.in 對應的流是位元組讀入流,所以要將其進行轉換,將位元組轉換為字元。
3 )、所以要使用 InputStreamReader 轉換流
4 )、如果想提高效率,要加入快取機制,那麼就要加入字元流的緩衝區。 BufferedReader,因此前四步構造出的輸入流為:
BufferedReader bur = new BufferedReader(new InputStreamReader(System.in));
5 )、明白資料匯:既然是資料匯,則一定是輸出流,可以用 OutputStream 或 Writer 。
6 )、往檔案中儲存的都是文字檔案,因此選用 Writer 。
7 )、因為要指定編碼表,所以使用 Writer 中的轉換流, OutputStreamWriter 。
注意:雖然最終是檔案,但是不可以選擇 FileWriter ,因為該物件是使用預設編碼表。
8 )是否要提高效率,選擇 BufferedWriter 。
9 )轉換輸出流需要接收一個位元組輸出流進來,所以要是用 OutputStream 體系,而最終輸出到一個檔案中。那麼就要使用 OutputStream 體系中可以操作的檔案的字元流物件, FileOutputStream 。
10 )、通過前面的分析,得到的輸出流物件如下:
//String charSet = System.getProperty("file.encoding");
String charSet = "utf-8";
BufferedWriter bufw = new BufferedWriter(new OutputStreamWriter(new
FileOutputStream("a.txt"),charSet);
4 、可以和流相關聯的集合物件 Properties 。
Map
|--HashTable
|--Properties
Properties :該集合不需要泛型,因為該集合中的鍵值都是 String 型別。
5、其他流物件:
( 1 )列印流:
PrintStream :是一個位元組列印流 System.out 對應的就是 PrintStream 。它的建構函式可以接收三種資料型別的值:字串路徑、 File 物件、 OutputStream (當為 System.out 的時候即把輸入顯示到螢幕上)
PrintWriter :是一個字元列印流。建構函式可以接收四種型別的值。字串路徑、 File 物件(對於這兩中型別的資料,還可以指定編碼表。也即是是字符集)、 OutPutSream 、 Writer (對於三、四型別的資料,可以指定自動重新整理,注意:當自動重新整理的值為 true 時,只有三個方法可以用: printlf 、 printf 、 format )
(2)管道流: PipedOutputStream 和 PipedInputStream 。一般在多執行緒中通訊的時候用。
(3)RandomAccessFile :該物件不是流體系中的一員,但是該隊選中封裝了位元組流,同時還封裝了一個緩衝區(位元組陣列),通過內部的指標來運算元組中的資料。該物件特點:只能操作檔案和對檔案讀寫都可以。多用於多執行緒下載。、
(4)合併流:可以將多個讀取流合併成一個流。其實就是將每一個讀取流物件儲存到一個集合中,最後一個流物件結尾作為這個流的結尾。
(5)物件的序列化。 ObjectInputStream 和 ObjectInputStream 。
(6)操作基本資料型別的流物件: DataInputStream 和 DataOutputStream 。
(7)操縱記憶體陣列的流物件,這些物件的資料來源是記憶體,資料匯也是記憶體: ByteArrayInputStream 和 ByteArrayOutputStream , CharArrayReader 和 CharArrayWriter 。這些流並未呼叫系統資源,使用的是記憶體中的陣列,所以在使用的時候不用 close 。
(8)編碼轉換:
在 IO 中涉及到編碼轉換的流是轉換流和列印流,但是列印流只有輸出。轉換流是可以指定編碼表的,預設情況下,都是本機預設的編碼表, GBK 。可以通過: Syetem.getProperty( “file.encoding”) 得到。字串到位元組陣列成為編碼的過程,通過 getBytes(charset) 完成,從位元組陣列到字串的過程是解碼的過程,通過 String 類的建構函式完成 String ( byte[],charset ) .
(9)編碼例項與解析:
(10)JavaNIO 的 Charset 類專門用來編碼和解碼。
想要了解更多分散式知識點的,可以關注我一下,我後續也會整理更多關於分散式架構這一塊的知識點分享出來,另外順便給大家推薦一個交流學習群:650385180,裡面會分享一些資深架構師錄製的視訊錄影:有Spring,MyBatis,Netty原始碼分析,高併發、高效能、分散式、微服務架構的原理,JVM效能優化這些成為架構師必備的知識體系。還能領取免費的學習資源,目前受益良多,以下的課程體系圖也是在群裡獲取。