一文搞懂Java中對象池的實現(xiàn)
最近在分析一個應(yīng)用中的某個接口的耗時情況時,發(fā)現(xiàn)一個看起來極其普通的對象創(chuàng)建操作,竟然每次需要消耗 8ms 左右時間,分析后發(fā)現(xiàn)這個對象可以通過對象池模式進(jìn)行優(yōu)化,優(yōu)化后此步耗時僅有 0.01ms,這篇文章介紹對象池相關(guān)知識。
1. 什么是對象池
池化并不是什么新鮮的技術(shù),它更像一種軟件設(shè)計模式,主要功能是緩存一組已經(jīng)初始化的對象,以供隨時可以使用。對象池大多數(shù)場景下都是緩存著創(chuàng)建成本過高或者需要重復(fù)創(chuàng)建使用的對象,從池子中取對象的時間是可以預(yù)測的,但是新建一個對象的時間是不確定的。
當(dāng)需要一個新對象時,就向池中借出一個,然后對象池標(biāo)記當(dāng)前對象正在使用,使用完畢后歸還到對象池,以便再次借出。
常見的使用對象池化場景:
- 1. 對象創(chuàng)建成本過高。
- 2. 需要頻繁的創(chuàng)建大量重復(fù)對象,會產(chǎn)生很多內(nèi)存碎片。
- 3. 同時使用的對象不會太多。
- 4. 常見的具體場景如數(shù)據(jù)庫連接池、線程池等。
2. 為什么需要對象池
如果一個對象的創(chuàng)建成本很高,比如建立數(shù)據(jù)庫的連接時耗時過長,在不使用池化技術(shù)的情況下,我們的查詢過程可能是這樣的。
- 查詢 1:建立數(shù)據(jù)庫連接 -> 發(fā)起查詢 -> 收到響應(yīng) -> 關(guān)閉連接
- 查詢 2:建立數(shù)據(jù)庫連接 -> 發(fā)起查詢 -> 收到響應(yīng) -> 關(guān)閉連接
- 查詢 3:建立數(shù)據(jù)庫連接 -> 發(fā)起查詢 -> 收到響應(yīng) -> 關(guān)閉連接
在這種模式下,每次查詢都要重新建立關(guān)閉連接,因為建立連接是一個耗時的操作,所以這種模式會影響程序的總體性能。
那么使用池化思想是怎么樣的呢?同樣的過程會轉(zhuǎn)變成下面的步驟。
- 初始化:建立 N 個數(shù)據(jù)庫連接 -> 緩存起來
- 查詢 1:從緩存借到數(shù)據(jù)庫連接 -> 發(fā)起查詢 -> 收到響應(yīng) -> 歸還數(shù)據(jù)庫連接對象到緩存
- 查詢 2:從緩存借到數(shù)據(jù)庫連接 -> 發(fā)起查詢 -> 收到響應(yīng) -> 歸還數(shù)據(jù)庫連接對象到緩存
- 查詢 3:從緩存借到數(shù)據(jù)庫連接 -> 發(fā)起查詢 -> 收到響應(yīng) -> 歸還數(shù)據(jù)庫連接對象到緩存
使用池化思想后,數(shù)據(jù)庫連接并不會頻繁的創(chuàng)建關(guān)閉,而是啟動后就初始化了 N 個連接以供后續(xù)使用,使用完畢后歸還對象,這樣程序的總體性能得到提升。
3. 對象池的實現(xiàn)
通過上面的例子也可以發(fā)現(xiàn)池化思想的幾個關(guān)鍵步驟:初始化、借出、歸還。上面沒有展示銷毀步驟, 某些場景下還需要對象的銷毀這一過程,比如釋放連接。
下面我們手動實現(xiàn)一個簡陋的對象池,加深下對對象池的理解。主要是定一個對象池管理類,然后在里面實現(xiàn)對象的初始化、借出、歸還、銷毀等操作。
package?com.wdbyet.tool.objectpool.mypool; import?java.io.Closeable; import?java.io.IOException; import?java.util.HashSet; import?java.util.Stack; /** ?*?@author?https://www.wdbyte.com ?*/ public?class?MyObjectPool<T?extends?Closeable>?{ ????//?池子大小 ????private?Integer?size?=?5; ????//?對象池棧。后進(jìn)先出 ????private?Stack<T>?stackPool?=?new?Stack<>(); ????//?借出的對象的?hashCode?集合 ????private?HashSet<Integer>?borrowHashCodeSet?=?new?HashSet<>(); ????/** ?????*?增加一個對象 ?????* ?????*?@param?t ?????*/ ????public?synchronized?void?addObj(T?t)?{ ????????if?((stackPool.size()?+?borrowHashCodeSet.size())?==?size)?{ ????????????throw?new?RuntimeException("池中對象已經(jīng)達(dá)到最大值"); ????????} ????????stackPool.add(t); ????????System.out.println("添加了對象:"?+?t.hashCode()); ????} ????/** ?????*?借出一個對象 ?????* ?????*?@return ?????*/ ????public?synchronized?T?borrowObj()?{ ????????if?(stackPool.isEmpty())?{ ????????????System.out.println("沒有可以被借出的對象"); ????????????return?null; ????????} ????????T?pop?=?stackPool.pop(); ????????borrowHashCodeSet.add(pop.hashCode()); ????????System.out.println("借出了對象:"?+?pop.hashCode()); ????????return?pop; ????} ????/** ?????*?歸還一個對象 ?????* ?????*?@param?t ?????*/ ????public?synchronized?void?returnObj(T?t)?{ ????????if?(borrowHashCodeSet.contains(t.hashCode()))?{ ????????????stackPool.add(t); ????????????borrowHashCodeSet.remove(t.hashCode()); ????????????System.out.println("歸還了對象:"?+?t.hashCode()); ????????????return; ????????} ????????throw?new?RuntimeException("只能歸還從池中借出的對象"); ????} ????/** ?????*?銷毀池中對象 ?????*/ ????public?synchronized?void?destory()?{ ????????if?(!borrowHashCodeSet.isEmpty())?{ ????????????throw?new?RuntimeException("尚有未歸還的對象,不能關(guān)閉所有對象"); ????????} ????????while?(!stackPool.isEmpty())?{ ????????????T?pop?=?stackPool.pop(); ????????????try?{ ????????????????pop.close(); ????????????}?catch?(IOException?e)?{ ????????????????throw?new?RuntimeException(e); ????????????} ????????} ????????System.out.println("已經(jīng)銷毀了所有對象"); ????} }
代碼還是比較簡單的,只是簡單的示例,下面我們通過池化一個 Redis 連接對象 Jedis 來演示如何使用。
其實 Jedis 中已經(jīng)有對應(yīng)的 Jedis 池化管理對象了 JedisPool 了,不過我們這里為了演示對象池的實現(xiàn),就不使用官方提供的 JedisPool 了。
啟動一個 Redis 服務(wù)這里不做介紹,假設(shè)你已經(jīng)有了一個 Redis 服務(wù),下面引入 Java 中連接 Redis 需要用到的 Maven 依賴。
<dependency> ????<groupId>redis.clients</groupId> ????<artifactId>jedis</artifactId> ????<version>4.2.0</version> </dependency>
正常情況下 Jedis 對象的使用方式:
Jedis?jedis?=?new?Jedis("localhost",?6379); String?name?=?jedis.get("name"); System.out.println(name); jedis.close();
如果使用上面的對象池,就可以像下面這樣使用。
package?com.wdbyet.tool.objectpool.mypool; import?redis.clients.jedis.Jedis; /** ?*?@author?niulang ?*?@date?2022/07/02 ?*/ public?class?MyObjectPoolTest?{ ????public?static?void?main(String[]?args)?{ ????????MyObjectPool<Jedis>?objectPool?=?new?MyObjectPool<>(); ????????//?增加一個?jedis?連接對象 ????????objectPool.addObj(new?Jedis("127.0.0.1",?6379)); ????????objectPool.addObj(new?Jedis("127.0.0.1",?6379)); ????????//?從對象池中借出一個?jedis?對象 ????????Jedis?jedis?=?objectPool.borrowObj(); ????????//?一次?redis?查詢 ????????String?name?=?jedis.get("name"); ????????System.out.println(String.format("redis?get:"?+?name)); ????????//?歸還?redis?連接對象 ????????objectPool.returnObj(jedis); ????????//?銷毀對象池中的所有對象 ????????objectPool.destory(); ????????//?再次借用對象 ????????objectPool.borrowObj(); ????} }
輸出日志:
添加了對象:1556956098
添加了對象:1252585652
借出了對象:1252585652
redis get:www.wdbyte.com
歸還了對象:1252585652
已經(jīng)銷毀了所有對象
沒有可以被借出的對象
如果使用 JMH 對使用對象池化進(jìn)行 Redis 查詢,和正常創(chuàng)建 Redis 連接然后查詢關(guān)閉連接的方式進(jìn)行性能對比,會發(fā)現(xiàn)兩者的性能差異很大。下面是測試結(jié)果,可以發(fā)現(xiàn)使用對象池化后的性能是非池化方式的 5 倍左右。
Benchmark Mode Cnt Score Error Units
MyObjectPoolTest.test thrpt 15 2612.689 ± 358.767 ops/s
MyObjectPoolTest.testPool thrpt 9 12414.228 ± 11669.484 ops/s
4. 開源的對象池工具
上面自己實現(xiàn)的對象池總歸有些簡陋了,其實開源工具中已經(jīng)有了非常好用的對象池的實現(xiàn),如 Apache 的 commons-pool2
工具,很多開源工具中的對象池都是基于此工具實現(xiàn),下面介紹這個工具的使用方式。
maven 依賴:
<dependency> ????<groupId>org.apache.commons</groupId> ????<artifactId>commons-pool2</artifactId> ????<version>2.11.1</version> </dependency>
在 commons-pool2
對象池工具中有幾個關(guān)鍵的類。
- •
PooledObjectFactory
類是一個工廠接口,用于實現(xiàn)想要池化對象的創(chuàng)建、驗證、銷毀等操作。 - •
GenericObjectPool
類是一個通用的對象池管理類,可以進(jìn)行對象的借出、歸還等操作。 - •
GenericObjectPoolConfig
類是對象池的配置類,可以進(jìn)行對象的最大、最小等容量信息進(jìn)行配置。
下面通過一個具體的示例演示 commons-pool2
工具類的使用,這里依舊選擇 Redis 連接對象 Jedis 作為演示。
實現(xiàn) PooledObjectFactory
工廠類,實現(xiàn)其中的對象創(chuàng)建和銷毀方法。
public?class?MyPooledObjectFactory?implements?PooledObjectFactory<Jedis>?{ ????@Override ????public?void?activateObject(PooledObject<Jedis>?pooledObject)?throws?Exception?{ ????} ????@Override ????public?void?destroyObject(PooledObject<Jedis>?pooledObject)?throws?Exception?{ ????????Jedis?jedis?=?pooledObject.getObject(); ????????jedis.close(); ??????????System.out.println("釋放連接"); ????} ????@Override ????public?PooledObject<Jedis>?makeObject()?throws?Exception?{ ????????return?new?DefaultPooledObject(new?Jedis("localhost",?6379)); ????} ????@Override ????public?void?passivateObject(PooledObject<Jedis>?pooledObject)?throws?Exception?{ ????} ????@Override ????public?boolean?validateObject(PooledObject<Jedis>?pooledObject)?{ ????????return?false; ????} }
繼承 GenericObjectPool
類,實現(xiàn)對對象的借出、歸還等操作。
public?class?MyGenericObjectPool?extends?GenericObjectPool<Jedis>?{ ????public?MyGenericObjectPool(PooledObjectFactory?factory)?{ ????????super(factory); ????} ????public?MyGenericObjectPool(PooledObjectFactory?factory,?GenericObjectPoolConfig?config)?{ ????????super(factory,?config); ????} ????public?MyGenericObjectPool(PooledObjectFactory?factory,?GenericObjectPoolConfig?config, ????????AbandonedConfig?abandonedConfig)?{ ????????super(factory,?config,?abandonedConfig); ????} }
可以看到 MyGenericObjectPool
類的構(gòu)造函數(shù)中的入?yún)⒂?nbsp;GenericObjectPoolConfig
對象,這是個對象池的配置對象,可以配置對象池的容量大小等信息,這里就不配置了,使用默認(rèn)配置。
通過 GenericObjectPoolConfig
的源碼可以看到默認(rèn)配置中,對象池的容量是 8 個。
public?class?GenericObjectPoolConfig<T>?extends?BaseObjectPoolConfig<T>?{ ????/** ?????*?The?default?value?for?the?{@code?maxTotal}?configuration?attribute. ?????*?@see?GenericObjectPool#getMaxTotal() ?????*/ ????public?static?final?int?DEFAULT_MAX_TOTAL?=?8; ????/** ?????*?The?default?value?for?the?{@code?maxIdle}?configuration?attribute. ?????*?@see?GenericObjectPool#getMaxIdle() ?????*/ ????public?static?final?int?DEFAULT_MAX_IDLE?=?8;
下面編寫一個對象池使用測試類。
public?class?ApachePool?{ ????public?static?void?main(String[]?args)?throws?Exception?{ ????????MyGenericObjectPool?objectMyObjectPool?=?new?MyGenericObjectPool(new?MyPooledObjectFactory()); ????????Jedis?jedis?=?objectMyObjectPool.borrowObject(); ????????String?name?=?jedis.get("name"); ????????System.out.println(name); ????????objectMyObjectPool.returnObject(jedis); ????????objectMyObjectPool.close(); ????} }
輸出日志:
redis get:www.wdbyte.com
釋放連接
上面已經(jīng)演示了 commons-pool2
工具中的對象池的使用方式,從上面的例子中可以發(fā)現(xiàn)這種對象池中只能存放同一種初始化條件的對象,如果這里的 Redis 我們需要存儲一個本地連接和一個遠(yuǎn)程連接的兩種 Jedis 對象,就不能滿足了。那么怎么辦呢?
其實 commons-pool2
工具已經(jīng)考慮到了這種情況,通過增加一個 key 值可以在同一個對象池管理中進(jìn)行區(qū)分,代碼和上面類似,直接貼出完整的代碼實現(xiàn)。
package?com.wdbyet.tool.objectpool.apachekeyedpool; import?org.apache.commons.pool2.BaseKeyedPooledObjectFactory; import?org.apache.commons.pool2.KeyedPooledObjectFactory; import?org.apache.commons.pool2.PooledObject; import?org.apache.commons.pool2.impl.AbandonedConfig; import?org.apache.commons.pool2.impl.DefaultPooledObject; import?org.apache.commons.pool2.impl.GenericKeyedObjectPool; import?org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig; import?redis.clients.jedis.Jedis; /** ?*?@author?https://www.wdbyte.com ?*?@date?2022/07/07 ?*/ public?class?ApacheKeyedPool?{ ????public?static?void?main(String[]?args)?throws?Exception?{ ????????String?key?=?"local"; ????????MyGenericKeyedObjectPool?objectMyObjectPool?=?new?MyGenericKeyedObjectPool(new?MyKeyedPooledObjectFactory()); ????????Jedis?jedis?=?objectMyObjectPool.borrowObject(key); ????????String?name?=?jedis.get("name"); ????????System.out.println("redis?get?:"?+?name); ????????objectMyObjectPool.returnObject(key,?jedis); ????} } class?MyKeyedPooledObjectFactory?extends?BaseKeyedPooledObjectFactory<String,?Jedis>?{ ????@Override ????public?Jedis?create(String?key)?throws?Exception?{ ????????if?("local".equals(key))?{ ????????????return?new?Jedis("localhost",?6379); ????????} ????????if?("remote".equals(key))?{ ????????????return?new?Jedis("192.168.0.105",?6379); ????????} ????????return?null; ????} ????@Override ????public?PooledObject<Jedis>?wrap(Jedis?value)?{ ????????return?new?DefaultPooledObject<>(value); ????} } class?MyGenericKeyedObjectPool?extends?GenericKeyedObjectPool<String,?Jedis>?{ ????public?MyGenericKeyedObjectPool(KeyedPooledObjectFactory<String,?Jedis>?factory)?{ ????????super(factory); ????} ????public?MyGenericKeyedObjectPool(KeyedPooledObjectFactory<String,?Jedis>?factory, ????????GenericKeyedObjectPoolConfig<Jedis>?config)?{ ????????super(factory,?config); ????} ????public?MyGenericKeyedObjectPool(KeyedPooledObjectFactory<String,?Jedis>?factory, ????????GenericKeyedObjectPoolConfig<Jedis>?config,?AbandonedConfig?abandonedConfig)?{ ????????super(factory,?config,?abandonedConfig); ????} }
輸出日志:
redis get :www.wdbyte.com
5. JedisPool 對象池實現(xiàn)分析
這篇文章中的演示都使用了 Jedis 連接對象,其實在 Jedis SDK 中已經(jīng)實現(xiàn)了相應(yīng)的對象池,也就是我們常用的 JedisPool 類。那么這里的 JedisPool 是怎么實現(xiàn)的呢?我們先看一下 JedisPool 的使用方式。
package?com.wdbyet.tool.objectpool; import?redis.clients.jedis.Jedis; import?redis.clients.jedis.JedisPool; /** ?*?@author?https://www.wdbyte.com ?*/ public?class?JedisPoolTest?{ ????public?static?void?main(String[]?args)?{ ????????JedisPool?jedisPool?=?new?JedisPool("localhost",?6379); ????????//?從對象池中借一個對象 ????????Jedis?jedis?=?jedisPool.getResource(); ????????String?name?=?jedis.get("name"); ????????System.out.println("redis?get?:"?+?name); ????????jedis.close(); ????????//?徹底退出前,關(guān)閉?Redis?連接池 ????????jedisPool.close(); ????} }
代碼中添加了注釋,可以看到通過 jedisPool.getResource()
拿到了一個對象,這里和上面 commons-pool2
工具中的 borrowObject
十分相似,繼續(xù)追蹤它的代碼實現(xiàn)可以看到下面的代碼。
//?redis.clients.jedis.JedisPool //?public?class?JedisPool?extends?Pool<Jedis>?{ public?Jedis?getResource()?{ ????Jedis?jedis?=?(Jedis)super.getResource(); ????jedis.setDataSource(this); ????return?jedis; } //?繼續(xù)追蹤?super.getResource() //?redis.clients.jedis.util.Pool public?T?getResource()?{ ????try?{ ????????return?super.borrowObject(); ????}?catch?(JedisException?var2)?{ ????????throw?var2; ????}?catch?(Exception?var3)?{ ????????throw?new?JedisException("Could?not?get?a?resource?from?the?pool",?var3); ????} }
竟然看到了 super.borrowObject()
,多么熟悉的方法,繼續(xù)分析代碼可以發(fā)現(xiàn) Jedis 對象池也是使用了 commons-pool2
工具作為實現(xiàn)。既然如此,那么 jedis.close()
方法的邏輯我們應(yīng)該也可以猜到了,應(yīng)該有一個歸還的操作,查看代碼發(fā)現(xiàn)果然如此。
//?redis.clients.jedis.JedisPool //?public?class?JedisPool?extends?Pool<Jedis>?{ public?void?close()?{ ????if?(this.dataSource?!=?null)?{ ????????Pool<Jedis>?pool?=?this.dataSource; ????????this.dataSource?=?null; ????????if?(this.isBroken())?{ ????????????pool.returnBrokenResource(this); ????????}?else?{ ????????????pool.returnResource(this); ????????} ????}?else?{ ????????this.connection.close(); ????} } //?繼續(xù)追蹤?super.getResource() //?redis.clients.jedis.util.Pool public?void?returnResource(T?resource)?{ ????if?(resource?!=?null)?{ ????????try?{ ????????????super.returnObject(resource); ????????}?catch?(RuntimeException?var3)?{ ????????????throw?new?JedisException("Could?not?return?the?resource?to?the?pool",?var3); ????????} ????} }
通過上面的分析,可見 Jedis 確實使用了 commons-pool2
工具進(jìn)行對象池的管理,通過分析 JedisPool 類的繼承關(guān)系圖也可以發(fā)現(xiàn)。
JedisPool 繼承關(guān)系
6. 對象池總結(jié)
通過這篇文章的介紹,可以發(fā)現(xiàn)池化思想有幾個明顯的優(yōu)勢。
- 1. 可以顯著的提高應(yīng)用程序的性能。
- 2. 如果一個對象創(chuàng)建成本過高,那么使用池化非常有效。
- 3. 池化提供了一種對象的管理以及重復(fù)使用的方式,減少內(nèi)存碎片。
- 4. 可以為對象的創(chuàng)建數(shù)量提供限制,對某些對象不能創(chuàng)建過多的場景提供保護(hù)。
但是使用對象池化也有一些需要注意的地方,比如歸還對象時應(yīng)確保對象已經(jīng)被重置為可以重復(fù)使用的狀態(tài)。同時也要注意,使用池化時要根據(jù)具體的場景合理的設(shè)置池子的大小,過小達(dá)不到想要的效果,過大會造成內(nèi)存浪費(fèi)。
以上就是一文搞懂Java中對象池的實現(xiàn)的詳細(xì)內(nèi)容,更多關(guān)于Java對象池的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
SpringBoot里使用Servlet進(jìn)行請求的實現(xiàn)示例
這篇文章主要介紹了SpringBoot里使用Servlet進(jìn)行請求的實現(xiàn)示例,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2021-01-01