窺探 Netty 源碼!Recycler 對象池實現原理剖析

2020-09-10 Java高級架構師

Netty 的對象池 Recycler 是什麼

Recycler 是 Netty 中基於 ThreadLocal 的輕量化的對象池實現。既然是基於 ThreadLocal,那麼就可以將其理解為當前線程在通過對象池 Recycler 得到一個對象之後,在回收對象的時候,不需要將其銷毀,而是放回到該線程的對象池中即可,在該線程下一次用到該對象的時候,不需要重新申請空間創建,而是直接重新從對象池中獲取。

Recycler 在 netty 中被如何使用

Recycler 對象池在 netty 中最重要的使用,就在於 netty 的池化 ByteBuf 的場景下。首先,何為池化?以 PooledDirectByteBuf 舉例,每一個 PooledDirectByteBuf 在應用線程中使用完畢之後,並不會被釋放,而是等待被重新利用,類比線程池每個線程在執行完畢之後不會被立即釋放,而是等待下一次執行的時候被重新利用。所謂的對象池也是如此,池化減少了 ByteBuf 創建和銷毀的開銷,也是 netty 高性能表現的基石之一。

private static final Recycler<PooledDirectByteBuf> RECYCLER = new Recycler<PooledDirectByteBuf>() { @Override protected PooledDirectByteBuf newObject(Handle<PooledDirectByteBuf> handle) { return new PooledDirectByteBuf(handle, 0); }};static PooledDirectByteBuf newInstance(int maxCapacity) { PooledDirectByteBuf buf = RECYCLER.get(); buf.reuse(maxCapacity); return buf;}

PooledDirectByteBuf 在其類加載的過程中,初始化了一個靜態的 RECYCLER 成員,通過重寫其 newObject()方法達到使 Recycler 可以初始化一個 PooledDirectByteBuf。而在接下來的使用中,只需要通過靜態方法 newInstance()就可以從 RECYCLER 對象池的 get()方法獲取一個新的 PooledDirectByteBuf 對象返回,而重寫的方法 newObject()中的入參 Handler 則提供了 recycle()方法給出了對象重新放入池中回收的能力,這裡的具體實現在下文展開。因此,newInstance()方法和 recycle()方法就提供了對象池出池和入池的能力,也通過此,PooledDirectByteBuf 達到了池化的目標。

Recycler 的實現原理分析

Recycler 的實現原理很抽象,可以先直接閱讀文末的例子再閱讀這部分內容。

Recycler 中,最核心的是兩個通過 ThreadLocal 作為本地線程私有的兩個成員,而其實現原理只需要圍繞這兩個成員分析,就可以對對象池的設計有直接的理解和認識。

•第一個成員是在 Recycler 被定義的 Stack 成員對象。

private final FastThreadLocal<Stack<T>> threadLocal = new FastThreadLocal<Stack<T>>() { @Override protected Stack<T> initialValue() { return new Stack<T>(Recycler.this, Thread.currentThread(), maxCapacityPerThread, maxSharedCapacityFactor, ratioMask, maxDelayedQueuesPerThread); }};

顧名思義,這個 Stack 主體是一個堆棧,但是其還維護著一個鍊表,而鍊表中的每一個節點都是一個隊列。

private DefaultHandle<?>[] elements;private WeakOrderQueue cursor, prev;

上述的 elements 數組便是存放當前線程被回收的對象,噹噹前線程從該線程的 Recycler 對象池嘗試獲取新的對象的時候,首先就會從當前 Stack 的這個數組中嘗試獲取已經在先前被創建並且在當前線程被回收的對象,因為當對象池的對象在當前線程被調用 recycle()的時候,是會直接放到 elements 數組中等待下一次的利用。那麼問題來了,如果從該線程中被申請的這個對象是在另外一個線程中被調用 recycle()方法回收呢?那麼該對象就會處於鍊表中的隊列中,當堆棧數組中的對象不存在的時候,將會嘗試把鍊表隊列中的對象轉移到數組中供當前線程獲取。那麼其他線程是如何把被回收的對象放到這些鍊表中的隊列的呢?接下來就是另一個成員的使命了。

•第二個成員是在 Recycler 中也是通過 ThreadLocal 所實現的一個線程本地變量,DELAYED_RECYCLED ,是一個 Stack 和隊列的映射 Map。

private static final FastThreadLocal<Map<Stack<?>, WeakOrderQueue>> DELAYED_RECYCLED = new FastThreadLocal<Map<Stack<?>, WeakOrderQueue>>() { @Override protected Map<Stack<?>, WeakOrderQueue> initialValue() { return new WeakHashMap<Stack<?>, WeakOrderQueue>(); }};

第二個成員 DELAYED_RECYCLED 可以通過上文的 Stack 獲取一個隊列。
在前一個成員的解釋中提到,當別的線程調用另一個線程的對象池的 recycle()方法進行回收的時候,並不會直接落到持有對象池的線程的 Stack 數組當中,當然原因也很簡單,在並發情況下這樣的操作顯然是線程不安全的,而加鎖也會帶來性能的開銷。因此,netty 在 Recycler 對象池中通過更巧妙的方式解決這一問題。
在前面提到,除了數組,Stack 還持有了一系列隊列的組成的鍊表,這些鍊表中的每一個節點都是一個隊列,這些隊列又存放著別的線程所回收到當前線程對象池的對象。那麼,這些隊列就是各個線程針對持有對象池的專屬回收隊列,說起來很拗口,看下面的代碼。

private void pushLater(DefaultHandle<?> item, Thread thread) { // we don't want to have a ref to the queue as the value in our weak map // so we null it out; to ensure there are no races with restoring it later // we impose a memory ordering here (no-op on x86) Map<Stack<?>, WeakOrderQueue> delayedRecycled = DELAYED_RECYCLED.get(); WeakOrderQueue queue = delayedRecycled.get(this); if (queue == null) { if (delayedRecycled.size() >= maxDelayedQueues) { // Add a dummy queue so we know we should drop the object delayedRecycled.put(this, WeakOrderQueue.DUMMY); return; } // Check if we already reached the maximum number of delayed queues and if we can allocate at all. if ((queue = WeakOrderQueue.allocate(this, thread)) == null) { // drop object return; } delayedRecycled.put(this, queue); } else if (queue == WeakOrderQueue.DUMMY) { // drop object return; } queue.add(item);}private WeakOrderQueue(Stack<?> stack, Thread thread) { head = tail = new Link(); owner = new WeakReference<Thread>(thread); synchronized (stack) { next = stack.head; stack.head = this; } // Its important that we not store the Stack itself in the WeakOrderQueue as the Stack also is used in // the WeakHashMap as key. So just store the enclosed AtomicInteger which should allow to have the // Stack itself GCed. availableSharedCapacity = stack.availableSharedCapacity;}

pushLater()方法發生在當一個對象被回收的時候,噹噹前線程不是這個對象所申請的時候的線程時,將會通過該對象的 Stack 直接去通過 DELAYED_RECYCLED 映射到一條隊列上,如果沒有則創建並建立映射,再把該對象放入到該隊列中,以上操作結束後該次回收即宣告結束

private WeakOrderQueue(Stack<?> stack, Thread thread) { head = tail = new Link(); owner = new WeakReference<Thread>(thread); synchronized (stack) { next = stack.head; stack.head = this; } // Its important that we not store the Stack itself in the WeakOrderQueue as the Stack also is used in // the WeakHashMap as key. So just store the enclosed AtomicInteger which should allow to have the // Stack itself GCed. availableSharedCapacity = stack.availableSharedCapacity;}

如果在操作中,隊列是被創建的,會把該隊列放置在 Stack 中的鍊表裡的頭結點,保證創建該對象的線程在數組空了之後能夠通過鍊表訪問到該隊列並將該隊列中的回收對象重新放到數組中等待被下次重新利用,隊列交給 A 線程的鍊表是唯一的阻塞操作。在這裡通過一次阻塞操作,避免後續都不存在資源的競爭問題。

舉一個例子來解釋對象池的原理

A 線程申請,A 線程回收的場景。

•顯然,當對象的申請與回收是在一個線程中時,直接把對象放入到 A 線程的對象池中即可,不存在資源的競爭,簡單輕鬆。

A 線程申請,B 線程回收的場景。

•首先,當 A 線程通過其對象池申請了一個對象後,在 B 線程調用 recycle()方法回收該對象。顯然,該對象是應該回收到 A 線程私有的對象池當中的,不然,該對象池也失去了其意義。

•那麼 B 線程中,並不會直接將該對象放入到 A 線程的對象池中,如果這樣操作在多線程場景下存在資源的競爭,只有增加性能的開銷,才能保證並發情況下的線程安全,顯然不是 netty 想要看到的。

•那麼 B 線程會專門申請一個針對 A 線程回收的專屬隊列,在首次創建的時候會將該隊列放入到 A 線程對象池的鍊表首節點(這裡是唯一存在的資源競爭場景,需要加鎖),並將被回收的對象放入到該專屬隊列中,宣告回收結束。

•在 A 線程的對象池數組耗盡之後,將會嘗試把各個別的線程針對 A 線程的專屬隊列裡的對象重新放入到對象池數組中,以便下次繼續使用。

相關焦點

  • Dubbo源碼學習——從源碼看看dubbo對netty的使用
    前言前段時間,從頭開始將netty源碼了解了個大概,但都是原理上理解。剛好博主對dubbo框架了解過一些,這次就以dubbo框架為例,詳細看看dubbo這種出色的開源框架是如何使用netty的,又是如何與框架本身邏輯進行融合的。
  • 阿里P9都窺視已久的「Java並發實現原理:JDK源碼剖析」
    本書基於JDK 7和JDK 8,對整個Concurrent包進行全面的源碼剖析。JDK 8中大部分並發功能的實現和JDK 7一樣,但新增了一些額外特性。例如CompletableFuture、ConcurrentHashMap的新實現、StampedLock、LongAdder等。
  • 深度剖析C++對象池自動回收技術實現
    對象池可以顯著提高性能,如果一個對象的創建非常耗時或非常昂貴,頻繁去創建的話會非常低效。對象池通過對象復用的方式來避免重複創建對象,它會事先創建一定數量的對象放到池中,當用戶需要創建對象的時候,直接從對象池中獲取即可,用完對象之後再放回到對象池中,以便復用。這種方式避免了重複創建耗時或耗資源的大對象,大幅提高了程序性能。本文將探討對象池的技術特性以及源碼實現。
  • Netty學習-netty的工作原理
    首先來一段簡單的Netty服務端代碼(用於理解工作原理): public static void main(String[] args) throws Exception { //創建BossGroup 和 WorkerGroup //說明 //1.
  • 華為資深架構師,6年實戰經驗傾注一份Netty權威指南(含源碼)
    但是很多人還是用不好Netty這一優秀的異步通信框架,甚至都不熟悉,那又該怎樣去熟悉Netty,學好netty呢?小編免費分享給大家一份文檔:異步非阻塞通信領域的經典之作,「Netty權威指南」,是國內首本深入介紹Netty原理和架構的技術文檔,也是一名資深架構師多年實戰經驗的總結和濃縮。
  • Github一夜爆火的SSM源碼剖析手冊也太香了吧
    前言SSM作為目前主流的JavaEE企業級框架,可以說現在面試必被問到SSM的實現原理、架構設計,似乎不啃上幾遍源碼,都不好意思跟面試官交流。何況在面試「造火箭」、工作「螺絲釘」的大環境下,很多程式設計師雖然對框架使用得非常嫻熟,但對底層原理及架構設計缺少足夠的積累與認知,知其然卻不知其所以然。我們學習的各種設計模式,最終都需要在源碼中進行落地。當然,我們也需要從優秀的源碼中挖掘設計模式及設計模式的應用場景,學習其中的設計藝術。所以,學習源碼已經是大勢所趨!如何高效閱讀源碼?
  • 尚矽谷Netty視頻教程重磅發布!
    按照我們尚大的調性,依照慣例,視頻、源碼、課件、筆記,全部毫無保留送給你!這麼說吧,要是韓老師本人沒意見,淨重的韓老師我們都分成幾個包給你快遞過去!本套視頻詳細講解了Netty核心技術點,同時進行底層機制和源碼剖析,並編寫了大量的應用實例。通過學習可以快速掌握Netty的底層實現機制,熟練運用Netty解決網絡高並發問題!
  • 餓了麼架構師發布「絕版」Java並發實現原理:JDK源碼剖析
    今天就來分享一份餓了麼架構師純手打的Java並發實現原理:JDK源碼剖析,由於這份筆記的內容過多,小編沒辦法全部為大家展示出來,有不盡完美之處,還望大家多多海涵,同時小編已經整理成PDF藍光版,需要免費獲取的朋友麻煩私信我【333】或者【666】即可!
  • 美團大牛手擼並發原理筆記,由淺入深剖析JDK源碼
    相信很多人對於Concurrent並發包都是一知半解,更別說Concurrent包源碼了。(大牛另當別論)可以說要是Concurrent包與其源碼有一定的了解的話是完全可以避免重複造輪子,也能避免因為使用不當而掉到「坑」裡,更不會說停留於一個「似是而非」的階段。那麼問題來了,如何學?
  • Python源碼剖析,豆瓣評分8.8
    Python具體實現的著作 2、 內容新鮮,採用的Python語言版本 3、 大量的圖表形象地展示Python內部的運作機理  為了更好地利用Python語言,無論是使用Python語言本身,還是將Python與C/C 交互使用,深刻理解Python的運行原理都是重要的。本書以CPython為研究對象,在C代碼一級,深入細緻地剖析了Python的實現。
  • Netty實現高性能RPC伺服器優化篇之消息序列化
    有關如何利用Netty開發實現,高性能RPC伺服器的一些設計思路、設計原理,以及具體的實現方案。在文章的最後提及到,其實基於該方案設計的RPC伺服器的處理性能,還有優化的餘地。3、利用第三方編解碼框架(Kryo、Hessian)的時候,考慮到高並發的場景下,頻繁的創建、銷毀序列化對象,會非常消耗JVM的內存資源,影響整個RPC伺服器的處理性能,因此引入對象池化(Object Pooling)技術。眾所周知,創建新對象並初始化,可能會消耗很多的時間。當需要產生大量對象的時候,可能會對性能造成一定的影響。
  • Java8線程池ThreadPoolExecutor底層原理及其源碼解析
    小侃一下日常開發中, 或許不會直接new線程或線程池, 但這些線程相關的基礎或思想是非常重要的, 參考林迪效應;就算沒有直接用到, 可能間接也用到了類似的思想或原理, 例如tomcat, jetty, 資料庫連接池, MQ;本文不會對線程的基礎知識進行介紹
  • 騰訊首推Netty成長筆記:(原理+應用+源碼+調優全都有)
    Netty普通開發人員在工作中一般很少接觸Netty,只有在閱讀一 些分布式框架底層源碼時,才會發現底層通信模塊大部分是Netty,現代網際網路架構,Netty這個優秀的網絡通信框架其實在分布式系統的構建中是起到了舉足輕重的作用。
  • C++ 簡單對象池實現
    一、前言C++內存管理可謂是讓程式設計師操碎了心,前面寫了C++內存池設計與實現和C++智能指針,這次該是大家經常聽說的對象池了;什麼是對象池所謂對象池就是有很多對象的池子,其實也就是開始創建大量的對象放在一個池子裡面;這不免讓我想起了
  • 拿下Netty這座城,從現在開始
    之後,我把市面上有關Netty的書籍和博客幾乎全部看了一遍,並跟著書中的示例邊看邊練,但是,最後,我發現,在Netty的知識方面,我只是從一個學徒變成了一個熟練工,對Netty的理解還是談不上有多深刻,因為很多書籍或者博客對Netty的講解都停留在使用的角度,對於核心知識和底層原理,講解得很少,或者說是很不全面。
  • 網際網路輕量級SSM框架揭秘:Spring、Spring MVC、MyBatis源碼剖析
    本書Spring源碼剖析篇基於Spring4.3.2版本,剖析了Spring 上下文、Spring AOP和Spring事務的實現,並通過實例展示了框架陷阱的隱蔽性及學習框架原理的必要性。Spring MVC源碼剖析篇基於SpringMVC3.0版本,這個版本比較簡單、核心清晰,便於讀者理解透徹,這裡主要講解其中的設計模式及可插拔的設計思路。
  • IAVA高級開發-刨根問底netty源碼分析之writeAndFlush全解析
    前言在前面的文章中,我們已經詳細闡述了事件和異常傳播在netty中的實現,(netty源碼分析之pipeline(一),netty源碼分析之pipeline(二)),其中有一類事件我們在實際編碼中用得最多,那就是 write或者writeAndFlush,也就是我們今天的主要內容主要內容本文分以下幾個部分闡述一個
  • 京東T8全面詳解Java開源框架,透徹剖析盡在《Netty權威指南》
    《Netty權威指南》是異步非阻塞通信領域的經典之作,適合架構師、設計師、軟體開發工程師、測試人員和其他對Java NIO 框架、Java 通信感興趣的相關人士閱讀,相信通過學習本書,能夠熟悉和掌握Netty這一優秀的異步通信框架,實現高可用分布式系統的構建。
  • Netty伺服器啟動過程源碼帶你分析「你能堅持看完嗎?」
    基本說明1、只有看過Netty源碼,才能說是真正的掌握了Netty框架;2、在io.netty.example包下,有很多netty源碼案例,可以用來分析;3、源碼分析,是針對有Java項目經驗,並且玩過框架源碼的人員來講的,否則會有相當的難度;源碼剖析目的
  • 當Tomcat遇上Netty
    第五步,參數及監控改造其實,很簡單,看過Netty源碼的同學,應該比較清楚,Netty默認使用的是 池化的直接內存實現的ByteBuf,即PooledDirectByteBuf,所以,為了調試,首先,要把池化這個功能關閉。直接內存,即堆外內存。