關于對象池的一些分析
在日常的開發工作中,我們可能使用或者聽說過對象池,線程池以及連接池。本文將介紹對象池的產生緣由,具體實現細節,以及需要注意的問題。
什么是對象池(模式)
- 對象池(模式)是一種創建型設計模式
- 它持有一個初始化好的對象的集合,將對象提供給調用者。
對象池的目的
- 減少頻繁創建和銷毀對象帶來的成本,實現對象的緩存和復用
什么條件下使用對象池
- 創建對象的成本比較大,并且創建比較頻繁。比如線程的創建代價比較大,于是就有了常用的線程池。
對象池的例子
Android中使用對象池的應用有很多,比如下面的這些都是應用了該模式
- Handler處理的Message
- 線程池執行器ThreadPoolExecutor
- 控件TabLayout
- 控制TypedArray的Resources
以一個簡單的獲取SytledAttributions代碼為例,展示一下對象池的應用
// Text colors/sizes come from the text appearance first
final TypedArray ta = context.obtainStyledAttributes(mTabTextAppearance,
android.support.v7.appcompat.R.styleable.TextAppearance);
try {
mTabTextSize = ta.getDimensionPixelSize(
android.support.v7.appcompat.R.styleable.TextAppearance_android_textSize, 0);
mTabTextColors = ta.getColorStateList(
android.support.v7.appcompat.R.styleable.TextAppearance_android_textColor);
} finally {
ta.recycle();
}
想必這段代碼都可能寫過,那就是在一開始的時候,我們都會被告誡:使用TypedArray結束的時候,一定要調用它的recycle方法。
回想起來,當時自己還很疑惑為什么要這么規定,其實很簡單,它使用了對象池。
調用者通過obtain方法從對象池中獲取對象,然后使用完畢后,需要使用recycle方法返還給對象池。
三種角色
上面的介紹中,也或多或少提到了下面的三種角色
- Reusable 可重用的對象
- Client 調用者
- ReusablePool 可重用的對象的池
Reusable
- 創建的成本較大,比如線程或者數據庫連接
- 被ReusablePool持有
- 被Client消費使用,使用完成應該被返回到ReusablePool
ReusablePool
- 維護一定數量的Reusable,提供給客戶端使用
- 提供 aquire 或者 obtain 等方法,便于客戶端請求Reusable
- 提供 recycle 或者 release 等方法,便于客戶端使用完畢后,將Reusable對象奉還。
Client
- 請求ReusablePool或者Reusable對象
- 使用完畢Reusable對象后,返回給ReusablePool
對象池無可用的對象時,再次對象請求,可能的表現行為
- 如果池的大小可以增長,創建新的對象并返回給client
- 阻塞client調用,直到有可用的對象回收并返回
- 拋出異常,通知client
- 返回null給client
同步處理
在多線程的環境下,我們也會使用對象池。因此做好必要的同步是必須的。
要進行同步處理的通常是這兩個方法
- aquire或obtain 負責返回對象
- release或recycle 負責回收對象
下面是一段進行同步處理了的對象池的源碼。
public static class SynchronizedPool<T> extends SimplePool<T> {
private final Object mLock = new Object();
/**
* Creates a new instance.
*
* @param maxPoolSize The max pool size.
*
* @throws IllegalArgumentException If the max pool size is less than zero.
*/
public SynchronizedPool(int maxPoolSize) {
super(maxPoolSize);
}
@Override
public T acquire() {
synchronized (mLock) {
return super.acquire();
}
}
@Override
public boolean release(T element) {
synchronized (mLock) {
return super.release(element);
}
}
}
上述代碼為Android中 android.support.v4.util 提供的Pools中的 SynchronizedPool 的實現,它使用了synchronized關鍵字實現同步問題。
對象池與單例模式
為了統一管理對象,建議將對象池設為單例。
應用單例模式的時候,需要確保在多線程并發的情況下保持唯一的實例創建,具體實現方案,可以參考單例這種設計模式
池的大小選擇
- 通常情況下,我們需要控制對象池的大小
- 如果對象池沒有限制,可能導致對象池持有過多的閑置對象,增加內存的占用
- 如果對象池閑置過小,沒有可用的對象時,會造成之前 對象池無可用的對象時,再次請求 出現的問題
- 對象池的大小選取應該結合具體的使用場景,結合數據(觸發池中無可用對象的頻率)分析來確定。
空間換時間的折中
- 本質上,對象池屬于空間換時間的折中
- 它通過緩存初始化好的對象來提升調用者請求對象的響應速度。
- 除此之外,折中(tradeoff)是軟件開發中的一個重要的概念,會貫穿整個軟件開發過程中。
對象池好處
- 提升了client獲取對象的響應速度,比如單個線程和資源連接的創建成本都比較大。
- 一定程度上減少了GC的壓力。
- 對于實時性要求較高的程序有很大的幫助
對象池弊端
臟對象的問題
所謂的臟對象就是指的是當對象被放回對象池后,還保留著剛剛被客戶端調用時生成的數據。
臟對象可能帶來兩個問題
- 臟對象持有上次使用的引用,導致內存泄漏等問題。
- 臟對象如果下一次使用時沒有做清理,可能影響程序的處理數據。
生命周期的問題
處于對象池中的對象生命周期要比普通的對象要長久。維持大量的對象也是比較占用內存空間的。
以ThreadPoolExecutor為例,它提供了 allowCoreThreadTimeOut 和 setKeepAliveTime 兩種方法,可以在超時后銷毀核心線程。我們在具體的實踐中可以參考這個策略。
異常處理問題
相對來說,使用對象池client調用也會復雜一些,比如請求對象時有可能出現的阻塞,異常或者null值。這些都需要我們做一些額外的處理,來確保程序的正常運行。
除此之外,還有上面的提到的兩個問題,他們分別是
- 同步問題
- 池大小設置問題
所以當我們想要使用對象池時,需要謹慎的衡量并準確的實現,享受它帶來的好處,并避免其帶來的問題。
來自:http://droidyue.com/blog/2016/12/12/dive-into-object-pool/