前言 池化思想在實際開發中有很多應用,指的是針對一些創建成本高,創建頻繁的對象,用完不棄,將其緩存在對象池子里,下次使用時優先從池子里獲取,如果獲取到則可以直接使用,以此降低創建對象的開銷。 我們最熟悉的資料庫連接池就是一種池化思想的應用,資料庫操作是非常頻繁的,資料庫連接的創建、銷毀開銷很大,每次 ...
前言
池化思想在實際開發中有很多應用,指的是針對一些創建成本高,創建頻繁的對象,用完不棄,將其緩存在對象池子里,下次使用時優先從池子里獲取,如果獲取到則可以直接使用,以此降低創建對象的開銷。
我們最熟悉的資料庫連接池就是一種池化思想的應用,資料庫操作是非常頻繁的,資料庫連接的創建、銷毀開銷很大,每次都需要進行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內部維護了兩個重要的變數,Stack和WeakOrderQueue,實際對象就是包裝成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
有了這些思路,代碼就出來了:
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