Object Pool 物件池設計模式 | 實現與解說 | 利用 JVM

Object Pool 物件池設計模式 | 實現與解說 | 利用 JVM

Overview of Content

本文將深入探討「Object Pool」設計模式,從概念到實現進行詳細解說

首先,將介紹Object Pool的概念及其在軟體開發中的使用場景,接著,將展示 Object PoolUML 示意圖,以便讀者更好地理解其結構和運作方式


在進入具體實現之前,我們會簡單說明 Object Pool 設計模式的優缺點,幫助讀者在適當的情況下做出適切的選擇。隨後,將深入討論如何實現 Object Pool,包括標準的實現方法以及如何配合 Java虛擬機(JVM)的垃圾回收機制。通過這些內容,讀者將能夠全面了解Object Pool設計模式,並在實際應用中獲得更好的效果。


Object Pool 概述、使用場景

當你的業務邏輯需要在一個時間區間 創建大量的物件時,就可以提前使用 Object Pool 設計用來優化性能,提高應用效率

Object Pool UML

● 把對象池化的本地是 一次性初始化所有對象,減少物件在初始化(new)耗費的時間,透這種個操作可以間接提高效能

功能
ObjectPool<T>泛型類,有兩個主要責任,內部 1.操作緩存池的邏輯,並 2.一次性初始化所需的緩存大小
ConcretePool<Data>實做 ObjectPool
Data實際要被緩存的子物件

Object Pool 設計優缺點

Object Pool 設計優點

● 在使用時僅第一次需要耗費時間創建 instance,之後的使用都 不必須要經過物件初始化 的行為就可以使用,提高了性能效率

可以減少 JVM 創建物件耗費的時間、效能(像是併發時 CAS 比較、物件初始化… 等等行為)

Object Pool 設計缺點

● 如果沒有適時的安排時機釋放物件,仍是佔用內存空間的一種行為,而這可能導致大量 GC,操作會因為 GC 而變慢

● 增加程式的複雜度,如果是處於簡易場合建議不要使用


Object Pool 實做

Object Pool 標準

A. ObjectPool:這個類有兩個重點事項要完成

操作緩存池的邏輯(取出 checkout、放置 checkin

一次性初始化所需的緩存大小:這裡預設該池中預設有 1 個物件

abstract class ObjectPool<T> {

    inner class ObjectStatus {
        var isFree = true
    }

    private val pool = mutableMapOf<T, ObjectStatus>()

    init {
        val tmp = create()

        // 預設物件
        pool[tmp] = ObjectStatus()
    }

    abstract fun lazyCreate(): () -> T

    fun create(): T {
        return lazyCreate().invoke()
    }

    @Synchronized
    fun checkout(): T? {

        for ((key, item) in pool.entries) {
            if (!item.isFree) {
                continue
            }

            return key.also {
                // 標誌該物件已經不可被使用
                item.isFree = false
            }
        }

        return null
    }

    @Synchronized
    fun checkIn(t: T) {
        var findKey = false

        for ((key, item) in pool.entries) {

            // 判斷物件是否相同
            if(key == t) {
                findKey = true
                item.isFree = true
                break
            }

        }

        if (!findKey) {
            pool[t] = ObjectStatus()

            println("add now instance to pool.")
        }
    }

}

● 除了基本使用之外,在實際使用中 還要考慮到 池(Pool)的大小、最大值、池狀態、異常處理、回收... 等等狀況

B. ConcretePool<Data>Data

ConcretePool<Data>:該類繼承於 ObjectPool 類,是 池對象的實做

class MyDataPool: ObjectPool<DataBean>() {

    override fun lazyCreate(): () -> DataBean {
        return {
            DataBean()
        }
    }

}

Data:這邊 覆寫 equalshashCode 兩個函數,因為在 ObjectPool 中會用來比較

data class DataBean constructor(var who: String = "", var msg: String = "") {

    override fun toString(): String {
        return "name=($who), msg=($msg), hashcode=(${hashCode()})"
    }

    override fun equals(other: Any?): Boolean {
        return other is DataBean &&
                other.msg == msg &&
                other.who == who
    }

    override fun hashCode(): Int {
        var result = who.hashCode()
        result = 31 * result + msg.hashCode()
        return result
    }

}

使用物件池:以下是測試步驟

A. 取出(checkout)物件 2 次:第一次可以正常取得預設物件,而第二次由於池中已經沒有物件可取,所以返回 null

B. 放置(checkin)物件 2 次:將預設物件、新創物件放回池中

這時池中就有 2 個可用物件了~

C. 取出(checkout)物件 3 次:由於池內只有 2 個物件,如果取出三次那第三次的物件就為 null

fun main() {

    val pool = MyDataPool()

    // 第一次取物件
    val reuseBean = pool.checkout()?.apply {
        msg = "Hello World"
        who = "Alien"

        // 正常可取得
        println("First time checkout: $this")
    }

    // 第二次取物件
    pool.checkout().apply {
        // 池中無物件,返回 null
        println("Second time checkout: $this")
    }

    println()

    // 放置原預設物件
    println("First time check in.")
    pool.checkIn(reuseBean!!)

    // 放置新物件
    println("Second time check in.")
    pool.checkIn(DataBean())

    println()

    repeat(3) {
        pool.checkout().apply {
            println("repeat time=${it.plus(1)}, $this")
        }
    }


}

設置引用 - 配合 JVM GC 機制

這裡使用 Java 為範例,因為 Kotlin 的靜態檢查較為完整,要模擬出 GC & 物件回收較為麻煩

● 由於上述的範例中,並沒有包括到何時回收物件,所以這裡特別 使用 JVM GC、類引用的特性來再創建一個可自動回收的 Object Pool

這裡使用簡單靜態工廠來作為 Pool

● 簡單 Bean 類

class MyBean {
    final String message;

    MyBean(String message) {
        this.message = message;
    }
}

● Object Pool 實作:這裡的重點是 使用 WeakReference 引用來存儲物件,當未來 GC 回收時,該物件會自動被回收

public class RefObjectPool {

    // 使用 WeakReference 來作為 Value,其內部才真正包裹數據
    private final HashMap<String, WeakReference<MyBean>> refCacheObject = new HashMap<>();

    public MyBean valueOf(String message) {
        for (Map.Entry<String, WeakReference<MyBean>> item : refCacheObject.entrySet()) {
            String key = item.getKey();
            MyBean value = item.getValue().get();

            if (value != null && key.equals(message)) {
                return value;
            }
        }

        MyBean newBean = new MyBean(message);

        refCacheObject.put(message, new WeakReference<>(newBean));

        return newBean;
    }

}

● 測試 ObjectPool 功能:測試它是否有物件暫存的功能

public static void main(String[] args) throws InterruptedException {

    RefObjectPool factory = new RefObjectPool();

    String apple = "Apple";
    String banana = "Banana";

    MyBean bean1 = factory.valueOf(apple);
    System.out.println("bean1: " + bean1.hashCode());

    MyBean bean2 = factory.valueOf(banana);
    System.out.println("bean2: " + bean2.hashCode());

    MyBean bean3 = factory.valueOf(apple);
    System.out.println("bean3: " + bean3.hashCode());

    System.out.println("Bean1 == Bean2: " + (bean1.hashCode() == bean2.hashCode()));
    System.out.println("Bean1 == Bean3: " + (bean1.hashCode() == bean3.hashCode()));

}

從下圖結果可以看出來 ObjectPool 類確實有暫存物件的功能

測試 JVM GC & WeakReference 的關係:來看看 GC 是否會影響到 ObjectPool 物件存取原來的物件,如果影響到 ObjectPool 存的物件,那物件暫存就會失效!(這是好事,這時可以把物件回收的責任交付給 JVM 負責)

public static void main(String[] args) throws InterruptedException {

    RefObjectPool factory = new RefObjectPool();

    String apple = "Apple";
    String banana = "Banana";

    MyBean bean1 = factory.valueOf(apple);
    System.out.println("bean1: " + bean1.hashCode());

    MyBean bean2 = factory.valueOf(banana);
    System.out.println("bean2: " + bean2.hashCode());

    MyBean bean3 = factory.valueOf(apple);
    System.out.println("bean3: " + bean3.hashCode());

    System.out.println("Bean1 == Bean2: " + (bean1.hashCode() == bean2.hashCode()));
    System.out.println("Bean1 == Bean3: " + (bean1.hashCode() == bean3.hashCode()));

    apple = banana = null;
    bean1 = bean2 = bean3 = null;

    System.gc();
    Thread.sleep(1000);

    apple = "Apple";
    banana = "Banana";
    MyBean afterGCBean1 = factory.valueOf(apple);
    System.out.println("afterGCBean1: " + afterGCBean1.hashCode());

    MyBean afterGCBean2 = factory.valueOf(banana);
    System.out.println("afterGCBean2: " + afterGCBean2.hashCode());
}

● 從上圖結果中我們可以看到,假設我們不使用 GCThread#sleep() 那會得到緩存在記憶體中的相同物件(同 hashcode)


更多的物件導向設計

物件導向的設計基礎如下,如果是初學者或是不熟悉的各位,建議可以從這些基礎開始認識,打好基底才能走個更穩(在學習的時候也需要不斷回頭看)!

設計建模 2 大概念- UML 分類、使用

物件導向設計原則 – 6 大原則(一)

物件導向設計原則 – 6 大原則(二)

創建、行為、結構型設計 8 個比較 | 包裝模式 | 最佳實踐

創建模式 Creation Patterns

創建模式 PK

創建模式 - Creation Patterns

結構模式 Structural Patterns

結構模式 PK

結構模式 - Structural Patterns

結構模式專注於「物件之間的組成」,以形成更大的結構。這些模式可以幫助你確保當系統進行擴展或修改時,不會破壞其整體結構。例如,外觀模式、代理模式… 等等,詳細解說請點擊以下連結

Bridge 橋接模式 | 解說實現 | 物件導向設計

Decorate 裝飾模式 | 解說實現 | 物件導向設計

Proxy 代理模式 | 解說實現 | 分析動態代理

Iterator 迭代設計 | 解說實現 | 物件導向設計

Facade 外觀、門面模式 | 解說實現 | 物件導向設計

Adapter 設計模式 | 解說實現 | 物件導向設計

Leave a Comment

Comments

No comments yet. Why don’t you start the discussion?

發表迴響