netty Recycler對象池

来源:https://www.cnblogs.com/jtea/p/18074792
-Advertisement-
Play Games

前言 池化思想在實際開發中有很多應用,指的是針對一些創建成本高,創建頻繁的對象,用完不棄,將其緩存在對象池子里,下次使用時優先從池子里獲取,如果獲取到則可以直接使用,以此降低創建對象的開銷。 我們最熟悉的資料庫連接池就是一種池化思想的應用,資料庫操作是非常頻繁的,資料庫連接的創建、銷毀開銷很大,每次 ...


前言

池化思想在實際開發中有很多應用,指的是針對一些創建成本高,創建頻繁的對象,用完不棄,將其緩存在對象池子里,下次使用時優先從池子里獲取,如果獲取到則可以直接使用,以此降低創建對象的開銷。
我們最熟悉的資料庫連接池就是一種池化思想的應用,資料庫操作是非常頻繁的,資料庫連接的創建、銷毀開銷很大,每次都需要進行TCP三次握手和四次揮手,許可權檢查等,所以如果每次操作資料庫都重新創建連接,用完就丟棄,對於應用程式來說是不可接受的。在java世界里,一切皆對象,所以需要有一個資料庫對象連接池,用於保存連接池對象。例如使用hikari,可以配置spring.datasource.hikari.maximum-pool-size=20,表示最多可以池化20個資料庫連接對象。
此外,頻繁的創建銷毀對象還會影響GC,當一個對象使用完,再沒被GC root引用,就變成不可達,所引用的記憶體可以被垃圾回收,GC是需要STW的,頻繁的GC也會影響程式的吞吐量。

本篇我們要介紹的是netty的對象池Recycler,Recycler是對象池核心類,netty為了減少依賴,以及追求高性能,並沒有使用第三方的對象池,而是自己設計了一套。
netty在高併發處理IO讀寫,記憶體對象的使用是非常頻繁的,如果每次都重新申請,無疑性能會大打折扣,特別是對於堆外記憶體,申請和銷毀的成本更高,所以對記憶體對象使用池化是很有必要的。
例如:PooledHeapByteBuf,PooledDirectByteBuf,ChannelOutboundBuffer.Entry都使用了對象池,這些類內部都有一個Recycler靜態變數和一個Handle實例變數。

static final class Entry {
    private static final Recycler<Entry> RECYCLER = new Recycler<Entry>() {
        @Override
        protected Entry newObject(Handle<Entry> handle) {
            return new Entry(handle);
        }
    };

    private final Handle<Entry> handle;
}

原理

我們先通過一個例子感受一下Recycler的使用,然後再來分析它的原理。

public final class Connection {

	private Recycler.Handle handle;

	private Connection(Recycler.Handle handle) {
		this.handle = handle;
	}

	private static final Recycler<Connection> RECYCLER = new Recycler<Connection>() {
		@Override
		protected Connection newObject(Handle<Connection> handle) {
			return new Connection(handle);
		}
	};

	public static Connection newInstance() {
		return RECYCLER.get();
	}

	public void recycle() {
		handle.recycle(this);
	}

	public static void main(String[] args) {
		Connection c1 = Connection.newInstance();
		int hc1 = c1.hashCode();
		c1.recycle();
		Connection c2 = Connection.newInstance();
		int hc2 = c2.hashCode();
		c2.recycle();
		System.out.println(hc1 == hc2); //true
	}
}

代碼非常簡單,我們用final修飾Connection,這樣就無法通過繼承創建對象。同時構造方法定義為私有,防止外部直接new創建對象,這樣就只能通過newInstance靜態方法創建對象。
Recycler是一個抽象類,newObject是它的抽象方法,這裡使用匿名類繼承Recycler並重寫newObject,用於創建一個新的對象。
Handle是一個介面,Recycler會創建並通過newObject方法傳進來,預設是DefaultHandle,它的作用是用來回收對象,放回對象池。
接著我們創建兩個Connection實例,可以看到它們的hashcode是一樣的,證明是同一個對象。
需要註意的是,使用對象池創建的對象,用完需要調用recycle回收。

原理分析
想象一下,如果由我們設計,怎麼設計一個高性能的對象池呢?對象池的操作很簡單,一取一放,但考慮到多線程,實際情況就變得複雜了。
如果只有一個全局的對象池,多線程操作需要保證線程安全,那就需要通過加鎖或者CAS,這都會影響存取效率,由於線程競爭,鎖等待,可能通過對象池獲取對象的效率還不如直接new一個,這樣就得不償失了。
針對這種情況,已經有很多的經驗供我們借鑒,核心思想都是一樣的,降低鎖競爭。例如ConcurrentHashMap,通過每個節點上鎖,hash到不同節點的線程就不會相互競爭;例如ThreadLocal,通過線上程級別綁定一個ThreadLocalMap,每個線程操作的都是自己的私有變數,不會相互競爭;再比如jvm在分配記憶體的時候,記憶體區域是共用的,所以jvm為每個線程設計了一塊私有的TLAB,可以高效進行記憶體分配,關於TLAB可以參考:這篇文章

這種無鎖化的設計在netty中非常常見,例如對象池,記憶體分配,netty還設計了FastThreadLocal來代替jdk的ThreadLocal,使得線程內的存取更加高效。
Recycler設計如下:

如上圖,Recycler內部維護了兩個重要的變數,StackWeakOrderQueue,實際對象就是包裝成DefaultHandle,保存在這兩個結構中。
預設情況一個線程最多存儲4 * 1024個對象,可以根據實際情況,通過Recycler的構造函數指定。

private static final int DEFAULT_INITIAL_MAX_CAPACITY_PER_THREAD = 4 * 1024; // Use 4k instances as default.

Stack是一個棧結構,是線程私有的,Recycler內部通過FastThreadLocal進行定義,對Stack的操作不會有線程安全問題。

 private final FastThreadLocal<Stack<T>> threadLocal = new FastThreadLocal<Stack<T>>() {};        

FastThreadLocal是netty版的ThreadLocal,搭配FastThreadLocalThread,FastThreadLocalMap使用,主要優化jdk ThreadLocal擴容需要rehash,和hash衝突問題。

當獲取對象時,就是嘗試從Stack棧頂pop出一個對象,如果有,則直接使用。如果沒有就嘗試從WeakOrderQueue“借”一點過來,放到Stack,如果借不到,那就調用newObject()創建一個。

WeakOrderQueue主要是用來解決多線程問題的,考慮這種情況,線程A創建的對象,可能被線程B使用,那麼對象的釋放就應該由線程B決定。如果線程B也將對象歸還到線程A的Stack,那就出現了線程安全問題,線程A對Stack的讀取,寫入就需要加鎖,影響併發效率。
為了無鎖化操作,netty為其它每個線程都設計了一個WeakOrderQueue,各個線程只會操作自己的WeakOrderQueue,不會有併發問題了。其它線程的WeakOrderQueue會通過指針構成一個鏈表,Stack對象內部通過3個指針指向鏈表,這樣就可以遍歷整個鏈表對象。

站線上程A的角度,其它線程就是B,C,D...,站線上程B的角度,其它線程就是A,C,D...

從上圖可以看到,WeakOrderQueue實際不是一個隊列,內部是由一些Link對象構成的雙向鏈表,它也是一個鏈表。
Link對象是一個包含讀寫索引,和一個長度為16的數組的對象,數組存儲的就是DefaultHandler對象。

整個過程是這樣的,當本線程從Stack獲取不到可用對象時,就會通過cursor指針變數WeakOrderQueue鏈表,開始從其它線程獲取對象。如果找到一個可用的Link,就會將整個Link里的對象遷移到Stack,然後刪除鏈表節點,為了保證效率,每次最多遷移一個Link。如果還獲取不到,就通過newObject()方法創建一個新的對象。

Recycler#get 方法如下:

 public final T get() {
    if (maxCapacityPerThread == 0) {
        return newObject((Handle<T>) NOOP_HANDLE);
    }
    Stack<T> stack = threadLocal.get();
    DefaultHandle<T> handle = stack.pop();
    if (handle == null) {
        handle = stack.newHandle();
        handle.value = newObject(handle);
    }
    return (T) handle.value;
}

pop方法判斷Stack沒有對象,就會調用scavenge方法,從WeakOrderQueue遷移對象。scavenge,翻譯過來是拾荒,撿的意思。

 DefaultHandle<T> pop() {
    int size = this.size;
    if (size == 0) {
        if (!scavenge()) {
            return null;
        }
        size = this.size;
    }
    //...
}

最終會調用到WeakOrderQueue的transfer方法,這個方法比較複雜,主要是對WeakOrderQueue鏈表和內部Link鏈表的遍歷。
這裡dst就是前面說的Stack對象,可以看到會把element元素遷移過去。

boolean transfer(Stack<?> dst) {
    //...
    if (srcStart != srcEnd) {
        final DefaultHandle[] srcElems = head.elements;
        final DefaultHandle[] dstElems = dst.elements;
        int newDstSize = dstSize;
        for (int i = srcStart; i < srcEnd; i++) {
            DefaultHandle element = srcElems[i];
            if (element.recycleId == 0) {
                    element.recycleId = element.lastRecycledId;
            } else if (element.recycleId != element.lastRecycledId) {
                throw new IllegalStateException("recycled already");
            }
            srcElems[i] = null;

            if (dst.dropHandle(element)) {
                // Drop the object.
                continue;
            }
            element.stack = dst;
            dstElems[newDstSize ++] = element;
        }            
    }
    //...
}

應用

我們項目使用了mybatis plus作為orm,其中用得最多的就是QueryWrapper了,每次查詢都需要new一個QueryWrapper。例如:

QueryWrapper<User> queryWrapper = new QueryWrapper();
queryWrapper.eq("uid", 123);
return userMapper.selectOne(queryWrapper);

資料庫查詢是非常頻繁的,QueryWrapper的創建雖然不會很耗時,但過多的對象也會給GC帶來壓力。
QueryWrapper是mp提供的類,它沒有池化的實現,不過我們可以參考上面netty DefaultHandle的思路,在它外面再包一層,然後池化包裝後的對象。
回收的時候還要註意清空對象的屬性,例如上面給uid賦值了123,下個對象就不能用這個條件,否則就亂套了,QueryWrapper提供了clear方法可以重置所有屬性。
同時,每次用完都需要手動recycle也是比較麻煩的,開發容易忘記,可以藉助AutoCloseable介面,使用try-with-resource的寫法,在結束後自動完成回收。
對於修改和刪除還有UpdateWrapper和DeleteWrapper,同樣思路也可以實現。

有了這些思路,代碼就出來了:

public final class WrapperUtils {

	private WrapperUtils() {}

	private static final Recycler<PooledQueryWrapper> QUERY_WRAPPER_RECYCLER = new Recycler<PooledQueryWrapper>() {
		@Override
		protected PooledQueryWrapper newObject(Handle<PooledQueryWrapper> handle) {
			return new PooledQueryWrapper<>(handle);
		}
	};

	public static <T> PooledQueryWrapper<T> newInstance() {
		return QUERY_WRAPPER_RECYCLER.get();
	}

	static class PooledQueryWrapper<T> implements AutoCloseable {

		private QueryWrapper<T> queryWrapper;
		private Recycler.Handle<PooledQueryWrapper> handle;

		public PooledQueryWrapper(Recycler.Handle<PooledQueryWrapper> handle) {
			this.queryWrapper = new QueryWrapper<>();
			this.handle = handle;
		}

		public QueryWrapper<T> getWrapper() {
			return this.queryWrapper;
		}

		@Override
		public void close() {
			queryWrapper.clear();
			handle.recycle(this);
		}
	}
}

使用如下,可以看到列印出來的hashcode都是一樣的,每次執行後都會自動調用close方法,進行QueryWrapper屬性重置。

public static void main(String[] args) {
	try (PooledQueryWrapper<Case> objectPooledWrapper = WrapperUtils.newInstance()) {
		QueryWrapper<Case> wrapper = objectPooledWrapper.getWrapper();
		wrapper.eq("age", 1);
		wrapper.select("id,name");
		wrapper.last("limit 1");
		System.out.println(wrapper.hashCode());
	}

	try (PooledQueryWrapper<Case> objectPooledWrapper = WrapperUtils.newInstance()) {
		QueryWrapper<Case> wrapper = objectPooledWrapper.getWrapper();
		wrapper.eq("age", 2);
		wrapper.select("id,email");
		wrapper.last("limit 2");
		System.out.println(wrapper.hashCode());
	}

	try (PooledQueryWrapper<Case> objectPooledWrapper = WrapperUtils.newInstance()) {
		QueryWrapper<Case> wrapper = objectPooledWrapper.getWrapper();
		wrapper.eq("age", 3);
		wrapper.select("id,phone");
		wrapper.last("limit 3");
		System.out.println(wrapper.hashCode());
	}
}

總結

之前我們也分析過apache common pool,這也是一個池化實現,在redis客戶端也有應用,但它是通過加鎖解決併發問題的,設計沒有netty這麼精細。
上面的源碼來自netty4.1.42,從整體上看整個Recycler的設計還是比較複雜的,主要為瞭解決多線程競爭和GC問題,導致整個代碼複雜度比較高,所以netty在後來的版本中對其進行重構。
不過這不影響我們對它思想的學習,以後也可以借鑒到實際開發中。

更多分享,歡迎關註我的github:https://github.com/jmilktea/jtea


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 涉及的技術棧 vue3 vite bootstrap5 背景 在用bootstrap5的時候遇到一個問題,就是offcanvas在nav上的時候居然會有兩個背景BackDrop,關閉之後頁面上還有一個backdrop留在那 bootstrap5文檔裡面提供了幾個Method可以控制Offcanvas ...
  • 本文介紹三種使用純 CSS 實現星級評分的方式。每種都值得細品一番~ 五角星取自 Element Plus 的 svg 資源 <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024" style=""> <path fill="c ...
  • 這裡給大家分享我在網上總結出來的一些知識,希望對大家有所幫助 一、介紹 定義: 用於定義基本操作的自定義行為 本質: 修改的是程式預設形為,就形同於在編程語言層面上做修改,屬於元編程(meta programming) 元編程(Metaprogramming,又譯超編程,是指某類電腦程式的編寫,這 ...
  • 前言 Vue3 作為一款現代的 JavaScript 框架,引入了許多新的特性和改進,其中包括 shallowRef 和 shallowReactive。這兩個功能在Vue 3中提供了更加靈活和高效的狀態管理選項,尤其適用於大型和複雜的應用程式。 Vue 3 的響應式系統 Vue3 引入了新的響應式 ...
  • 作為2024年最受歡迎的Vue.js組件庫之一,ViewDesign憑藉其現代化設計理念、強大功能和可定製性脫穎而出。這款開源UI組件庫提供了豐富的基礎組件、數據展示組件和交互反饋組件,涵蓋了大部分Web開發場景。同時,ViewDesign還具備良好的可訪問性、完善的文檔、活躍的社區支持,並對SEO... ...
  • 前言 我們每天寫vue代碼時都在用defineProps,但是你有沒有思考過下麵這些問題。為什麼defineProps不需要import導入?為什麼不能在非setup頂層使用defineProps?defineProps是如何將聲明的 props 自動暴露給模板? 舉幾個例子 我們來看幾個例子,分別 ...
  • “將抽象和實現解耦,讓它們可以獨立變化。” 橋接模式通過將一個類的抽象部分與實現部分分離開來,使它們可以獨立地進行擴展和修改。 ...
  • 我們都知道,我們寫的Java程式需要先經過編譯,生成了.class文件(位元組碼文件)。然而,電腦並不能直接解釋.class文件裡面的內容,這時候就需要一個能載入、解釋.class文件並且能按.class文件里的內容進行處理的一個東西--JVM。 JVM,就是Java虛擬機。它是一種規範,有針對不同 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...